Skip to content

Commit f12a207

Browse files
committed
finish multiple state updates
1 parent f0960ba commit f12a207

File tree

9 files changed

+247
-15
lines changed

9 files changed

+247
-15
lines changed

docs/examples.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def get_example_files_by_name(
5858
) -> list[Path]:
5959
path = _get_root_example_path_by_name(name, relative_to)
6060
if path.is_dir():
61-
return list(path.glob("*"))
61+
return [p for p in path.glob("*") if not p.is_dir()]
6262
else:
6363
path = path.with_suffix(".py")
6464
return [path] if path.exists() else []
@@ -163,12 +163,12 @@ def _make_example_did_not_run(example_name):
163163
def ExampleDidNotRun():
164164
return idom.html.code(f"Example {example_name} did not run")
165165

166-
return ExampleDidNotRun
166+
return ExampleDidNotRun()
167167

168168

169169
def _make_error_display(message):
170170
@idom.component
171171
def ShowError():
172172
return idom.html.pre(message)
173173

174-
return ShowError
174+
return ShowError()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
from pathlib import Path
2+
from typing import NamedTuple
3+
4+
from idom import component, html, run, use_state
5+
from idom.widgets import image
6+
7+
8+
HERE = Path(__file__)
9+
CHARACTER_IMAGE = (HERE.parent / "static" / "bunny.png").read_bytes()
10+
11+
12+
class Position(NamedTuple):
13+
x: int
14+
y: int
15+
angle: int
16+
17+
18+
def rotate(degrees):
19+
return lambda old_position: Position(
20+
old_position.x,
21+
old_position.y,
22+
old_position.angle + degrees,
23+
)
24+
25+
26+
def translate(x=0, y=0):
27+
return lambda old_position: Position(
28+
old_position.x + x,
29+
old_position.y + y,
30+
old_position.angle,
31+
)
32+
33+
34+
@component
35+
def Scene():
36+
actions, set_actions = use_state(())
37+
position, set_position = use_state(Position(100, 100, 0))
38+
39+
def handle_apply_actions(event):
40+
for act_function in actions:
41+
set_position(act_function)
42+
set_actions(())
43+
44+
def make_action_handler(act_function):
45+
return lambda event: set_actions(actions + (act_function,))
46+
47+
return html.div(
48+
{"style": {"width": "225px"}},
49+
html.div(
50+
{
51+
"style": {
52+
"width": "200px",
53+
"height": "200px",
54+
"backgroundColor": "slategray",
55+
}
56+
},
57+
image(
58+
"png",
59+
CHARACTER_IMAGE,
60+
{
61+
"style": {
62+
"position": "relative",
63+
"left": f"{position.x}px",
64+
"top": f"{position.y}.px",
65+
"transform": f"rotate({position.angle}deg) scale(2, 2)",
66+
}
67+
},
68+
),
69+
),
70+
html.button({"onClick": make_action_handler(translate(x=-10))}, "Move Left"),
71+
html.button({"onClick": make_action_handler(translate(x=10))}, "Move Right"),
72+
html.button({"onClick": make_action_handler(translate(y=-10))}, "Move Up"),
73+
html.button({"onClick": make_action_handler(translate(y=10))}, "Move Down"),
74+
html.button({"onClick": make_action_handler(rotate(-30))}, "Rotate Left"),
75+
html.button({"onClick": make_action_handler(rotate(30))}, "Rotate Right"),
76+
html.button({"onClick": handle_apply_actions}, html.b("Apply Actions")),
77+
)
78+
79+
80+
run(Scene)
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from idom import component, html, run, use_state
2+
3+
4+
@component
5+
def ColorButton():
6+
color, set_color = use_state("gray")
7+
8+
def handle_click(event):
9+
set_color("orange")
10+
set_color("pink")
11+
set_color("blue")
12+
13+
return html.button(
14+
{"onClick": handle_click, "style": {"backgroundColor": color}}, "Set Color"
15+
)
16+
17+
18+
run(ColorButton)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from idom import component, html, run, use_state
2+
3+
4+
def increment(old_number):
5+
new_number = old_number + 1
6+
return new_number
7+
8+
9+
@component
10+
def Counter():
11+
number, set_number = use_state(0)
12+
13+
def handle_click(event):
14+
set_number(increment)
15+
set_number(increment)
16+
set_number(increment)
17+
18+
return html.div(
19+
html.h1(number),
20+
html.button({"onClick": handle_click}, "Increment"),
21+
)
22+
23+
24+
run(Counter)

docs/source/adding-interactivity/compounding-state-updates.rst

-6
This file was deleted.

docs/source/adding-interactivity/index.rst

+5-5
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Adding Interactivity
77
responding-to-events
88
components-with-state
99
state-as-a-snapshot
10-
compounding-state-updates
10+
multiple-state-updates
1111
dangers-of-mutability
1212

1313

@@ -39,8 +39,8 @@ Adding Interactivity
3939
Learn why IDOM does not change component state the moment it is set, but
4040
instead schedules a re-render.
4141

42-
.. grid-item-card:: :octicon:`versions` Compounding State Updates
43-
:link: compounding-state-updates
42+
.. grid-item-card:: :octicon:`versions` Multiple State Updates
43+
:link: multiple-state-updates
4444
:link-type: doc
4545

4646
Under construction 🚧
@@ -151,11 +151,11 @@ snapshot.
151151
schedules a re-render.
152152

153153

154-
Section 4: Compounding State Updates
154+
Section 4: Multiple State Updates
155155
------------------------------------
156156

157157
.. card::
158-
:link: compounding-state-updates
158+
:link: multiple-state-updates
159159
:link-type: doc
160160

161161
:octicon:`book` Read More
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
Multiple State Updates
2+
======================
3+
4+
Setting a state variable will queue another render. But sometimes you might want to
5+
perform multiple operations on the value before queueing the next render. To do this, it
6+
helps to understand how React batches state updates.
7+
8+
9+
Batched Updates
10+
---------------
11+
12+
As we learned :ref:`previously <state as a snapshot>`, state variables remain fixed
13+
inside each render as if state were a snapshot taken at the begining of each render.
14+
This is why, in the example below, even though it might seem like clicking the
15+
"Increment" button would cause the ``number`` to increase by ``3``, it only does by
16+
``1``:
17+
18+
.. idom:: _examples/set_counter_3_times
19+
20+
The reason this happens is because, so long as the event handler is synchronous (i.e.
21+
the event handler is not an ``async`` function), IDOM waits until all the code in an
22+
event handler has run before processing state and starting the next render. Thus, it's
23+
the last call to a given state setter that matters. In the example below, even though we
24+
set the color of the button to ``"orange"`` and then ``"pink"`` before ``"blue"``,
25+
the color does not quickly flash orange and pink before blue - it alway remains blue:
26+
27+
.. idom:: _examples/set_color_3_times
28+
29+
This behavior let's you make multiple state changes without triggering unnecessary
30+
renders or renders with inconsistent state where only some of the variables have been
31+
updated. With that said, it also means that the UI won't change until after synchronous
32+
handlers have finished running.
33+
34+
.. note::
35+
36+
For asynchronous event handlers, IDOM will not render until you ``await`` something.
37+
As we saw in :ref:`prior examples <State And Delayed Reactions>`, if you introduce
38+
an asynchronous delay to an event handler after changing state, renders may take
39+
place before the remainder of the event handler completes. However, state variables
40+
within handlers, even async ones, always remains static.
41+
42+
This behavior of IDOM to "batch" state changes that take place inside a single event
43+
handler, do not extend across event handlers. In other words, distinct events will
44+
always produce distinct renders. For example, if clicking a button increments a counter
45+
by one, no matter how fast the user clicks, the view will never jump from 1 to 3 - it
46+
will always display 1, then 2, and then 3.
47+
48+
49+
Incremental Updates
50+
-------------------
51+
52+
While it's uncommon, you need to update a state variable more than once before the next
53+
render. In these cases, instead of having updates batched, you instead want them to be
54+
applied incrementally. That is, the next update can be made to depend on the prior one.
55+
For example, what it we wanted to make it so that, in our ``Counter`` example :ref:`from
56+
before <Batched Updates>`, each call to ``set_number`` did in fact increment
57+
``number`` by one causing the view to display ``0``, then ``3``, then ``6``, and so on?
58+
59+
To accomplish this, instead of passing the next state value as in ``set_number(number +
60+
1)``, we may pass an **"updater function"** to ``set_number`` that computes the next
61+
state based on the previous state. This would look like ``set_number(lambda number:
62+
number + 1)``. In other words we need a function of the form:
63+
64+
.. code-block::
65+
66+
def compute_new_state(old_state):
67+
...
68+
return new_state
69+
70+
In our case, ``new_state = old_state + 1``. So we might define:
71+
72+
.. code-block::
73+
74+
def increment(old_number):
75+
new_number = old_number + 1
76+
return new_number
77+
78+
Which we can use to replace ``set_number(number + 1)`` with ``set_number(increment)``:
79+
80+
.. idom:: _examples/set_state_function
81+
82+
The way to think about how IDOM runs though this series of ``set_state(increment)``
83+
calls is to imagine that each one updates the internally managed state with its return
84+
value, then that return value is being passed to the next updater function. Ultimately,
85+
this is functionally equivalent to the following:
86+
87+
.. code-block::
88+
89+
set_number(increment(increment(increment(number))))
90+
91+
So why might you want to do this? Well, in this case, we're using the same function on
92+
each call to ``set_number``, but what if you had more function. Perhaps you might want
93+
to do introduce ``squared`` or ``decrement`` functions:
94+
95+
.. code-block::
96+
97+
set_number(increment)
98+
set_number(squared)
99+
set_number(decrement)
100+
101+
Which would equate to:
102+
103+
.. code-block::
104+
105+
set_number(decrement(squared(increment(number))))
106+
107+
This example also presents a scenario with much simpler state. Consider an example where
108+
the state is more complex. In the scenario below, we need to represent and manipulate
109+
state that represents the position of a character in a scene. Then imagine that we want
110+
to allow the user to queue their actions and then apply them all at once. The simplest
111+
way to do this is to factor out the functions which manualuate this state into
112+
functions, add them to an ``actions`` queue when the user requests, and finally, once
113+
the user clicks "Apply Actions", iterator over the ``actions`` and call ``set_position``
114+
with each action function:
115+
116+
.. idom:: _examples/character_movement

docs/source/adding-interactivity/state-as-a-snapshot.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ render. As a result, you don't need to worry about whether state has changed whi
148148
code in an event handler is running.
149149

150150
.. card::
151-
:link: compounding-state-updates
151+
:link: multiple-state-updates
152152
:link-type: doc
153153

154154
:octicon:`book` Read More

0 commit comments

Comments
 (0)