Skip to content

Commit 5e1318c

Browse files
committed
docs: Add documentation for @ui.memo component memoization
- Update render-cycle.md with section on optimizing re-renders - Create memoizing-components.md with comprehensive guide: - Basic usage and how memoization works - When to use @ui.memo - Custom comparison with are_props_equal - Common pitfalls (new objects, callbacks) - Comparison with use_memo hook - Add memoizing-components to sidebar navigation
1 parent 806f223 commit 5e1318c

3 files changed

Lines changed: 369 additions & 0 deletions

File tree

Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
# Memoizing Components
2+
3+
`@ui.memo` is a decorator that optimizes component rendering by skipping re-renders when a component's props haven't changed. This is similar to [React.memo](https://react.dev/reference/react/memo) and is useful for improving performance in components that render often with the same props.
4+
5+
> [!NOTE] > `@ui.memo` is for memoizing entire components. To memoize a value or computation within a component, use the [`use_memo`](../hooks/use_memo.md) hook instead.
6+
7+
## Basic Usage
8+
9+
Wrap your component with `@ui.memo` to skip re-renders when props are unchanged:
10+
11+
```python
12+
from deephaven import ui
13+
14+
15+
@ui.memo
16+
@ui.component
17+
def greeting(name):
18+
print(f"Rendering greeting for {name}")
19+
return ui.text(f"Hello, {name}!")
20+
21+
22+
@ui.component
23+
def app():
24+
count, set_count = ui.use_state(0)
25+
26+
return ui.flex(
27+
ui.button("Increment", on_press=lambda: set_count(count + 1)),
28+
ui.text(f"Count: {count}"),
29+
greeting("World"), # Won't re-render when count changes
30+
direction="column",
31+
)
32+
33+
34+
app_example = app()
35+
```
36+
37+
In this example, clicking the button increments `count`, causing `app` to re-render. However, `greeting` will not re-render because its prop (`"World"`) hasn't changed.
38+
39+
## How It Works
40+
41+
By default, when a parent component re-renders, all of its child components re-render too. With `@ui.memo`, `deephaven.ui` compares the new props with the previous props using shallow equality. If all props are equal, the component skips rendering and reuses its previous result.
42+
43+
The render cycle with memoization:
44+
45+
1. **Trigger**: Parent component state changes
46+
2. **Render**: Parent re-renders, but memoized children with unchanged props are skipped
47+
3. **Commit**: Only changed parts of the UI are updated
48+
49+
## When to Use `@ui.memo`
50+
51+
Use `@ui.memo` when:
52+
53+
- A component renders often with the same props
54+
- A component is expensive to render (complex calculations, many children)
55+
- A parent component re-renders frequently but passes stable props to children
56+
57+
Don't use `@ui.memo` when:
58+
59+
- The component's props change on almost every render
60+
- The component is cheap to render
61+
- You're prematurely optimizing without measuring performance
62+
63+
```python
64+
from deephaven import ui
65+
66+
67+
# Good candidate: renders same static content while parent updates
68+
@ui.memo
69+
@ui.component
70+
def expensive_chart(data):
71+
# Imagine this does complex data processing
72+
return ui.text(f"Chart with {len(data)} points")
73+
74+
75+
# Not a good candidate: props change every render
76+
@ui.component
77+
def live_counter(count):
78+
return ui.text(f"Count: {count}")
79+
80+
81+
@ui.component
82+
def dashboard():
83+
count, set_count = ui.use_state(0)
84+
chart_data = [1, 2, 3, 4, 5] # Static data
85+
86+
return ui.flex(
87+
ui.button("Update", on_press=lambda: set_count(count + 1)),
88+
live_counter(count), # No benefit from memo - count always changes
89+
expensive_chart(chart_data), # Benefits from memo - data is stable
90+
direction="column",
91+
)
92+
93+
94+
dashboard_example = dashboard()
95+
```
96+
97+
## Custom Comparison with `are_props_equal`
98+
99+
By default, `@ui.memo` uses shallow equality to compare props. You can provide a custom comparison function using the `are_props_equal` parameter:
100+
101+
```python
102+
from deephaven import ui
103+
104+
105+
def compare_by_id(prev_props, next_props):
106+
"""Only re-render if the 'id' prop changes."""
107+
return prev_props.get("id") == next_props.get("id")
108+
109+
110+
@ui.memo(are_props_equal=compare_by_id)
111+
@ui.component
112+
def user_card(id, name, last_updated):
113+
return ui.flex(
114+
ui.text(f"User #{id}"),
115+
ui.text(f"Name: {name}"),
116+
ui.text(f"Updated: {last_updated}"),
117+
direction="column",
118+
)
119+
120+
121+
@ui.component
122+
def user_profile():
123+
name, set_name = ui.use_state("Alice")
124+
timestamp, set_timestamp = ui.use_state("12:00")
125+
126+
return ui.flex(
127+
ui.button("Update timestamp", on_press=lambda: set_timestamp("12:01")),
128+
ui.button("Change name", on_press=lambda: set_name("Bob")),
129+
# Only re-renders if id changes, not name or last_updated
130+
user_card(id=1, name=name, last_updated=timestamp),
131+
direction="column",
132+
)
133+
134+
135+
user_profile_example = user_profile()
136+
```
137+
138+
The `are_props_equal` function receives two dictionaries:
139+
140+
- `prev_props`: The props from the previous render
141+
- `next_props`: The props for the current render
142+
143+
Return `True` to skip re-rendering (props are "equal"), or `False` to re-render.
144+
145+
### Deep Equality Comparison
146+
147+
For props containing nested data structures, you might want deep equality:
148+
149+
```python
150+
from deephaven import ui
151+
152+
153+
def deep_equal(prev_props, next_props):
154+
"""Compare props using deep equality."""
155+
import json
156+
157+
return json.dumps(prev_props, sort_keys=True) == json.dumps(
158+
next_props, sort_keys=True
159+
)
160+
161+
162+
@ui.memo(are_props_equal=deep_equal)
163+
@ui.component
164+
def data_display(config):
165+
return ui.text(f"Config: {config}")
166+
167+
168+
@ui.component
169+
def app():
170+
count, set_count = ui.use_state(0)
171+
172+
return ui.flex(
173+
ui.button("Increment", on_press=lambda: set_count(count + 1)),
174+
# Even though a new dict is created each render, deep_equal
175+
# will detect the values are the same and skip re-rendering
176+
data_display(config={"setting": "value", "enabled": True}),
177+
direction="column",
178+
)
179+
180+
181+
app_example = app()
182+
```
183+
184+
### Threshold-Based Comparison
185+
186+
You can implement more sophisticated comparison logic:
187+
188+
```python
189+
from deephaven import ui
190+
191+
192+
def significant_change(prev_props, next_props, threshold=5):
193+
"""Only re-render if value changes by more than threshold."""
194+
prev_value = prev_props.get("value", 0)
195+
next_value = next_props.get("value", 0)
196+
return abs(next_value - prev_value) <= threshold
197+
198+
199+
@ui.memo(are_props_equal=significant_change)
200+
@ui.component
201+
def progress_bar(value):
202+
return ui.progress_bar(value=value, label=f"{value}%")
203+
204+
205+
@ui.component
206+
def app():
207+
value, set_value = ui.use_state(0)
208+
209+
return ui.flex(
210+
ui.button("+1", on_press=lambda: set_value(value + 1)),
211+
ui.button("+10", on_press=lambda: set_value(value + 10)),
212+
# Only re-renders when value changes by more than 5
213+
progress_bar(value=value),
214+
direction="column",
215+
)
216+
217+
218+
app_example = app()
219+
```
220+
221+
## Decorator Syntax
222+
223+
Both syntax forms are supported:
224+
225+
```python
226+
# Without parentheses (uses default shallow comparison)
227+
@ui.memo
228+
@ui.component
229+
def my_component(prop):
230+
return ui.text(prop)
231+
232+
233+
# With parentheses (allows custom comparison)
234+
@ui.memo()
235+
@ui.component
236+
def my_component_with_parens(prop):
237+
return ui.text(prop)
238+
239+
240+
# With custom comparison function
241+
@ui.memo(are_props_equal=my_custom_compare)
242+
@ui.component
243+
def my_component_custom(prop):
244+
return ui.text(prop)
245+
```
246+
247+
## Common Pitfalls
248+
249+
### Creating New Objects in Props
250+
251+
When you pass a new object, list, or dictionary as a prop, it will always be a different reference, causing re-renders even if the content is the same:
252+
253+
```python
254+
from deephaven import ui
255+
256+
257+
@ui.memo
258+
@ui.component
259+
def item_list(items):
260+
return ui.flex(*[ui.text(item) for item in items], direction="column")
261+
262+
263+
@ui.component
264+
def app():
265+
count, set_count = ui.use_state(0)
266+
267+
# BAD: Creates a new list on every render
268+
# item_list will re-render every time even though content is the same
269+
items_bad = ["apple", "banana"]
270+
271+
# GOOD: Use use_memo to keep the same reference
272+
items_good = ui.use_memo(lambda: ["apple", "banana"], [])
273+
274+
return ui.flex(
275+
ui.button("Increment", on_press=lambda: set_count(count + 1)),
276+
ui.text(f"Count: {count}"),
277+
item_list(items_good), # Won't re-render unnecessarily
278+
direction="column",
279+
)
280+
281+
282+
app_example = app()
283+
```
284+
285+
### Passing Callback Functions
286+
287+
Lambda functions and inline function definitions create new references each render:
288+
289+
```python
290+
from deephaven import ui
291+
292+
293+
@ui.memo
294+
@ui.component
295+
def button_row(on_click):
296+
return ui.button("Click me", on_press=on_click)
297+
298+
299+
@ui.component
300+
def app():
301+
count, set_count = ui.use_state(0)
302+
303+
# BAD: Creates a new function reference every render
304+
# handle_click_bad = lambda: print("clicked")
305+
306+
# GOOD: Use use_callback to memoize the function
307+
handle_click_good = ui.use_callback(lambda: print("clicked"), [])
308+
309+
return ui.flex(
310+
ui.button("Increment", on_press=lambda: set_count(count + 1)),
311+
button_row(on_click=handle_click_good), # Won't re-render unnecessarily
312+
direction="column",
313+
)
314+
315+
316+
app_example = app()
317+
```
318+
319+
## Comparison with `use_memo`
320+
321+
| Feature | `@ui.memo` | `use_memo` |
322+
| ------- | ----------------------------- | ---------------------- |
323+
| Purpose | Skip re-rendering a component | Cache a computed value |
324+
| Usage | Decorator on component | Hook inside component |
325+
| Input | Component props | Dependencies array |
326+
| Output | Memoized component | Memoized value |
327+
328+
Use `@ui.memo` to optimize component rendering. Use `use_memo` to optimize expensive calculations within a component.

plugins/ui/docs/add-interactivity/render-cycle.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,40 @@ clock_example = clock_wrapper()
122122
This works because during this last step, React only updates the content of `ui.header` with the new time. It sees that the `ui.text_field` appears in the JSX in the same place as last time, so React doesn’t touch the `ui.text_field` or its value.
123123

124124
After rendering is done and React updated the DOM, the browser will repaint the screen.
125+
126+
## Optimizing Re-renders with `@ui.memo`
127+
128+
By default, when any component's state changes, `deephaven.ui` re-renders the entire component tree from the root—not just the component that triggered the change or its children, but every component in the tree. This is usually not a problem, but if you have a deeply nested tree or expensive components, you can optimize performance by wrapping components with `@ui.memo`.
129+
130+
The `@ui.memo` decorator tells `deephaven.ui` to skip re-rendering a component when its props haven't changed:
131+
132+
```python
133+
from deephaven import ui
134+
135+
136+
@ui.memo
137+
@ui.component
138+
def expensive_child(value):
139+
# This component will only re-render when `value` changes
140+
return ui.text(f"Value: {value}")
141+
142+
143+
@ui.component
144+
def parent():
145+
count, set_count = ui.use_state(0)
146+
static_value = "hello"
147+
148+
return ui.flex(
149+
ui.button("Increment", on_press=lambda: set_count(count + 1)),
150+
ui.text(f"Count: {count}"),
151+
# This child won't re-render when count changes because static_value stays the same
152+
expensive_child(static_value),
153+
)
154+
155+
156+
parent_example = parent()
157+
```
158+
159+
In this example, clicking the button updates `count`, which causes `parent` to re-render. However, `expensive_child` will skip re-rendering because its `value` prop (`"hello"`) hasn't changed.
160+
161+
For more details on when and how to use memoization effectively, see [Memoizing Components](./memoizing-components.md).

plugins/ui/docs/sidebar.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@
8181
"label": "Render Cycle",
8282
"path": "add-interactivity/render-cycle.md"
8383
},
84+
{
85+
"label": "Memoizing Components",
86+
"path": "add-interactivity/memoizing-components.md"
87+
},
8488
{
8589
"label": "State as a Snapshot",
8690
"path": "add-interactivity/state-as-a-snapshot.md"

0 commit comments

Comments
 (0)