| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 094d997 commit bf6bb57
14 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -13,6 +13,10 @@ | |||
| 13 | 13 | ||
| 14 | 14 | Each returns an `Element` descriptor. Visual and layout properties are passed via `style={...}`. See the Component Property Reference for full details. | |
| 15 | 15 | ||
| 16 | + ### ErrorBoundary | ||
| 17 | + | ||
| 18 | + `pythonnative.ErrorBoundary(child, fallback=...)` — catches render errors in *child* and displays *fallback* instead. *fallback* may be an `Element` or a callable that receives the exception and returns an `Element`. | ||
| 19 | + | ||
| 16 | 20 | ### Element | |
| 17 | 21 | ||
| 18 | 22 | `pythonnative.Element` — the descriptor type returned by element functions. You generally don't create these directly. | |
@@ -23,7 +27,8 @@ Function component primitives: | |||
| 23 | 27 | ||
| 24 | 28 | - `pythonnative.component` — decorator to create a function component | |
| 25 | 29 | - `pythonnative.use_state(initial)` — local component state | |
| 26 | - - `pythonnative.use_effect(effect, deps)` — side effects | ||
| 30 | + - `pythonnative.use_reducer(reducer, initial_state)` — reducer-based state management; returns `(state, dispatch)` | ||
| 31 | + - `pythonnative.use_effect(effect, deps)` — side effects, run after native commit | ||
| 27 | 32 | - `pythonnative.use_navigation()` — navigation handle (push/pop/get_args) | |
| 28 | 33 | - `pythonnative.use_memo(factory, deps)` — memoised values | |
| 29 | 34 | - `pythonnative.use_callback(fn, deps)` — stable function references | |
@@ -32,6 +37,10 @@ Function component primitives: | |||
| 32 | 37 | - `pythonnative.create_context(default)` — create a new context | |
| 33 | 38 | - `pythonnative.Provider(context, value, child)` — provide a context value | |
| 34 | 39 | ||
| 40 | + ### Batching | ||
| 41 | + | ||
| 42 | + - `pythonnative.batch_updates()` — context manager that batches multiple state updates into a single re-render | ||
| 43 | + | ||
| 35 | 44 | ### Styling | |
| 36 | 45 | ||
| 37 | 46 | - `pythonnative.StyleSheet` — utility for creating and composing style dicts | |
@@ -53,7 +62,7 @@ Function component primitives: | |||
| 53 | 62 | ||
| 54 | 63 | ## Reconciler | |
| 55 | 64 | ||
| 56 | - `pythonnative.reconciler.Reconciler` — diffs element trees and applies minimal native mutations. Supports key-based child reconciliation, function components, and context providers. Used internally by `create_page`. | ||
| 65 | + `pythonnative.reconciler.Reconciler` — diffs element trees and applies minimal native mutations. Supports key-based child reconciliation, function components, context providers, and error boundaries. Effects are flushed after each mount/reconcile pass. Used internally by `create_page`. | ||
| 57 | 66 | ||
| 58 | 67 | ## Hot reload | |
| 59 | 68 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -5,24 +5,35 @@ PythonNative combines **direct native bindings** with a **declarative reconciler | |||
| 5 | 5 | ## High-level model | |
| 6 | 6 | ||
| 7 | 7 | 1. **Declarative element tree:** Your `@pn.component` function returns a tree of `Element` descriptors (similar to React elements / virtual DOM nodes). | |
| 8 | - 2. **Function components and hooks:** All UI is built with `@pn.component` functions using `use_state`, `use_effect`, `use_navigation`, etc. — inspired by React hooks but designed for Python. | ||
| 8 | + 2. **Function components and hooks:** All UI is built with `@pn.component` functions using `use_state`, `use_reducer`, `use_effect`, `use_navigation`, etc. — inspired by React hooks but designed for Python. | ||
| 9 | 9 | 3. **Reconciler:** On first render, the reconciler walks the tree and creates real native views via the platform backend. On subsequent renders (triggered by hook state changes), it diffs the new tree against the old one and applies the minimal set of native mutations. | |
| 10 | - 4. **Key-based reconciliation:** Children can be assigned stable `key` values to preserve identity across re-renders — critical for lists and dynamic content. | ||
| 11 | - 5. **Direct bindings:** Under the hood, native views are created and updated through direct platform calls: | ||
| 10 | + 4. **Post-render effects:** Effects queued via `use_effect` are flushed **after** the reconciler commits native mutations, matching React semantics. This guarantees that effect callbacks interact with the committed native tree. | ||
| 11 | + 5. **State batching:** Multiple state updates triggered during a render pass (e.g. from effects) are automatically batched into a single re-render. Explicit batching is available via `pn.batch_updates()`. | ||
| 12 | + 6. **Key-based reconciliation:** Children can be assigned stable `key` values to preserve identity across re-renders — critical for lists and dynamic content. | ||
| 13 | + 7. **Error boundaries:** `pn.ErrorBoundary` catches render errors in child subtrees and displays fallback UI, preventing a single component failure from crashing the entire page. | ||
| 14 | + 8. **Direct bindings:** Under the hood, native views are created and updated through direct platform calls: | ||
| 12 | 15 | - **iOS:** rubicon-objc exposes Objective-C/Swift classes (`UILabel`, `UIButton`, `UIStackView`, etc.). | |
| 13 | 16 | - **Android:** Chaquopy exposes Java classes (`android.widget.TextView`, `android.widget.Button`, etc.) via the JNI bridge. | |
| 14 | - 6. **Thin native bootstrap:** The host app remains native (Android `Activity` or iOS `UIViewController`). It calls `create_page()` internally to bootstrap your Python component, and the reconciler drives the UI from there. | ||
| 17 | + 9. **Thin native bootstrap:** The host app remains native (Android `Activity` or iOS `UIViewController`). It calls `create_page()` internally to bootstrap your Python component, and the reconciler drives the UI from there. | ||
| 15 | 18 | ||
| 16 | 19 | ## How it works | |
| 17 | 20 | ||
| 18 | 21 | ``` | |
| 19 | - @pn.component fn → Element tree → Reconciler → Native views | ||
| 22 | + @pn.component fn → Element tree → Reconciler → Native views → Flush effects | ||
| 20 | 23 | ↑ | |
| 21 | - Hook set_state() → re-render → diff → patch native views | ||
| 24 | + Hook set_state() → schedule render → diff → patch native views → Flush effects | ||
| 25 | + (batched) | ||
| 22 | 26 | ``` | |
| 23 | 27 | ||
| 24 | 28 | The reconciler uses **key-based diffing** (matching children by key first, then by position). When a child with the same key/type is found, its props are updated in-place on the native view. When the type changes, the old native view is destroyed and a new one is created. | |
| 25 | 29 | ||
| 30 | + ### Render lifecycle | ||
| 31 | + | ||
| 32 | + 1. **Render phase:** Component functions execute. Hooks record state reads, queue effects, and register memos. No native mutations happen yet. | ||
| 33 | + 2. **Commit phase:** The reconciler applies the diff to native views — creating, updating, and removing views as needed. | ||
| 34 | + 3. **Effect phase:** Pending effects are flushed in depth-first order (children before parents). Cleanup functions from the previous render run before new effect callbacks. | ||
| 35 | + 4. **Drain phase:** If effects set state, a new render pass is automatically triggered and the cycle repeats (up to a safety limit to prevent infinite loops). | ||
| 36 | + | ||
| 26 | 37 | ## Component model | |
| 27 | 38 | ||
| 28 | 39 | PythonNative uses a single component model: **function components** decorated with `@pn.component`. | |
@@ -40,7 +51,7 @@ def Counter(initial: int = 0): | |||
| 40 | 51 | ||
| 41 | 52 | Each component is a Python function that: | |
| 42 | 53 | - Accepts props as keyword arguments | |
| 43 | - - Uses hooks for state (`use_state`), side effects (`use_effect`), navigation (`use_navigation`), and more | ||
| 54 | + - Uses hooks for state (`use_state`, `use_reducer`), side effects (`use_effect`), navigation (`use_navigation`), and more | ||
| 44 | 55 | - Returns an `Element` tree describing the UI | |
| 45 | 56 | - Each call site creates an independent instance with its own hook state | |
| 46 | 57 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -52,6 +52,10 @@ pn.Column( | |||
| 52 | 52 | ||
| 53 | 53 | - `Modal(*children, visible, on_dismiss, title)` — modal dialog | |
| 54 | 54 | ||
| 55 | + **Error handling:** | ||
| 56 | + | ||
| 57 | + - `ErrorBoundary(child, fallback)` — catches render errors in child and displays fallback | ||
| 58 | + | ||
| 55 | 59 | **Lists:** | |
| 56 | 60 | ||
| 57 | 61 | - `FlatList(data, render_item, key_extractor, separator_height)` — scrollable data list | |
@@ -164,7 +168,8 @@ Changing one `Counter` doesn't affect the other — each has its own hook state. | |||
| 164 | 168 | ### Available hooks | |
| 165 | 169 | ||
| 166 | 170 | - `use_state(initial)` — local component state; returns `(value, setter)` | |
| 167 | - - `use_effect(effect, deps)` — side effects (timers, API calls, subscriptions) | ||
| 171 | + - `use_reducer(reducer, initial_state)` — reducer-based state; returns `(state, dispatch)` | ||
| 172 | + - `use_effect(effect, deps)` — side effects, run after native commit (timers, API calls, subscriptions) | ||
| 168 | 173 | - `use_memo(factory, deps)` — memoised computed values | |
| 169 | 174 | - `use_callback(fn, deps)` — stable function references | |
| 170 | 175 | - `use_ref(initial)` — mutable ref that persists across renders | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -58,9 +58,40 @@ If the initial value is expensive to compute, pass a callable: | |||
| 58 | 58 | count, set_count = pn.use_state(lambda: compute_default()) | |
| 59 | 59 | ``` | |
| 60 | 60 | ||
| 61 | + ### use_reducer | ||
| 62 | + | ||
| 63 | + For complex state logic, `use_reducer` lets you manage state transitions through a reducer function — similar to React's `useReducer`: | ||
| 64 | + | ||
| 65 | + ```python | ||
| 66 | + def reducer(state, action): | ||
| 67 | + if action == "increment": | ||
| 68 | + return state + 1 | ||
| 69 | + if action == "decrement": | ||
| 70 | + return state - 1 | ||
| 71 | + if action == "reset": | ||
| 72 | + return 0 | ||
| 73 | + return state | ||
| 74 | + | ||
| 75 | + @pn.component | ||
| 76 | + def Counter(): | ||
| 77 | + count, dispatch = pn.use_reducer(reducer, 0) | ||
| 78 | + | ||
| 79 | + return pn.Column( | ||
| 80 | + pn.Text(f"Count: {count}"), | ||
| 81 | + pn.Row( | ||
| 82 | + pn.Button("-", on_click=lambda: dispatch("decrement")), | ||
| 83 | + pn.Button("+", on_click=lambda: dispatch("increment")), | ||
| 84 | + pn.Button("Reset", on_click=lambda: dispatch("reset")), | ||
| 85 | + style={"spacing": 8}, | ||
| 86 | + ), | ||
| 87 | + ) | ||
| 88 | + ``` | ||
| 89 | + | ||
| 90 | + 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. | ||
| 91 | + | ||
| 61 | 92 | ### use_effect | |
| 62 | 93 | ||
| 63 | - Run side effects after render. The effect function may return a cleanup callable. | ||
| 94 | + Run side effects **after** the native view tree is committed. The effect function may return a cleanup callable. | ||
| 64 | 95 | ||
| 65 | 96 | ```python | |
| 66 | 97 | @pn.component | |
@@ -78,6 +109,8 @@ def Timer(): | |||
| 78 | 109 | return pn.Text(f"Elapsed: {seconds}s") | |
| 79 | 110 | ``` | |
| 80 | 111 | ||
| 112 | + 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. | ||
| 113 | + | ||
| 81 | 114 | Dependency control: | |
| 82 | 115 | ||
| 83 | 116 | - `pn.use_effect(fn, None)` — run on every render | |
@@ -169,6 +202,45 @@ def UserProfile(): | |||
| 169 | 202 | return pn.Text(f"Welcome, {user['name']}") | |
| 170 | 203 | ``` | |
| 171 | 204 | ||
| 205 | + ## Batching state updates | ||
| 206 | + | ||
| 207 | + 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: | ||
| 208 | + | ||
| 209 | + ```python | ||
| 210 | + @pn.component | ||
| 211 | + def Form(): | ||
| 212 | + name, set_name = pn.use_state("") | ||
| 213 | + email, set_email = pn.use_state("") | ||
| 214 | + | ||
| 215 | + def on_submit(): | ||
| 216 | + with pn.batch_updates(): | ||
| 217 | + set_name("Alice") | ||
| 218 | + set_email("alice@example.com") | ||
| 219 | + # single re-render here | ||
| 220 | + | ||
| 221 | + return pn.Column( | ||
| 222 | + pn.Text(f"{name} <{email}>"), | ||
| 223 | + pn.Button("Fill", on_click=on_submit), | ||
| 224 | + ) | ||
| 225 | + ``` | ||
| 226 | + | ||
| 227 | + 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. | ||
| 228 | + | ||
| 229 | + ## Error boundaries | ||
| 230 | + | ||
| 231 | + Wrap risky components in `pn.ErrorBoundary` to catch render errors and display a fallback UI: | ||
| 232 | + | ||
| 233 | + ```python | ||
| 234 | + @pn.component | ||
| 235 | + def App(): | ||
| 236 | + return pn.ErrorBoundary( | ||
| 237 | + MyRiskyComponent(), | ||
| 238 | + fallback=lambda err: pn.Text(f"Something went wrong: {err}"), | ||
| 239 | + ) | ||
| 240 | + ``` | ||
| 241 | + | ||
| 242 | + Without an error boundary, an exception during rendering crashes the entire page. Error boundaries catch errors during both initial mount and subsequent reconciliation. | ||
| 243 | + | ||
| 172 | 244 | ## Custom hooks | |
| 173 | 245 | ||
| 174 | 246 | Extract reusable stateful logic into plain functions: | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -20,6 +20,7 @@ def App(): | |||
| 20 | 20 | ActivityIndicator, | |
| 21 | 21 | Button, | |
| 22 | 22 | Column, | |
| 23 | + ErrorBoundary, | ||
| 23 | 24 | FlatList, | |
| 24 | 25 | Image, | |
| 25 | 26 | Modal, | |
@@ -39,13 +40,15 @@ def App(): | |||
| 39 | 40 | from .element import Element | |
| 40 | 41 | from .hooks import ( | |
| 41 | 42 | Provider, | |
| 43 | + batch_updates, | ||
| 42 | 44 | component, | |
| 43 | 45 | create_context, | |
| 44 | 46 | use_callback, | |
| 45 | 47 | use_context, | |
| 46 | 48 | use_effect, | |
| 47 | 49 | use_memo, | |
| 48 | 50 | use_navigation, | |
| 51 | + use_reducer, | ||
| 49 | 52 | use_ref, | |
| 50 | 53 | use_state, | |
| 51 | 54 | ) | |
@@ -57,6 +60,7 @@ def App(): | |||
| 57 | 60 | "ActivityIndicator", | |
| 58 | 61 | "Button", | |
| 59 | 62 | "Column", | |
| 63 | + "ErrorBoundary", | ||
| 60 | 64 | "FlatList", | |
| 61 | 65 | "Image", | |
| 62 | 66 | "Modal", | |
@@ -76,13 +80,15 @@ def App(): | |||
| 76 | 80 | "Element", | |
| 77 | 81 | "create_page", | |
| 78 | 82 | # Hooks | |
| 83 | + "batch_updates", | ||
| 79 | 84 | "component", | |
| 80 | 85 | "create_context", | |
| 81 | 86 | "use_callback", | |
| 82 | 87 | "use_context", | |
| 83 | 88 | "use_effect", | |
| 84 | 89 | "use_memo", | |
| 85 | 90 | "use_navigation", | |
| 91 | + "use_reducer", | ||
| 86 | 92 | "use_ref", | |
| 87 | 93 | "use_state", | |
| 88 | 94 | "Provider", | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -357,6 +357,29 @@ def Pressable( | |||
| 357 | 357 | return Element("Pressable", props, children, key=key) | |
| 358 | 358 | ||
| 359 | 359 | ||
| 360 | + def ErrorBoundary( | ||
| 361 | + child: Optional[Element] = None, | ||
| 362 | + *, | ||
| 363 | + fallback: Optional[Any] = None, | ||
| 364 | + key: Optional[str] = None, | ||
| 365 | + ) -> Element: | ||
| 366 | + """Catch render errors in *child* and display *fallback* instead. | ||
| 367 | + | ||
| 368 | + *fallback* may be an ``Element`` or a callable that receives the | ||
| 369 | + exception and returns an ``Element``:: | ||
| 370 | + | ||
| 371 | + pn.ErrorBoundary( | ||
| 372 | + MyRiskyComponent(), | ||
| 373 | + fallback=lambda err: pn.Text(f"Error: {err}"), | ||
| 374 | + ) | ||
| 375 | + """ | ||
| 376 | + props: Dict[str, Any] = {} | ||
| 377 | + if fallback is not None: | ||
| 378 | + props["__fallback__"] = fallback | ||
| 379 | + children = [child] if child is not None else [] | ||
| 380 | + return Element("__ErrorBoundary__", props, children, key=key) | ||
| 381 | + | ||
| 382 | + | ||
| 360 | 383 | def FlatList( | |
| 361 | 384 | *, | |
| 362 | 385 | data: Optional[List[Any]] = None, | |
| Back | FazBrowse Home | New Git URL |
0 commit comments