Skip to content

Commit e986efe

Browse files
authored
reactive-mutable: tweak problem/solution examples (#151)
* reactive-mutable: tweak problem/solution examples * add section on python operations that create copies * simplyify css to just what we need to override * use render.code in the other examples, too
1 parent d0a23fe commit e986efe

File tree

1 file changed

+72
-8
lines changed

1 file changed

+72
-8
lines changed

docs/reactive-mutable.qmd

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,28 @@ b
7373

7474
The advantage to this approach is not eagerly creating defensive copies all the time, as we must in the "copy on assignment" approach. However, if you are performing more updates than assignments, this approach actually makes _more_ copies, plus it gives you more opportunities to slip up and forget not to mutate the object.
7575

76+
### Python operations that create copies
77+
78+
We've seen that `x + [value]` creates a new list object and that `x.copy()` creates a new list object.
79+
There are some other common operations that create copies.
80+
You can use these patterns to avoid mutating reactive values in place.
81+
82+
1. **List comprehensions**:
83+
`[x for x in a]` creates a new list with the same elements as `a`.
84+
This approach is particularly useful when you need to transform the elements of a list in some way, as in `[x*2 for x in a]`.
85+
86+
2. **Slicing**:
87+
`a[:]` creates a new list with the same elements as `a`.
88+
This is useful when you need to copy the entire list, or a subset of it.
89+
90+
3. **Star operator**:
91+
`[*a, value]` creates a new list with the same elements as `a`, with the additional `value` appended after them.
92+
This is an easy way to add a single element to the end or start of a list (`[value, *a]`).
93+
94+
4. **Double star operator**:
95+
`{**a, key: value}` creates a new dictionary with the same key-value pairs as `a`, with the additional key-value pair `key: value` added.
96+
This is an easy way to add a single key-value pair to a dictionary.
97+
7698
### Use immutable objects
7799

78100
The third way is to use a different data structure entirely. Instead of list, we will use tuple, which is immutable. Immutable objects do not provide any way to change their values "in place", even if we wanted to. Therefore, we can be confident that nothing we do to tuple variable `a` could ever affect tuple variable `b`.
@@ -112,6 +134,7 @@ The rest of this article demonstrates these problems, and their solutions, in th
112134

113135
This demo app demonstrates that when an object that is stored in a `reactive.value` is mutated, the change is not visible to the `reactive.value` and no reactive invalidation occurs. Below, the `add_value_to_list` effect retrieves the list stored in `user_provided_values` and appends an item to it.
114136

137+
::: {.callout-warning title="Problem: No reactive update" appearance="simple"}
115138
```{shinylive-python}
116139
#| standalone: true
117140
#| components: [editor, viewer]
@@ -124,7 +147,7 @@ from shiny.express import input, render, ui
124147
ui.input_numeric("x", "Enter a value to add to the list:", 1)
125148
ui.input_action_button("submit", "Add Value")
126149
127-
@render.text
150+
@render.code
128151
def out():
129152
return f"Values: {user_provided_values()}"
130153
@@ -134,14 +157,17 @@ user_provided_values = reactive.value([])
134157
@reactive.effect
135158
@reactive.event(input.submit)
136159
def add_value_to_list():
160+
# WATCHOUT! This doesn't work as expected!
137161
values = user_provided_values()
138162
values.append(input.x())
139163
```
164+
:::
140165

141166
Each time the button is clicked, a new item is added to the list; but the ` reactive.value` has no way to know anything has changed. (Surprisingly, even adding `user_provided_values.set(values)` to the end of `add_value_to_list` will not help; the reactive value will see that the identity of the new object is the same as its existing object, and ignore the change.)
142167

143168
Switching to the "copy on update" technique fixes the problem. The app below is identical to the one above, except for the body of `add_value_to_list`. Click on the button a few times--the results now appear correctly.
144169

170+
::: {.callout-tip title="Solution: Copy on update" appearance="simple"}
145171
```{shinylive-python}
146172
#| standalone: true
147173
#| components: [editor, viewer]
@@ -154,7 +180,7 @@ from shiny.express import input, render, ui
154180
ui.input_numeric("x", "Enter a value to add to the list:", 1)
155181
ui.input_action_button("submit", "Add Value")
156182
157-
@render.text
183+
@render.code
158184
def out():
159185
return f"Values: {user_provided_values()}"
160186
@@ -164,13 +190,20 @@ user_provided_values = reactive.value([])
164190
@reactive.effect
165191
@reactive.event(input.submit)
166192
def add_value_to_list():
193+
# This works by creating a new list object
167194
user_provided_values.set(user_provided_values() + [input.x()])
168195
```
196+
:::
169197

170198
### Example 2: Leaky changes
171199

172-
Let's further modify our example; now, we will output not just the values entered by the user, but also a parallel list of those values after being doubled. This example is the same as the last one, with the addition of the `@reactive.calc` called `doubled_values`, which is then included in the text output. Click the button a few times, and you'll see that something is amiss.
200+
Let's further modify our example; now, we will output not just the values entered by the user, but also a parallel list of those values after being doubled. This example is the same as the last one, with the addition of the `@reactive.calc` called `doubled_values`, which is then included in the text output.
201+
202+
In the example below, if you click the button three times, you'd expect the user values to be `[1, 1, 1]` and the doubled values to be `[2, 2, 2]`.
203+
Click the button below three times.
204+
What values do you actually get?
173205

206+
::: {.callout-warning title="Problem: Mutating in place" appearance="simple"}
174207
```{shinylive-python}
175208
#| standalone: true
176209
#| components: [editor, viewer]
@@ -183,9 +216,9 @@ from shiny.express import input, render, ui
183216
ui.input_numeric("x", "Enter a value to add to the list:", 1)
184217
ui.input_action_button("submit", "Add Value")
185218
186-
@render.text
219+
@render.code
187220
def out():
188-
return f"Raw Values: {user_provided_values()}\n" + f"Doubled: {doubled_values()}"
221+
return f"User Values: {user_provided_values()}\n" + f"Doubled: {doubled_values()}"
189222
190223
# Stores all the values the user has submitted so far
191224
user_provided_values = reactive.value([])
@@ -202,9 +235,16 @@ def doubled_values():
202235
values[i] *= 2
203236
return values
204237
```
238+
:::
205239

206-
This is because `doubled_values` does its doubling by modifying the values of the list in place, causing these changes to "leak" back into `user_provided_values`. We could fix this by having `doubled_values` call `user_provided_values().copy()`, or by using a list comprehension (since it creates a new list and leaves the old one alone).
240+
By the third click, the user input that should be `[1, 1, 1]` is instead `[4, 2, 1]`!
241+
This is because `doubled_values` does its doubling by modifying the values of the list in place, causing these changes to "leak" back into `user_provided_values`.
207242

243+
We could fix this by having `doubled_values` call `user_provided_values().copy()`.
244+
Or we can use a list comprehension, which creates a new list in the process.
245+
The second option is shown below.
246+
247+
::: {.callout-tip title="Solution: Copy with list comprehension" appearance="simple"}
208248
```{shinylive-python}
209249
#| standalone: true
210250
#| components: [editor, viewer]
@@ -217,9 +257,9 @@ from shiny.express import input, render, ui
217257
ui.input_numeric("x", "Enter a value to add to the list:", 1)
218258
ui.input_action_button("submit", "Add Value")
219259
220-
@render.text
260+
@render.code
221261
def out():
222-
return f"Raw Values: {user_provided_values()}\n" + f"Doubled: {doubled_values()}"
262+
return f"User Values: {user_provided_values()}\n" + f"Doubled: {doubled_values()}"
223263
224264
# Stores all the values the user has submitted so far
225265
user_provided_values = reactive.value([])
@@ -233,3 +273,27 @@ def add_value_to_list():
233273
def doubled_values():
234274
return [x*2 for x in user_provided_values()]
235275
```
276+
:::
277+
278+
279+
```{=html}
280+
<style>
281+
div.callout-tip.callout {
282+
border-color: #008471
283+
}
284+
285+
div.callout-warning.callout {
286+
border-color: #fdcb3b
287+
}
288+
289+
.callout.callout-style-simple {
290+
padding: .4em .7em;
291+
border-top: 5px solid;
292+
border-left-width: 1px;
293+
}
294+
295+
.callout.callout-style-simple > .callout-body {
296+
padding-left: 0 !important;
297+
}
298+
</style>
299+
```

0 commit comments

Comments
 (0)