Skip to content

Lifecycle

PythonNative drives the UI through a small, predictable cycle: render, commit, effects, and an optional drain. This page walks through what happens at each step, how navigation changes fold into it, and where you can hook in.

A single render pass

A render pass is triggered by:

The phases:

  1. Render. Your @component function runs. Hooks register state, queue effects, and capture closures. No native widgets change yet, so this phase is cheap and pure (modulo use_state updates).
  2. Commit. The Reconciler diffs the new tree against the previous one and applies the smallest set of native mutations through the registered ViewHandlers.
  3. Effects. Cleanup callbacks from the previous render run first; new use_effect callbacks run after, in depth-first order so children commit before parents.
  4. Drain. If any effect set state, another render pass is queued immediately. The page host caps the loop to prevent runaway re-renders.
[render] -> [commit] -> [effects] -> drain? -> [render] ...

Effects vs focus effects

use_effect(fn, deps) fires after each commit when its deps list changes (or every commit if deps is omitted). This is right for subscriptions, timers, and synchronization with mutable globals.

use_focus_effect(fn, deps) is identical in shape but only fires when the screen is focused (and its cleanup runs when the screen is blurred). Use it for camera streams, GPS subscriptions, and anything that should be released as soon as the user navigates away.

Effects are not awaitable

Returning an awaitable from an effect doesn't await it. Schedule async work explicitly (e.g., via asyncio.create_task) and store the resulting cancellation handle in the effect's cleanup closure.

Mount, update, unmount

For a class-component-style mental model:

Class lifecycle PythonNative equivalent
componentDidMount use_effect(fn, deps=[])
componentDidUpdate use_effect(fn, deps=[a, b])
componentWillUnmount the cleanup function returned from use_effect
getDerivedStateFromProps a plain expression at the top of the component
getSnapshotBeforeUpdate not exposed; handle in commit-time platform APIs if needed

When a screen mounts inside a navigator (stack, tab, or drawer):

  1. The navigator builds the screen's element tree.
  2. The reconciler commits it (phase 2 above).
  3. Effects run; use_focus_effect callbacks fire because the screen is focused.

When the user navigates away:

  1. use_focus_effect cleanups run.
  2. If the screen is unmounted (e.g., popped from a stack), each use_effect cleanup runs as well.
  3. If the screen is kept alive (a previous tab, for example), only the focus cleanup runs; effect state is preserved.

App lifecycle (Android / iOS)

The page host forwards the platform's app-level lifecycle to navigators and effects:

  • Resume / viewWillAppear: the active screen's use_focus_effect is re-armed.
  • Pause / viewWillDisappear: focus cleanups run.
  • Destroy / dealloc: every effect cleanup runs and the reconciler tears down its native tree.

You can opt into these directly in app code by writing an effect that checks the navigation handle's is_focused() state, but most apps should reach for the use_focus_effect hook instead.

Putting it together: a subscription

import asyncio
import pythonnative as pn

@pn.component
def LiveClock():
    now, set_now = pn.use_state("--:--")

    def start_clock():
        async def tick():
            while True:
                set_now(_format_now())
                await asyncio.sleep(1)

        task = asyncio.create_task(tick())
        return task.cancel  # cleanup

    pn.use_focus_effect(start_clock, deps=[])
    return pn.Text(now, style={"font_size": 48})
  • The clock starts only while LiveClock is on the focused screen.
  • Navigating away cancels the task because the focus cleanup runs immediately.
  • Returning to the screen restarts a fresh task; the user never sees the previous, stale state because use_state resets on remount.

Next steps