-
Notifications
You must be signed in to change notification settings - Fork 625
Description
Duplicate Check
- I have searched the opened issues and there are no duplicates
Describe the bug
When using ft.use_state() with an updater function that mutates the current state in-place and returns the same object reference, the component does not re-render.
In set_state, new_value is computed by calling:
new_value_or_fn(hook.value)If updater mutates hook.value directly (e.g., list.append()), then hook.value is already changed before comparison.
So this check may fail to detect change:
if new_value != hook.value:because both refer to the same mutated object.
Code sample
Code
import asyncio
import flet as ft
@ft.component
def App():
items, set_items = ft.use_state(list(range(3)))
async def add_new(e):
def add_item(cur: list):
# in-place mutation
cur.append(len(cur))
return cur
set_items(add_item)
await asyncio.sleep(0.1)
return ft.Column(
controls=[
ft.Text(f"Count: {len(items)}"),
ft.Column([ft.Text(str(i)) for i in items]),
ft.OutlinedButton("Add new", on_click=add_new),
]
)
ft.run(lambda page: page.render(App))
To reproduce
-
Run the repro code.
-
Click "Add new".
-
Observe that state content is mutated in-place, but UI may not update/re-render as expected.
Expected behavior
State update should trigger component re-render when updater function changes content, even if it returns the same object reference after in-place mutation.
Screenshots / Videos
Captures
[Upload media here]
Operating System
Windows
Operating system details
Windows 11 ###
Flet version
0.81.0
Regression
No, it isn't
Suggestions
Possible fix in set_state:
-
Keep a snapshot of old value before calling updater (for callable updaters), then compare against snapshot when same object reference is returned.
-
Example logic:
old_value = hook.valueold_snapshot = deepcopy(old_value)(for updater path)new_value = updater(hook.value)- if
new_value is old_value, comparenew_value != old_snapshot; else comparenew_value != old_value.
This correctly handles both:
- Immutable replacement (
return new_list) - In-place mutation (
cur.append(...); return cur)
def set_state(new_value_or_fn: StateT | Updater):
"""
Update the state value.
Can be called with either:
- a direct new value, or
- a function that takes the current value and returns the next one.
"""
# Compute next value
old_value = hook.value
is_updater = callable(new_value_or_fn)
# If updater mutates value in-place and returns the same object,
# `old_value` reference will also reflect the mutation.
# Keep a snapshot to correctly detect that change.
old_value_snapshot = deepcopy(old_value) if is_updater else old_value
new_value = (
new_value_or_fn(hook.value)
if is_updater
else new_value_or_fn
)
# Trigger update only when value changed.
# Handles both replacement and in-place mutation patterns.
if is_updater and new_value is old_value:
value_changed = new_value != old_value_snapshot
else:
value_changed = new_value != old_value
if value_changed:
hook.value = new_value
update_subscription(hook)
hook.version += 1
if hook.component:
hook.component._schedule_update()
Logs
Logs
[Paste your logs here]Additional details
No response