PythonNative uses React-like function components with hooks for managing state, effects, navigation, memoisation, and context. Function components decorated with @pn.component are the only way to build UI in PythonNative.
Decorate a Python function with @pn.component:
import pythonnative as pn
@pn.component
def Greeting(name: str = "World"):
return pn.Text(f"Hello, {name}!", style={"font_size": 20})Use it like any other component:
@pn.component
def MyPage():
return pn.Column(
Greeting(name="Alice"),
Greeting(name="Bob"),
style={"spacing": 12},
)Hooks let function components manage state and side effects. They must be called at the top level of a @pn.component function (not inside loops or conditions).
Local component state. Returns (value, setter).
@pn.component
def Counter(initial: int = 0):
count, set_count = pn.use_state(initial)
return pn.Column(
pn.Text(f"Count: {count}"),
pn.Button("+", on_click=lambda: set_count(count + 1)),
)The setter accepts a value or a function that receives the current value:
set_count(10) # set directly
set_count(lambda prev: prev + 1) # functional updateIf the initial value is expensive to compute, pass a callable:
count, set_count = pn.use_state(lambda: compute_default())For complex state logic, use_reducer lets you manage state transitions through a reducer function — similar to React's useReducer:
def reducer(state, action):
if action == "increment":
return state + 1
if action == "decrement":
return state - 1
if action == "reset":
return 0
return state
@pn.component
def Counter():
count, dispatch = pn.use_reducer(reducer, 0)
return pn.Column(
pn.Text(f"Count: {count}"),
pn.Row(
pn.Button("-", on_click=lambda: dispatch("decrement")),
pn.Button("+", on_click=lambda: dispatch("increment")),
pn.Button("Reset", on_click=lambda: dispatch("reset")),
style={"spacing": 8},
),
)The reducer receives the current state and an action, and returns the new state. Actions can be any value (strings, dicts, etc.). The component only re-renders when the reducer returns a different state.
Run side effects after the native view tree is committed. The effect function may return a cleanup callable.
@pn.component
def Timer():
seconds, set_seconds = pn.use_state(0)
def tick():
import threading
t = threading.Timer(1.0, lambda: set_seconds(seconds + 1))
t.start()
return t.cancel # cleanup: cancel the timer
pn.use_effect(tick, [seconds])
return pn.Text(f"Elapsed: {seconds}s")Effects are deferred — they are queued during the render phase and executed after the reconciler finishes committing native view mutations. This means effect callbacks can safely measure layout or interact with the committed native tree.
Dependency control:
pn.use_effect(fn, None)— run on every renderpn.use_effect(fn, [])— run on mount onlypn.use_effect(fn, [a, b])— run whenaorbchange
Access navigation from any component. Returns a NavigationHandle with .navigate(), .go_back(), and .get_params().
@pn.component
def HomeScreen():
nav = pn.use_navigation()
return pn.Column(
pn.Text("Home", style={"font_size": 24}),
pn.Button(
"Go to Details",
on_click=lambda: nav.navigate("Detail", params={"id": 42}),
),
style={"spacing": 12, "padding": 16},
)
@pn.component
def DetailScreen():
nav = pn.use_navigation()
item_id = nav.get_params().get("id", 0)
return pn.Column(
pn.Text(f"Detail #{item_id}", style={"font_size": 20}),
pn.Button("Back", on_click=nav.go_back),
style={"spacing": 12, "padding": 16},
)See the Navigation guide for full details.
Convenience hook to read the current route's parameters:
@pn.component
def DetailScreen():
params = pn.use_route()
item_id = params.get("id", 0)
return pn.Text(f"Detail #{item_id}")Like use_effect but only runs when the screen is focused. Useful for refreshing data when navigating back to a screen:
@pn.component
def FeedScreen():
items, set_items = pn.use_state([])
pn.use_focus_effect(lambda: load_items(set_items), [])
return pn.FlatList(data=items, render_item=lambda item, i: pn.Text(item))Memoise an expensive computation:
sorted_items = pn.use_memo(lambda: sorted(items, key=lambda x: x.name), [items])Return a stable function reference (avoids unnecessary re-renders of children):
handle_click = pn.use_callback(lambda: set_count(count + 1), [count])A mutable container that persists across renders without triggering re-renders:
render_count = pn.use_ref(0)
render_count["current"] += 1Read a value from the nearest Provider ancestor:
theme = pn.use_context(pn.ThemeContext)
color = theme["primary_color"]Share values through the component tree without passing props manually:
user_context = pn.create_context({"name": "Guest"})
@pn.component
def App():
return pn.Provider(user_context, {"name": "Alice"},
UserProfile()
)
@pn.component
def UserProfile():
user = pn.use_context(user_context)
return pn.Text(f"Welcome, {user['name']}")By default, each state setter call triggers a re-render. When you need to update multiple pieces of state at once, use pn.batch_updates() to coalesce them into a single render pass:
@pn.component
def Form():
name, set_name = pn.use_state("")
email, set_email = pn.use_state("")
def on_submit():
with pn.batch_updates():
set_name("Alice")
set_email("alice@example.com")
# single re-render here
return pn.Column(
pn.Text(f"{name} <{email}>"),
pn.Button("Fill", on_click=on_submit),
)State updates triggered by effects during a render pass are automatically batched — the framework drains any pending re-renders after effect flushing completes, so you don't need batch_updates() inside effects.
Wrap risky components in pn.ErrorBoundary to catch render errors and display a fallback UI:
@pn.component
def App():
return pn.ErrorBoundary(
MyRiskyComponent(),
fallback=lambda err: pn.Text(f"Something went wrong: {err}"),
)Without an error boundary, an exception during rendering crashes the entire page. Error boundaries catch errors during both initial mount and subsequent reconciliation.
Extract reusable stateful logic into plain functions:
def use_toggle(initial: bool = False):
value, set_value = pn.use_state(initial)
toggle = pn.use_callback(lambda: set_value(not value), [value])
return value, toggle
def use_text_input(initial: str = ""):
text, set_text = pn.use_state(initial)
return text, set_textUse them in any component:
@pn.component
def Settings():
dark_mode, toggle_dark = use_toggle(False)
return pn.Column(
pn.Text("Settings", style={"font_size": 24, "bold": True}),
pn.Row(
pn.Text("Dark mode"),
pn.Switch(value=dark_mode, on_change=lambda v: toggle_dark()),
),
)- Only call hooks inside
@pn.componentfunctions - Call hooks at the top level — not inside loops, conditions, or nested functions
- Hooks must be called in the same order on every render