Skip to content

Commit 54878c0

Browse files
committed
feat(components,hooks)!: add props, fragment, memo, and native picker
1 parent a43e15f commit 54878c0

23 files changed

Lines changed: 1628 additions & 670 deletions

docs/api/pythonnative.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ The reference is split per module so each page stays scannable:
3636

3737
| Area | Page | Key symbols |
3838
|---|---|---|
39-
| Element factories | [Components](components.md) | [`Text`][pythonnative.Text], [`Button`][pythonnative.Button], [`Column`][pythonnative.Column], [`Row`][pythonnative.Row], [`ScrollView`][pythonnative.ScrollView], [`FlatList`][pythonnative.FlatList], [`SectionList`][pythonnative.SectionList], [`Modal`][pythonnative.Modal], [`Pressable`][pythonnative.Pressable], [`StatusBar`][pythonnative.StatusBar], [`KeyboardAvoidingView`][pythonnative.KeyboardAvoidingView], [`RefreshControl`][pythonnative.RefreshControl], [`Picker`][pythonnative.Picker], [`ErrorBoundary`][pythonnative.ErrorBoundary] |
40-
| Hooks | [Hooks](hooks.md) | [`use_state`][pythonnative.use_state], [`use_reducer`][pythonnative.use_reducer], [`use_effect`][pythonnative.use_effect], [`use_memo`][pythonnative.use_memo], [`use_ref`][pythonnative.use_ref], [`use_context`][pythonnative.use_context], [`use_window_dimensions`][pythonnative.use_window_dimensions], [`use_safe_area_insets`][pythonnative.use_safe_area_insets], [`use_keyboard_height`][pythonnative.use_keyboard_height] |
41-
| Animations | [Animated](animated.md) | `Animated`, [`AnimatedValue`][pythonnative.AnimatedValue] |
39+
| Element factories | [Components](components.md) | [`Text`][pythonnative.Text], [`Button`][pythonnative.Button], [`Column`][pythonnative.Column], [`Row`][pythonnative.Row], [`ScrollView`][pythonnative.ScrollView], [`FlatList`][pythonnative.FlatList], [`SectionList`][pythonnative.SectionList], [`Modal`][pythonnative.Modal], [`Pressable`][pythonnative.Pressable], [`StatusBar`][pythonnative.StatusBar], [`KeyboardAvoidingView`][pythonnative.KeyboardAvoidingView], [`RefreshControl`][pythonnative.RefreshControl], [`Picker`][pythonnative.Picker], [`Fragment`][pythonnative.Fragment], [`ErrorBoundary`][pythonnative.ErrorBoundary] |
40+
| Hooks | [Hooks](hooks.md) | [`use_state`][pythonnative.use_state], [`use_reducer`][pythonnative.use_reducer], [`use_effect`][pythonnative.use_effect], [`use_memo`][pythonnative.use_memo], [`use_ref`][pythonnative.use_ref], [`use_context`][pythonnative.use_context], [`use_window_dimensions`][pythonnative.use_window_dimensions], [`use_safe_area_insets`][pythonnative.use_safe_area_insets], [`use_keyboard_height`][pythonnative.use_keyboard_height], [`memo`][pythonnative.memo] |
41+
| Animations | [Animated](animated.md) | `Animated`, [`AnimatedValue`][pythonnative.AnimatedValue], [`use_animated_value`][pythonnative.use_animated_value] |
4242
| System dialogs | [Alerts](alerts.md) | [`Alert`][pythonnative.Alert] |
4343
| Platform | [Platform](platform.md) | [`Platform`][pythonnative.Platform] |
4444
| Navigation | [Navigation](navigation.md) | [`NavigationContainer`][pythonnative.NavigationContainer], [`create_stack_navigator`][pythonnative.create_stack_navigator], [`create_tab_navigator`][pythonnative.create_tab_navigator], [`create_drawer_navigator`][pythonnative.create_drawer_navigator], [`use_navigation`][pythonnative.use_navigation] |

docs/api/sdk.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ convenience and are documented on their canonical pages:
2323
| [`Element`][pythonnative.element.Element] | [Element](element.md) |
2424
| [`ViewHandler`][pythonnative.native_views.base.ViewHandler] | [Native views](native_views.md) |
2525
| [`Style`][pythonnative.style.Style], [`StyleProp`][pythonnative.style.StyleProp], [`Color`][pythonnative.style.Color], [`Dimension`][pythonnative.style.Dimension], [`EdgeInsets`][pythonnative.style.EdgeInsets], [`EdgeValue`][pythonnative.style.EdgeValue], `FlexDirection`, `JustifyContent`, `Overflow`, `Position`, [`TransformSpec`][pythonnative.style.TransformSpec], [`style`][pythonnative.style.style] | [Style](style.md) |
26-
| `parse_color_int`, `resolve_padding` | `pythonnative.native_views.base` |
26+
| `parse_color_int` | `pythonnative.native_views.base` |
2727

2828
## Custom-component primitives
2929

docs/concepts/components.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@ pn.Column(
7575
- [`ErrorBoundary(child, fallback)`][pythonnative.ErrorBoundary]:
7676
catches render errors in child and displays fallback.
7777

78+
**Composition:**
79+
80+
- [`Fragment(*children)`][pythonnative.Fragment]: group siblings into a
81+
parent's child list without an extra wrapping view (analogous to
82+
React's `<>…</>`).
83+
7884
**Lists:**
7985

8086
- [`FlatList(data, render_item, key_extractor, item_height, ...)`][pythonnative.FlatList]:
@@ -86,7 +92,7 @@ pn.Column(
8692

8793
**Platform UI:**
8894

89-
- [`StatusBar(style, background_color, hidden)`][pythonnative.StatusBar]:
95+
- [`StatusBar(bar_style, background_color, hidden)`][pythonnative.StatusBar]:
9096
configure the device's status bar (light/dark icons, color, hidden).
9197
- [`KeyboardAvoidingView(*children, behavior)`][pythonnative.KeyboardAvoidingView]:
9298
shift content up when the software keyboard appears.
@@ -249,6 +255,9 @@ hook state.
249255
persists across renders. When passed via the `ref=` prop, the
250256
reconciler populates `ref["current"]` with the underlying native
251257
view.
258+
- [`use_animated_value(initial)`][pythonnative.use_animated_value]:
259+
stable [`AnimatedValue`][pythonnative.AnimatedValue] across renders;
260+
the canonical way to drive `Animated.View`.
252261
- [`use_context(context)`][pythonnative.use_context]: read from a
253262
context provider.
254263
- [`use_navigation()`][pythonnative.use_navigation]: navigation
@@ -263,6 +272,9 @@ hook state.
263272
reactive safe-area insets.
264273
- [`use_keyboard_height()`][pythonnative.use_keyboard_height]:
265274
reactive software-keyboard height.
275+
- [`@memo`][pythonnative.memo]: decorator that skips a function
276+
component's re-render when its props are shallowly equal and its
277+
internal state is unchanged.
266278

267279
### Custom hooks
268280

docs/concepts/hooks.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,17 @@ render_count = pn.use_ref(0)
209209
render_count["current"] += 1
210210
```
211211

212+
### use_animated_value
213+
214+
Create an [`AnimatedValue`][pythonnative.AnimatedValue] that's stable
215+
across renders. Equivalent to wrapping `pn.Animated.Value(initial)` in
216+
`use_memo(..., [])` but more discoverable:
217+
218+
```python
219+
opacity = pn.use_animated_value(0.0)
220+
pn.Animated.timing(opacity, to=1.0, duration=300).start()
221+
```
222+
212223
### use_context
213224

214225
Read a value from the nearest `Provider` ancestor:
@@ -267,6 +278,32 @@ automatically batched; the framework drains any pending re-renders
267278
after effect flushing completes, so you don't need `batch_updates()`
268279
inside effects.
269280

281+
## Memoizing function components
282+
283+
Wrap a function component with [`@pn.memo`][pythonnative.memo] to skip
284+
its body when neither its props nor its internal state have changed:
285+
286+
```python
287+
@pn.memo
288+
@pn.component
289+
def ExpensiveRow(label: str, value: int):
290+
return pn.Row(
291+
pn.Text(label, style={"flex": 1}),
292+
pn.Text(str(value)),
293+
)
294+
```
295+
296+
When a `memo`'d component is reconciled, the reconciler compares the
297+
new props against the previous props using shallow equality. If they
298+
match and none of the component's `use_state` / `use_reducer` setters
299+
have fired since the last render, the previously-rendered subtree is
300+
reused and the component body is not re-executed. This is the
301+
component-level equivalent of [`use_memo`][pythonnative.use_memo].
302+
303+
`memo` is typically used on pure, prop-driven leaves that re-render
304+
frequently as part of a larger tree, e.g. rows inside a list whose
305+
identity doesn't change between renders of the parent.
306+
270307
## Error boundaries
271308

272309
Wrap risky components in

docs/guides/animations.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ frame.
88

99
## Mental model
1010

11-
1. Create an [`AnimatedValue`][pythonnative.AnimatedValue] using
12-
[`use_memo`][pythonnative.use_memo] (so it survives re-renders).
11+
1. Create an [`AnimatedValue`][pythonnative.AnimatedValue] with
12+
[`use_animated_value`][pythonnative.use_animated_value] (so it
13+
survives re-renders).
1314
2. Bind the value into the `style` of an `Animated.View`,
1415
`Animated.Text`, or `Animated.Image`.
1516
3. Drive the value with `Animated.timing`, `Animated.spring`, or
@@ -31,7 +32,7 @@ import pythonnative as pn
3132

3233
@pn.component
3334
def FadeInBox():
34-
opacity = pn.use_memo(lambda: pn.Animated.Value(0.0), [])
35+
opacity = pn.use_animated_value(0.0)
3536

3637
def _fade_in():
3738
pn.Animated.timing(opacity, to=1.0, duration=400).start()
@@ -57,7 +58,7 @@ def FadeInBox():
5758
```python
5859
@pn.component
5960
def Bouncy():
60-
scale = pn.use_memo(lambda: pn.Animated.Value(1.0), [])
61+
scale = pn.use_animated_value(1.0)
6162

6263
def _press():
6364
pn.Animated.spring(scale, to=1.2, stiffness=200, damping=8).start()
@@ -79,8 +80,8 @@ animation property.
7980
## Sequencing and parallel composition
8081

8182
```python
82-
opacity = pn.Animated.Value(0.0)
83-
translate_y = pn.Animated.Value(20.0)
83+
opacity = pn.use_animated_value(0.0)
84+
translate_y = pn.use_animated_value(20.0)
8485

8586
pn.Animated.parallel([
8687
pn.Animated.timing(opacity, to=1.0, duration=300),

docs/guides/platform-accessibility.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ Mount [`StatusBar`][pythonnative.StatusBar] anywhere in the tree (it
5555
renders nothing visible) to control style and visibility:
5656

5757
```python
58-
pn.StatusBar(style="light", background_color="#000000")
58+
pn.StatusBar(bar_style="light", background_color="#000000")
5959
```
6060

6161
`style` is `"light"` (light icons, dark background), `"dark"` (dark

examples/hello-world/app/screens/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def _view_showcase() -> None:
3838

3939
return pn.ScrollView(
4040
pn.Column(
41-
pn.StatusBar(style="dark"),
41+
pn.StatusBar(bar_style="dark"),
4242
pn.Text("Settings", style=styles["title"]),
4343
pn.Text(f"PythonNative v{pn.__version__}", style=styles["subtitle"]),
4444
pn.Text(

examples/hello-world/app/screens/showcase.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@
3232

3333
@pn.component
3434
def AnimatedCard() -> pn.Element:
35-
"""Demonstrates ``Animated.View`` driven by ``AnimatedValue`` + ``use_memo``."""
36-
opacity = pn.use_memo(lambda: pn.Animated.Value(0.0), [])
37-
scale = pn.use_memo(lambda: pn.Animated.Value(0.9), [])
35+
"""Demonstrates ``Animated.View`` driven by ``use_animated_value``."""
36+
opacity = pn.use_animated_value(0.0)
37+
scale = pn.use_animated_value(0.9)
3838

3939
def _enter() -> None:
4040
pn.Animated.parallel(
@@ -63,8 +63,11 @@ def _enter() -> None:
6363
)
6464

6565

66+
@pn.memo
6667
@pn.component
6768
def TypographyDemo() -> pn.Element:
69+
"""Wrapped in [`pn.memo`][pythonnative.memo] so it skips re-render when parent state changes."""
70+
print("[TypographyDemo] render (should only appear once)")
6871
return pn.Column(
6972
pn.Text("Headline", style={"font_size": 28, "font_weight": "700"}),
7073
pn.Text(
@@ -84,6 +87,7 @@ def TypographyDemo() -> pn.Element:
8487
)
8588

8689

90+
@pn.memo
8791
@pn.component
8892
def BordersAndShadows() -> pn.Element:
8993
return pn.View(
@@ -96,6 +100,7 @@ def BordersAndShadows() -> pn.Element:
96100
)
97101

98102

103+
@pn.memo
99104
@pn.component
100105
def Chips() -> pn.Element:
101106
return pn.Row(
@@ -112,6 +117,20 @@ def Chips() -> pn.Element:
112117
)
113118

114119

120+
def section_heading(title: str, hint: str) -> pn.Element:
121+
"""Compose two sibling [`pn.Text`][pythonnative.Text] nodes via [`pn.Fragment`][pythonnative.Fragment].
122+
123+
Returning a Fragment from a plain helper (not a ``@pn.component``)
124+
lets the surrounding parent (here a [`pn.Column`][pythonnative.Column])
125+
flatten the siblings into its own child list without an extra
126+
wrapper view.
127+
"""
128+
return pn.Fragment(
129+
pn.Text(title, style=styles["section_title"]),
130+
pn.Text(hint, style=styles["hint"]),
131+
)
132+
133+
115134
@pn.component
116135
def ShowcaseScreen() -> pn.Element:
117136
nav = pn.use_navigation()
@@ -133,10 +152,10 @@ def go_back() -> None:
133152
pn.Column(
134153
pn.Text(message, style=styles["title"]),
135154
AnimatedCard(),
136-
pn.Text("Typography", style=styles["section_title"]),
155+
section_heading("Typography", "Memoized via @pn.memo; renders only once."),
137156
TypographyDemo(),
138157
BordersAndShadows(),
139-
pn.Text("Chips", style=styles["section_title"]),
158+
section_heading("Chips", "Composed via pn.Fragment without an extra container."),
140159
Chips(),
141160
pn.Pressable(
142161
pn.View(

src/pythonnative/__init__.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,39 +55,59 @@ def App():
5555

5656
from . import sdk
5757
from .alerts import Alert
58-
from .animated import Animated, AnimatedValue
58+
from .animated import Animated, AnimatedValue, use_animated_value
5959
from .components import (
6060
ActivityIndicator,
61+
ActivityIndicatorProps,
6162
Button,
63+
ButtonProps,
6264
Column,
6365
ErrorBoundary,
6466
FlatList,
67+
Fragment,
6568
Image,
69+
ImageProps,
6670
KeyboardAvoidingView,
71+
KeyboardAvoidingViewProps,
6772
Modal,
73+
ModalProps,
6874
Picker,
75+
PickerProps,
6976
Pressable,
77+
PressableProps,
7078
ProgressBar,
79+
ProgressBarProps,
7180
RefreshControl,
7281
Row,
7382
SafeAreaView,
83+
SafeAreaViewProps,
7484
ScrollView,
85+
ScrollViewProps,
7586
SectionList,
7687
Slider,
88+
SliderProps,
7789
Spacer,
90+
SpacerProps,
7891
StatusBar,
92+
StatusBarProps,
7993
Switch,
94+
SwitchProps,
8095
Text,
8196
TextInput,
97+
TextInputProps,
98+
TextProps,
8299
View,
100+
ViewProps,
83101
WebView,
102+
WebViewProps,
84103
)
85104
from .element import Element
86105
from .hooks import (
87106
Provider,
88107
batch_updates,
89108
component,
90109
create_context,
110+
memo,
91111
use_callback,
92112
use_context,
93113
use_effect,
@@ -152,6 +172,7 @@ def App():
152172
"Column",
153173
"ErrorBoundary",
154174
"FlatList",
175+
"Fragment",
155176
"Image",
156177
"KeyboardAvoidingView",
157178
"Modal",
@@ -171,13 +192,33 @@ def App():
171192
"TextInput",
172193
"View",
173194
"WebView",
195+
# Built-in Props dataclasses
196+
"ActivityIndicatorProps",
197+
"ButtonProps",
198+
"ImageProps",
199+
"KeyboardAvoidingViewProps",
200+
"ModalProps",
201+
"PickerProps",
202+
"PressableProps",
203+
"ProgressBarProps",
204+
"SafeAreaViewProps",
205+
"ScrollViewProps",
206+
"SliderProps",
207+
"SpacerProps",
208+
"StatusBarProps",
209+
"SwitchProps",
210+
"TextInputProps",
211+
"TextProps",
212+
"ViewProps",
213+
"WebViewProps",
174214
# Core
175215
"Element",
176216
"create_screen",
177217
# Hooks
178218
"batch_updates",
179219
"component",
180220
"create_context",
221+
"memo",
181222
"use_callback",
182223
"use_context",
183224
"use_effect",
@@ -225,6 +266,7 @@ def App():
225266
# Animation
226267
"Animated",
227268
"AnimatedValue",
269+
"use_animated_value",
228270
# Imperative
229271
"Alert",
230272
# Native modules

0 commit comments

Comments
 (0)