Skip to content

Commit 1c1e573

Browse files
committed
add some diagrams for state as snapshot
1 parent f8b0b99 commit 1c1e573

File tree

15 files changed

+179
-72
lines changed

15 files changed

+179
-72
lines changed

docs/examples.py

Lines changed: 41 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ def all_example_names() -> set[str]:
3535

3636

3737
def load_one_example(file_or_name: Path | str) -> Callable[[], ComponentType]:
38+
return lambda: (
39+
# we do this to ensure each instance is fresh
40+
_load_one_example(file_or_name)
41+
)
42+
43+
44+
def _load_one_example(file_or_name: Path | str) -> ComponentType:
3845
if isinstance(file_or_name, str):
3946
file = get_main_example_file_by_name(file_or_name)
4047
else:
@@ -43,8 +50,14 @@ def load_one_example(file_or_name: Path | str) -> Callable[[], ComponentType]:
4350
if not file.exists():
4451
raise FileNotFoundError(str(file))
4552

53+
print_buffer = _PrintBuffer()
54+
55+
def capture_print(*args, **kwargs):
56+
buffer = StringIO()
57+
print(*args, file=buffer, **kwargs)
58+
print_buffer.write(buffer.getvalue())
59+
4660
captured_component_constructor = None
47-
capture_print, ShowPrint = _printout_viewer()
4861

4962
def capture_component(component_constructor):
5063
nonlocal captured_component_constructor
@@ -68,13 +81,18 @@ def capture_component(component_constructor):
6881

6982
if captured_component_constructor is None:
7083
return _make_example_did_not_run(str(file))
71-
else:
7284

73-
@idom.component
74-
def Wrapper():
75-
return idom.html.div(captured_component_constructor(), ShowPrint())
85+
@idom.component
86+
def Wrapper():
87+
return idom.html.div(captured_component_constructor(), PrintView())
7688

77-
return Wrapper
89+
@idom.component
90+
def PrintView():
91+
text, set_text = idom.hooks.use_state(print_buffer.getvalue())
92+
print_buffer.set_callback(set_text)
93+
return idom.html.pre({"class": "printout"}, text) if text else idom.html.div()
94+
95+
return Wrapper()
7896

7997

8098
def get_main_example_file_by_name(name: str) -> Path:
@@ -98,44 +116,26 @@ def _get_root_example_path_by_name(name: str) -> Path:
98116
return EXAMPLES_DIR.joinpath(*name.split("/"))
99117

100118

101-
def _printout_viewer():
102-
print_callbacks: set[Callable[[str], None]] = set()
119+
class _PrintBuffer:
120+
def __init__(self, max_lines: int = 10):
121+
self._callback = None
122+
self._lines = ()
123+
self._max_lines = max_lines
103124

104-
@idom.component
105-
def ShowPrint():
106-
lines, set_lines = idom.hooks.use_state(())
107-
108-
def set_buffer(text: str):
109-
if len(lines) > 10:
110-
# limit printout size - protects against malicious actors
111-
# plus it gives you some nice scrolling printout
112-
set_lines(lines[1:] + (text,))
113-
else:
114-
set_lines(lines + (text,))
115-
116-
@idom.hooks.use_effect(args=[set_buffer])
117-
def add_set_buffer_callback():
118-
print_callbacks.add(set_buffer)
119-
return lambda: print_callbacks.remove(set_buffer)
120-
121-
if not lines:
122-
return idom.html.div()
123-
else:
124-
return idom.html.pre({"class": "printout"}, "".join(lines))
125+
def set_callback(self, function: Callable[[str], None]) -> None:
126+
self._callback = function
127+
return None
125128

126-
def capture_print(*args, **kwargs):
127-
buffer = StringIO()
128-
print(*args, file=buffer, **kwargs)
129-
value = buffer.getvalue()
130-
for cb in print_callbacks:
131-
cb(value)
132-
133-
return capture_print, ShowPrint
129+
def getvalue(self) -> str:
130+
return "".join(self._lines)
134131

135-
136-
def _use_force_update():
137-
toggle, set_toggle = idom.hooks.use_state(False)
138-
return lambda: set_toggle(not toggle)
132+
def write(self, text: str) -> None:
133+
if len(self._lines) == self._max_lines:
134+
self._lines = self._lines[1:] + (text,)
135+
else:
136+
self._lines += (text,)
137+
if self._callback is not None:
138+
self._callback(self.getvalue())
139139

140140

141141
def _make_example_did_not_run(example_name):
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import asyncio
2+
3+
from idom import component, event, html, run, use_state
4+
5+
6+
@component
7+
def App():
8+
recipient, set_recipient = use_state("Alice")
9+
message, set_message = use_state("")
10+
11+
@event(prevent_default=True)
12+
async def handle_submit(event):
13+
set_message("")
14+
print("About to send message...")
15+
await asyncio.sleep(5)
16+
print(f"Sent '{message}' to {recipient}")
17+
18+
return html.form(
19+
{"onSubmit": handle_submit, "style": {"display": "inline-grid"}},
20+
html.label(
21+
"To: ",
22+
html.select(
23+
{
24+
"value": recipient,
25+
"onChange": lambda event: set_recipient(event["value"]),
26+
},
27+
html.option({"value": "Alice"}, "Alice"),
28+
html.option({"value": "Bob"}, "Bob"),
29+
),
30+
),
31+
html.input(
32+
{
33+
"type": "text",
34+
"placeholder": "Your message...",
35+
"value": message,
36+
"onChange": lambda event: set_message(event["value"]),
37+
}
38+
),
39+
html.button({"type": "submit"}, "Send"),
40+
)
41+
42+
43+
run(App)

docs/source/_exts/widget_example.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class WidgetExample(SphinxDirective):
2525
}
2626

2727
def run(self):
28+
print(self.get_source_info())
2829
example_name = self.arguments[0]
2930
show_linenos = "linenos" in self.options
3031
live_example_is_default_tab = "result-is-default-tab" in self.options

docs/source/_static/custom.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1483,7 +1483,17 @@ const targetTransformCategories = {
14831483
};
14841484

14851485
const targetTagCategories = {
1486-
hasValue: ["BUTTON", "INPUT", "OPTION", "LI", "METER", "PROGRESS", "PARAM"],
1486+
hasValue: [
1487+
"BUTTON",
1488+
"INPUT",
1489+
"OPTION",
1490+
"LI",
1491+
"METER",
1492+
"PROGRESS",
1493+
"PARAM",
1494+
"SELECT",
1495+
"TEXTAREA",
1496+
],
14871497
hasCurrentTime: ["AUDIO", "VIDEO"],
14881498
hasFiles: ["INPUT"],
14891499
};
@@ -1941,7 +1951,9 @@ function mountLayoutWithReconnectingWebSocket(
19411951
mountState
19421952
);
19431953

1944-
console.info(`IDOM WebSocket connection lost. Reconnecting in ${reconnectTimeout} seconds...`);
1954+
console.info(
1955+
`IDOM WebSocket connection lost. Reconnecting in ${reconnectTimeout} seconds...`
1956+
);
19451957

19461958
setTimeout(function () {
19471959
mountState.reconnectAttempts++;
Loading
Loading

docs/source/adding-interactivity/components-with-state.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ below highlights a line of code where something of interest occurs:
224224

225225
.. raw:: html
226226

227-
<h2>Event handler triggers</h2>
227+
<h2>New state is set</h2>
228228

229229
.. literalinclude:: /_examples/adding_interactivity/adding_state_variable/app.py
230230
:lines: 12-33

docs/source/adding-interactivity/index.rst

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ Adding Interactivity
3636
:link: state-as-a-snapshot
3737
:link-type: doc
3838

39-
Under construction 🚧
39+
Learn why IDOM does not change component state the moment it is set, but
40+
instead schedules a re-render.
4041

4142
.. grid-item-card:: :octicon:`issue-opened` Dangers of Mutability
4243
:link: dangers-of-mutability
@@ -112,14 +113,32 @@ Section 3: State as a Snapshot
112113

113114
As we :ref:`learned earlier <Components with State>`, state setters behave a little
114115
differently than you might exepct at first glance. Instead of updating your current
115-
handle on the corresponding state variable it schedules a re-render of the component
116-
which owns the state:
116+
handle on the setter's corresponding variable, it schedules a re-render of the component
117+
which owns the state.
117118

118119
.. code-block::
119120
120-
print(count)
121-
set_count(count + 1)
122-
print(count)
121+
count, set_count = use_state(0)
122+
print(count) # prints: 0
123+
set_count(count + 1) # schedule a re-render where count is 1
124+
print(count) # still prints: 0
125+
126+
This behavior of IDOM means that each render of a component is like taking a snapshot of
127+
the UI based on the component's state at that time. Treating state in this way can help
128+
reduce subtle bugs. For example, in the code below there's a simple chat app with a
129+
message input and recipient selector. The catch is that the message actually gets sent 5
130+
seconds after the "Send" button is clicked. So what would happen if we changed the
131+
recipient between the time the "Send" button was clicked and the moment the message is
132+
actually sent?
133+
134+
.. example:: adding_interactivity/print_chat_message
135+
:activate-result:
136+
137+
As it turns out, changing the message recipient after pressing send does not change
138+
where the message ulitmately goes. However, one could imagine a bug where the recipient
139+
of a message is determined at the time the message is sent rather than at the time the
140+
"Send" button it clicked. In many cases, IDOM avoids this class of bug entirely because
141+
it treats state as a snapshot.
123142

124143
.. card::
125144
:link: state-as-a-snapshot
Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
State as a Snapshot
22
===================
33

4-
While you can read state variables like you might normally in Python, as we
5-
:ref:`learned earlier <Components with State>`, assigning to them is somewhat different.
6-
When you use a state setter to update a component, instead of modifying your handle to
7-
its corresponding state variable, a re-render is triggered. Only after that next render
8-
begins will things change.
4+
When you watch the user interfaces you build change as you interact with them, it's easy
5+
to imagining that they do so because there's some bit of code that modifies the relevant
6+
parts of the view directly. For example, you may think that when a user clicks a "Send"
7+
button, there's code which reaches into the view and adds some text saying "Message
8+
sent!":
9+
10+
.. image:: _static/direct-state-change.png
11+
12+
13+
14+
.. image:: _static/idom-state-change.png

docs/source/creating-interfaces/your-first-components.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ At their core, components are just normal Python functions that return HTML. To
1515
component you just need to add a ``@component`` `decorator
1616
<https://realpython.com/primer-on-python-decorators/>`__ to a function. Functions
1717
decorator in this way are known as **render function** and, by convention, we name them
18-
like classes - with ``CamelCase``. So for example, if we wanted to write, and then
19-
:ref:`display <Running IDOM>` a ``Photo`` component, we might write:
18+
like classes - with ``CamelCase``. So consider what we would do if we wanted to write,
19+
and then :ref:`display <Running IDOM>` a ``Photo`` component:
2020

2121
.. example:: creating_interfaces/simple_photo
2222
:activate-result:

0 commit comments

Comments
 (0)