Lists¶
Render a dynamic list with FlatList, use
keys for stable reconciliation, and hook in delete and toggle actions.
This example builds a simple to-do list.
Paste it into app/main.py of a project scaffolded with pn init,
then run pn preview app.main.TodoList.
The code¶
from collections.abc import Callable
from dataclasses import dataclass, replace
import pythonnative as pn
@dataclass(frozen=True)
class Todo:
id: str
title: str
done: bool = False
INITIAL_TODOS: tuple[Todo, ...] = (
Todo("1", "Try PythonNative", done=True),
Todo("2", "Build a real app"),
Todo("3", "Profit"),
)
@pn.component
def TodoRow(todo: Todo, on_toggle: Callable[[str], None], on_delete: Callable[[str], None]) -> pn.Node:
return pn.Row(
pn.Switch(value=todo.done, on_change=lambda value: on_toggle(todo.id)),
pn.Text(
todo.title,
style={
"flex": 1,
"font_size": 16,
"color": "#888888" if todo.done else "#000000",
},
),
pn.Button("Delete", on_press=lambda: on_delete(todo.id)),
style={"gap": 12, "padding": 8, "align_items": "center"},
)
@pn.component
def TodoList() -> pn.Node:
todos, set_todos = pn.use_state(INITIAL_TODOS)
draft, set_draft = pn.use_state("")
def add() -> None:
title = draft.strip()
if not title:
return
new_id = str(max(int(t.id) for t in todos) + 1) if todos else "1"
set_todos((*todos, Todo(new_id, title)))
set_draft("")
def toggle(todo_id: str) -> None:
set_todos(tuple(replace(t, done=not t.done) if t.id == todo_id else t for t in todos))
def delete(todo_id: str) -> None:
set_todos(tuple(t for t in todos if t.id != todo_id))
return pn.Column(
pn.Row(
pn.TextInput(
value=draft,
on_change=set_draft,
placeholder="What needs doing?",
style={"flex": 1},
),
pn.Button("Add", on_press=add),
style={"gap": 8, "padding": 16},
),
pn.FlatList(
data=todos,
render_item=lambda t, index: TodoRow(todo=t, on_toggle=toggle, on_delete=delete),
key_extractor=lambda t, index: t.id,
style={"flex": 1},
),
)
Why keys matter¶
The reconciler matches children by key first and by position only as
a fallback. Position-based keys can transfer a row's local state to another
item after a deletion. With stable keys, "todo 2" keeps its identity even
when its index shifts, as long as it remains in the mounted window.
FlatList keys each row with key_extractor, which reads the same
todo.id the row's callbacks use. When you render rows yourself (in a
Column, say), key your own components with .with_key():
pn.Column(*(TodoRow(t, toggle, delete).with_key(t.id) for t in todos)).
Built-in elements take key= directly. When in doubt: use a stable
identifier that's part of the data, not the position.
Performance notes¶
FlatListlazily mounts only the rows that are visible (or near visible); the native list owns scrolling, item geometry, and recycling. For long lists it scales much better than wrapping aColumnin aScrollView.- Avoid recomputing
dataon every render. If you derive it from another piece of state, wrap it inuse_memo. - For row callbacks that you pass deeply, consider
use_callbackto keep references stable. - Ordinary sequences are convenient for small, derived collections. For large
collections with frequent edits, use
ListDatato publish keyed changes without scanning every item. See the incremental list guide.
Sorting and filtering¶
Sorting and filtering are pure functions over the todos tuple; do
them at render time:
If the input list is large, memoize the result:
visible = pn.use_memo(
lambda: sorted((t for t in todos if not hide_done or not t.done), key=lambda t: t.title),
[todos, hide_done],
)
Next steps¶
- Capture user input: Forms.
- Move the to-do list onto its own screen: Navigation.
- Persist between launches with the file system: Native modules.