Updating Arrays in State
Overview¶
Lists are mutable in Python, but you should treat them as immutable when you store them in state. Just like with objects, when you want to update a list stored in state, you need to create a new one (or make a copy of an existing one), and then set state to use the new list.
You will learn
- How to add, remove, or change items in a list in ReactPy state
- How to update an object inside of a list
- Why a mutating update is dropped without any error
Updating lists without mutation¶
In JavaScript you copy arrays with the spread syntax, and mutating methods like push() and pop() are easy to reach for by mistake. Python's equivalent methods — append(), extend(), insert(), remove(), pop(), sort(), reverse() — are in-place and return None, so the mistake looks different and fails harder:
def handle_add(event):
items.append({"id": next_id, "text": text}) # mutates, returns None
set_items(items) # same object -> ignored
Nothing happens. ReactPy compares the new value against the old one with == (core/hooks.py, strictly_equal), sees nothing changed, and schedules no render. There is no error, no warning, and nothing in the browser console. Verified directly: mutating then setting the same list produces no render at all. Nor does setting a new list that happens to compare equal — which is why set_rows([*rows, 4]) works while mutating rows and passing it back does not.
So whenever you want to update a list, pass a new list to the setter. Python gives you two tools for that:
| Instead of (mutates) | Use (new list) |
|---|---|
items.append(x) | [*items, x] |
items.remove(x) | [i for i in items if i is not x] |
items[i] = x | [*items[:i], x, *items[i + 1:]] |
items.pop(i) | [*items[:i], *items[i + 1:]] |
items.insert(i, x) | [*items[:i], x, *items[i:]] |
items.reverse() | list(reversed(items)) |
items.sort(key=f) | sorted(items, key=f) |
Never store an iterator in state
This one has no JavaScript analogue and will bite you. Python's filter() and map() return lazy, single-use iterators, not lists:
set_items(filter(lambda i: i["id"] != id_to_delete, items)) # 🚩
That renders correctly once, which is exactly what makes it so hard to diagnose. The iterator is consumed while ReactPy walks the children, so the next render of that same state sees an exhausted iterator and displays nothing. Verified: a list of three items filtered to "a,c" on the first render, then an unrelated state change rendered an empty line.
Always build a list — a comprehension is clearest, list(filter(...)) if you must:
set_items([i for i in items if i["id"] != id_to_delete]) # correct
Adding to a list¶
The common case is adding an item. The handler creates a new list ending with the new item:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | |
1 | |
Type a task and submit; it appends while the existing ones stay put. Note that the new id is computed from the existing items rather than from a module-level counter. A global counter is shared by every visitor to the server, so two users would collide on the same key — derive ids from the state you already have, or use uuid4().
Removing from a list¶
The most common removal is filtering out one item:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | |
1 | |
[item for item in items if item["id"] != id_to_keep] produces a brand-new list containing everything except the deleted item, and the original is left alone.
Transforming every item¶
To apply a change to every item, build a new list by transforming each element:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
1 | |
Each item becomes a new dict, because mutating item["text"] in place would leave the previous state pointing at the same mutated object.
Comprehensions do not modify the original
React's docs point out that filter() doesn't touch the original array, and a Python comprehension has the same property — it reads the old list and returns a fresh one. The catch, stated above, is that the built-in filter() does not return a list at all.
Replacing an item¶
Unlike most languages, Python has no setAt(i, x) method, and the obvious items[i] = x mutates. Build the list from slices instead:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | |
1 | |
[{"id": 0, "text": text}, *items[1:]] replaces the first item and copies the rest. For an arbitrary index, [*items[:i], new_item, *items[i + 1:]] is the general form.
Inserting into a list¶
To insert an item somewhere other than the end, slice around the insertion point:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | |
1 | |
[*items[:1], new_item, *items[1:]] puts the new item at index 1. This is how React's "insert into an array" example translates; list.insert() mutates and is unusable here.
Making other changes to a list¶
The rule is uniform: copy first, then operate on the copy, or use a function that already returns a new list.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | |
1 | |
reversed() returns an iterator, so wrap it in list(...); sorted() already returns a new list; items[:2] is a slice, which is already a new list.
list.sort() and list.reverse() return None
These two are the most expensive traps because the failure is invisible. items.sort() sorts in place and returns None, so set_items(items.sort()) stores None in state. Use sorted(items) and list(reversed(items)).
Updating objects inside lists¶
The examples above replaced whole dicts, which is the safe default. When an item is a dict whose fields you want to change one at a time, you need two copies: a new list, and a new dict for the item that changed.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | |
1 | |
Tick one checkbox and only that artwork's "seen" flips — verified, the pair goes from [False, False] to [True, False]. The comprehension copies every item, and only the targeted one is rebuilt with the changed field.
A shallow copy of the list is not enough
It is tempting to write:
next_items = list(artworks) # new list...
next_items[0]["seen"] = True # ...but the SAME dicts inside
set_items(next_items)
That is a new list containing the same dict objects. Two problems follow. The assignment mutates a dict the previous state still references, so the change is invisible to ReactPy's comparison. And anything else holding that dict — another component seeded from the same fixture, a cached render — sees it flip too.
Copying is shallow at every level in Python; there is no deep copy on assignment or unpacking. Build the changed dict explicitly, as the example does. See Updating a nested object for the object-side version of this.
Keep identity stable for unchanged items
Notice the else artwork branch: unchanged items are passed through as the same objects, not rebuilt. That is both cheaper and useful — if you rebuild everything on each keystroke, nothing can be recognised as unchanged later.
Writing concise update logic¶
React's docs finish this page with Immer, which lets you write draft.push(...) behind a proxy. ReactPy has no Immer and no equivalent. There is no proxy layer to record your mutations, and no use_immer to import.
What you have instead is what this page already used:
- List unpacking and comprehensions —
[*items, x],[f(i) for i in items],[*items[:i], x, *items[i:]]. Verbose but explicit, and it fails loudly rather than silently. copy.deepcopy— for a structure too nested to copy by hand.use_reducer— moves the update logic out of the component, which is where the verbosity actually starts to hurt. See Extracting State Logic into a Reducer.
A homegrown Immer is worth avoiding specifically because it would paper over the failure mode this page is about: mutations are more dangerous here than in the browser, because ReactPy decides whether to render by comparing the old and new values on the server — so a mutation that hands back the same object, or an equal one, is dropped without a trace.
Recap¶
- Treat lists in state as read-only: never call
append(),sort(),insert(),remove(), and friends on them. - Build a new list with
[*items, x]or a comprehension, and pass that to the setter. - Mutating and passing the same list back is silently dropped — no render, no error.
- Never store
filter()/map()results in state; they are single-use iterators and the second render will be empty. - To change an object inside a list, copy the list and copy the changed dict.
- There is no Immer in ReactPy; use comprehensions,
deepcopy, oruse_reducer.