Dangers of Mutability#
While state can hold any type of value, you should be careful to avoid directly modifying objects that you declare as state with ReactPy. In other words, you must not “mutate” values which are held as state. Rather, to change these values you should use new ones or create copies.
What is a Mutation?#
In Python, values may be either “mutable” or “immutable”. Mutable objects are those
whose underlying data can be changed after they are created, and immutable objects are
those which cannot. A “mutation” then, is the act of changing the underlying data of a
mutable value. In particular, a dict
is a mutable type of value. In the code
below, an initially empty dictionary is created. Then, a key and value is added to it:
x = {}
x["a"] = 1
assert x == {"a": 1}
This is different from something like a str
which is immutable. Instead of
modifying the underlying data of an existing value, a new one must be created to
facilitate change:
x = "Hello"
y = x + " world!"
assert x is not y
Note
In Python, the is
and is not
operators check whether two values are
identitcal. This is distinct from checking whether two
values are equivalent with the ==
or !=
operators.
Thus far, all the values we’ve been working with have been immutable. These include
int
, float
, str
, and bool
values. As a result, we
have not had to consider the consequences of mutations.
Why Avoid Mutation?#
Unfortunately, ReactPy does not understand that when a value is mutated, it may have changed. As a result, mutating values will not trigger re-renders. Thus, you must be careful to avoid mutation whenever you want ReactPy to re-render a component. For example, the intention of the code below is to make the red dot move when you touch or hover over the preview area. However it doesn’t - the dot remains stationary:
1# :linenos:
2
3from reactpy import component, html, run, use_state
4
5
6@component
7def MovingDot():
8 position, _ = use_state({"x": 0, "y": 0})
9
10 def handle_pointer_move(event):
11 outer_div_info = event["currentTarget"]
12 outer_div_bounds = outer_div_info["boundingClientRect"]
13 position["x"] = event["clientX"] - outer_div_bounds["x"]
14 position["y"] = event["clientY"] - outer_div_bounds["y"]
15
16 return html.div(
17 {
18 "on_pointer_move": handle_pointer_move,
19 "style": {
20 "position": "relative",
21 "height": "200px",
22 "width": "100%",
23 "background_color": "white",
24 },
25 },
26 html.div(
27 {
28 "style": {
29 "position": "absolute",
30 "background_color": "red",
31 "border_radius": "50%",
32 "width": "20px",
33 "height": "20px",
34 "left": "-10px",
35 "top": "-10px",
36 "transform": f"translate({position['x']}px, {position['y']}px)",
37 }
38 }
39 ),
40 )
41
42
43run(MovingDot)
The problem is with this section of code:
13 position["x"] = event["clientX"] - outer_div_bounds["x"]
14 position["y"] = event["clientY"] - outer_div_bounds["y"]
This code mutates the position
dictionary from the prior render instead of using the
state variable’s associated state setter. Without calling setter ReactPy has no idea that
the variable’s data has been modified. While it can be possible to get away with
mutating state variables, it’s highly dicsouraged. Doing so can cause strange and
unpredictable behavior. As a result, you should always treat the data within a state
variable as immutable.
To actually trigger a render we need to call the state setter. To do that we’ll assign
it to set_position
instead of the unused _
variable we have above. Then we can
call it by passing a new dictionary with the values for the next render. Notice how,
by making these alterations to the code, that the dot now follows your pointer when
you touch or hover over the preview:
from reactpy import component, html, run, use_state
@component
def MovingDot():
position, set_position = use_state({"x": 0, "y": 0})
async def handle_pointer_move(event):
outer_div_info = event["currentTarget"]
outer_div_bounds = outer_div_info["boundingClientRect"]
set_position(
{
"x": event["clientX"] - outer_div_bounds["x"],
"y": event["clientY"] - outer_div_bounds["y"],
}
)
return html.div(
{
"on_pointer_move": handle_pointer_move,
"style": {
"position": "relative",
"height": "200px",
"width": "100%",
"background_color": "white",
},
},
html.div(
{
"style": {
"position": "absolute",
"background_color": "red",
"border_radius": "50%",
"width": "20px",
"height": "20px",
"left": "-10px",
"top": "-10px",
"transform": f"translate({position['x']}px, {position['y']}px)",
}
}
),
)
run(MovingDot)
Local mutation can be alright
While code like this causes problems:
position["x"] = event["clientX"] - outer_div_bounds["x"]
position["y"] = event["clientY"] - outer_div_bounds["y"]
It’s ok if you mutate a fresh dictionary that you have just created before calling the state setter:
new_position = {}
new_position["x"] = event["clientX"] - outer_div_bounds["x"]
new_position["y"] = event["clientY"] - outer_div_bounds["y"]
set_position(new_position)
It’s actually nearly equivalent to having written:
set_position(
{
"x": event["clientX"] - outer_div_bounds["x"],
"y": event["clientY"] - outer_div_bounds["y"],
}
)
Mutation is only a problem when you change data assigned to existing state variables. Mutating an object you’ve just created is okay because no other code references it yet. Changing it isn’t going to accidentally impact something that depends on it. This is called a “local mutation.” You can even do local mutation while rendering. Very convenient and completely okay!
Working with Dictionaries#
Below are some ways to update dictionaries without mutating them:
Avoid using item assignment, dict.update
, or dict.setdefault
. Instead try
the strategies below:
{**d, "key": value}
# Python >= 3.9
d | {"key": value}
# Equivalent to dict.setdefault()
{"key": value, **d}
Avoid using item deletion or dict.pop
. Instead try the strategies below:
{
k: v
for k, v in d.items()
if k != key
}
# Better for removing multiple items
{
k: d[k]
for k in set(d).difference([key])
}
Updating Dictionary Items#
d[key] = value
d.update({key: value})
d.setdefault(key, value)
{**d, key: value}
# Python >= 3.9
d | {key: value}
# Equivalent to setdefault()
{key: value, **d}
As we saw in an earlier example, instead of mutating dictionaries to update their items you should instead create a copy that contains the desired changes.
However, sometimes you may only want to update some of the information in a dictionary
which is held by a state variable. Consider the case below where we have a form for
updating user information with a preview of the currently entered data. We can
accomplish this using “unpacking” with
the **
syntax:
from reactpy import component, html, run, use_state
@component
def Form():
person, set_person = use_state(
{
"first_name": "Barbara",
"last_name": "Hepworth",
"email": "bhepworth@sculpture.com",
}
)
def handle_first_name_change(event):
set_person({**person, "first_name": event["target"]["value"]})
def handle_last_name_change(event):
set_person({**person, "last_name": event["target"]["value"]})
def handle_email_change(event):
set_person({**person, "email": event["target"]["value"]})
return html.div(
html.label(
"First name: ",
html.input(
{"value": person["first_name"], "on_change": handle_first_name_change}
),
),
html.label(
"Last name: ",
html.input(
{"value": person["last_name"], "on_change": handle_last_name_change}
),
),
html.label(
"Email: ",
html.input({"value": person["email"], "on_change": handle_email_change}),
),
html.p(f"{person['first_name']} {person['last_name']} {person['email']}"),
)
run(Form)
Removing Dictionary Items#
del d[key]
d.pop(key)
{
k: v
for k, v in d.items()
if k != key
}
# Better for removing multiple items
{
k: d[k]
for k in set(d).difference([key])
}
This scenario doesn’t come up very frequently. When it does though, the best way to remove items from dictionaries is to create a copy of the original, but with a filtered set of keys. One way to do this is with a dictionary comprehension. The example below shows an interface where you’re able to enter a new term and definition. Once added, you can click a delete button to remove the term and definition:
from reactpy import component, html, run, use_state
@component
def Definitions():
term_to_add, set_term_to_add = use_state(None)
definition_to_add, set_definition_to_add = use_state(None)
all_terms, set_all_terms = use_state({})
def handle_term_to_add_change(event):
set_term_to_add(event["target"]["value"])
def handle_definition_to_add_change(event):
set_definition_to_add(event["target"]["value"])
def handle_add_click(event):
if term_to_add and definition_to_add:
set_all_terms({**all_terms, term_to_add: definition_to_add})
set_term_to_add(None)
set_definition_to_add(None)
def make_delete_click_handler(term_to_delete):
def handle_click(event):
set_all_terms({t: d for t, d in all_terms.items() if t != term_to_delete})
return handle_click
return html.div(
html.button({"on_click": handle_add_click}, "add term"),
html.label(
"Term: ",
html.input({"value": term_to_add, "on_change": handle_term_to_add_change}),
),
html.label(
"Definition: ",
html.input(
{
"value": definition_to_add,
"on_change": handle_definition_to_add_change,
}
),
),
html.hr(),
[
html.div(
{"key": term},
html.button(
{"on_click": make_delete_click_handler(term)}, "delete term"
),
html.dt(term),
html.dd(definition),
)
for term, definition in all_terms.items()
],
)
run(Definitions)
Working with Lists#
Below are some ways to update lists without mutating them:
Avoid using list.append
, list.extend
, and list.insert
. Instead try the
strategies below:
[*l, value]
l + [value]
l + values
l[:index] + [value] + l[index:]
Avoid using item deletion or list.pop
. Instead try the strategy below:
l[:index - 1] + l[index:]
Avoid using item or slice assignment. Instead try the strategies below:
l[:index] + [value] + l[index + 1:]
l[:start] + values + l[end + 1:]
Avoid using list.sort
or list.reverse
. Instead try the strategies below:
list(sorted(l))
list(reversed(l))
Inserting List Items#
l.append(value)
l.extend(values)
l.insert(index, value)
# Adding a list "in-place" mutates!
l += [value]
[*l, value]
l + [value]
l + values
l[:index] + [value] + l[index:]
Instead of mutating a list to add items to it, we need to create a new list which has
the items we want to append instead. There are several ways to do this for one or more
values however it’s often simplest to use “unpacking” with the *
syntax.
from reactpy import component, html, run, use_state
@component
def ArtistList():
artist_to_add, set_artist_to_add = use_state("")
artists, set_artists = use_state([])
def handle_change(event):
set_artist_to_add(event["target"]["value"])
def handle_click(event):
if artist_to_add and artist_to_add not in artists:
set_artists([*artists, artist_to_add])
set_artist_to_add("")
return html.div(
html.h1("Inspiring sculptors:"),
html.input({"value": artist_to_add, "on_change": handle_change}),
html.button({"on_click": handle_click}, "add"),
html.ul([html.li({"key": name}, name) for name in artists]),
)
run(ArtistList)
Removing List Items#
del l[index]
l.pop(index)
l[:index] + l[index + 1:]
Unfortunately, the syntax for creating a copy of a list with one of its items removed is
not quite as clean. You must select the portion the list prior to the item which should
be removed (l[:index]
) and the portion after the item (l[index + 1:]
) and add
them together:
from reactpy import component, html, run, use_state
@component
def ArtistList():
artist_to_add, set_artist_to_add = use_state("")
artists, set_artists = use_state(
["Marta Colvin Andrade", "Lamidi Olonade Fakeye", "Louise Nevelson"]
)
def handle_change(event):
set_artist_to_add(event["target"]["value"])
def handle_add_click(event):
if artist_to_add not in artists:
set_artists([*artists, artist_to_add])
set_artist_to_add("")
def make_handle_delete_click(index):
def handle_click(event):
set_artists(artists[:index] + artists[index + 1 :])
return handle_click
return html.div(
html.h1("Inspiring sculptors:"),
html.input({"value": artist_to_add, "on_change": handle_change}),
html.button({"on_click": handle_add_click}, "add"),
html.ul(
[
html.li(
{"key": name},
name,
html.button(
{"on_click": make_handle_delete_click(index)}, "delete"
),
)
for index, name in enumerate(artists)
]
),
)
run(ArtistList)
Replacing List Items#
l[index] = value
l[start:end] = values
l[:index] + [value] + l[index + 1:]
l[:start] + values + l[end + 1:]
In a similar manner to Removing List Items, to replace an item in a list, you must select the portion before and after the item in question. But this time, instead of adding those two selections together, you must insert that values you want to replace between them:
from reactpy import component, html, run, use_state
@component
def CounterList():
counters, set_counters = use_state([0, 0, 0])
def make_increment_click_handler(index):
def handle_click(event):
new_value = counters[index] + 1
set_counters(counters[:index] + [new_value] + counters[index + 1 :])
return handle_click
return html.ul(
[
html.li(
{"key": index},
count,
html.button({"on_click": make_increment_click_handler(index)}, "+1"),
)
for index, count in enumerate(counters)
]
)
run(CounterList)
Re-ordering List Items#
l.sort()
l.reverse()
list(sorted(l))
list(reversed(l))
There are many different ways that list items could be re-ordered, but two of the most
common are reversing or sorting items. Instead of calling the associated methods on a
list object, you should use the builtin functions sorted()
and reversed()
and pass the resulting iterator into the list
constructor to create a sorted
or reversed copy of the given list:
from reactpy import component, html, run, use_state
@component
def ArtistList():
artists, set_artists = use_state(
["Marta Colvin Andrade", "Lamidi Olonade Fakeye", "Louise Nevelson"]
)
def handle_sort_click(event):
set_artists(sorted(artists))
def handle_reverse_click(event):
set_artists(list(reversed(artists)))
return html.div(
html.h1("Inspiring sculptors:"),
html.button({"on_click": handle_sort_click}, "sort"),
html.button({"on_click": handle_reverse_click}, "reverse"),
html.ul([html.li({"key": name}, name) for name in artists]),
)
run(ArtistList)
Working with Sets#
Below are ways to update sets without mutating them:
Avoid using item assignment, set.add
or set.update
. Instead try the
strategies below:
s.union({value})
s.union(values)
Avoid using item deletion or dict.pop
. Instead try the strategies below:
s.difference({value})
s.difference(values)
s.intersection(values)
Adding Set Items#
s.add(value)
s |= {value} # "in-place" operators mutate!
s.update(values)
s |= values # "in-place" operators mutate!
s.union({value})
s | {value}
s.union(values)
s | values
Sets have some nice ways for evolving them without requiring mutation. The binary
or operator |
serves as a succinct way to compute the union of two sets. However,
you should be careful to not use an in-place assignment with this operator as that will
(counterintuitively) mutate the original set rather than creating a new one.
from reactpy import component, html, run, use_state
@component
def Grid():
line_size = 5
selected_indices, set_selected_indices = use_state(set())
def make_handle_click(index):
def handle_click(event):
set_selected_indices(selected_indices | {index})
return handle_click
return html.div(
{"style": {"display": "flex", "flex-direction": "row"}},
[
html.div(
{
"on_click": make_handle_click(index),
"style": {
"height": "30px",
"width": "30px",
"background_color": (
"black" if index in selected_indices else "white"
),
"outline": "1px solid grey",
"cursor": "pointer",
},
"key": index,
}
)
for index in range(line_size)
],
)
run(Grid)
Removing Set Items#
s.remove(value)
s.difference_update(values)
s -= values # "in-place" operators mutate!
s.symmetric_difference_update(values)
s ^= values # "in-place" operators mutate!
s.intersection_update(values)
s &= values # "in-place" operators mutate!
s.difference({value})
s.difference(values)
s - values
s.symmetric_difference(values)
s ^ values
s.intersection(values)
s & values
To remove items from sets you can use the various binary operators or their associated methods to return new sets without mutating them. As before when Adding Set Items you need to avoid using the inline assignment operators since that will (counterintuitively) mutate the original set rather than given you a new one:
from reactpy import component, html, run, use_state
@component
def Grid():
line_size = 5
selected_indices, set_selected_indices = use_state({1, 2, 4})
def make_handle_click(index):
def handle_click(event):
if index in selected_indices:
set_selected_indices(selected_indices - {index})
else:
set_selected_indices(selected_indices | {index})
return handle_click
return html.div(
{"style": {"display": "flex", "flex-direction": "row"}},
[
html.div(
{
"on_click": make_handle_click(index),
"style": {
"height": "30px",
"width": "30px",
"background_color": (
"black" if index in selected_indices else "white"
),
"outline": "1px solid grey",
"cursor": "pointer",
},
"key": index,
}
)
for index in range(line_size)
],
)
run(Grid)
Useful Packages#
Under construction 🚧