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:

Updating Items

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}
Removing Items

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#

Avoid
d[key] = value

d.update({key: value})

d.setdefault(key, value)
Prefer
{**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#

Avoid
del d[key]

d.pop(key)
Prefer
{
    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:

Inserting Items

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:]
Removing Items

Avoid using item deletion or list.pop. Instead try the strategy below:

l[:index - 1] + l[index:]
Replacing Items

Avoid using item or slice assignment. Instead try the strategies below:

l[:index] + [value] + l[index + 1:]

l[:start] + values + l[end + 1:]
Re-ordering Items

Avoid using list.sort or list.reverse. Instead try the strategies below:

list(sorted(l))

list(reversed(l))

Inserting List Items#

Avoid
l.append(value)

l.extend(values)

l.insert(index, value)

# Adding a list "in-place" mutates!
l += [value]
Prefer
[*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#

Avoid
del l[index]

l.pop(index)
Prefer
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#

Avoid
l[index] = value

l[start:end] = values
Prefer
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#

Avoid
l.sort()

l.reverse()
Prefer
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:

Adding Items

Avoid using item assignment, set.add or set.update. Instead try the strategies below:

s.union({value})

s.union(values)
Removing Items

Avoid using item deletion or dict.pop. Instead try the strategies below:

s.difference({value})

s.difference(values)

s.intersection(values)

Adding Set Items#

Avoid
s.add(value)
s |= {value}  # "in-place" operators mutate!

s.update(values)
s |= values  # "in-place" operators mutate!
Prefer
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#

Avoid
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!
Prefer
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 🚧

https://pypi.org/project/pyrsistent/