Skip to content

feat: DH-21756: Add ui.component memoization and selective re-rendering#1296

Open
mofojed wants to merge 23 commits into
deephaven:mainfrom
mofojed:ui-component-memoization
Open

feat: DH-21756: Add ui.component memoization and selective re-rendering#1296
mofojed wants to merge 23 commits into
deephaven:mainfrom
mofojed:ui-component-memoization

Conversation

@mofojed

@mofojed mofojed commented Feb 6, 2026

Copy link
Copy Markdown
Member
  • Added memo parameter to @ui.component to memoize a component, or pass a custom memoization function for checking behaviour
  • Implemented selective re-rendering - only rendering components that have had their state changed
    • This is more in line with how React renders components, and is much more efficient
    • Kind of needed to do this along with memoization; since we already needed to know if a child component was dirty if a parent was memoized

@mofojed mofojed requested a review from mattrunyon February 6, 2026 15:11
@mofojed mofojed self-assigned this Feb 6, 2026
@mofojed mofojed force-pushed the ui-component-memoization branch from f383c9d to e85f1bc Compare February 6, 2026 15:13
)
```

---

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this notation we could also write:

memo_parent = ui.memo(parent)

@mofojed mofojed changed the title plan: Add component memoization implementation plan feat: Add ui.memo component memoization Feb 10, 2026
@mofojed mofojed force-pushed the ui-component-memoization branch from 5e1318c to 305fc47 Compare February 10, 2026 21:01

@jnumainville jnumainville left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just gave some comments on the two options.

raise TypeError(
f"@ui.memo can only be used with @ui.component decorated functions. "
f"Got {type(element).__name__} instead."
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the fact that we are throwing this error after checking for @ui.component is a point against this option.
A third option would be that @ui.memo creates a ui.component under the hood since it has to be one anyways, but then it would have to duplicate arguments if we add more to ui.component, so more to maintain. I don't love that option either.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea after playing with @ui.memo more, I think it makes the most sense to just do the @ui.component(memo= option.

Comment on lines +474 to +475
- ❌ Two decorators required (more verbose)
- ❌ Easy to get decorator order wrong (`@ui.component` then `@ui.memo` won't work)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if there is more possible decorators (routers?), but these cons would compound quickly if we did have any others.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we thought of the decorators as just wrapper components like React, then the order at least makes intuitive sense

But I'm not sure I have a strong opinion on either syntax


**Cons:**

- ❌ Cannot memoize third-party components

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand this con, or at least I don't think it's meaningful? It would be easy enough to take third-party components and put them in your own memoized component without any real problems?
Maybe it's saying you can't do something like ui.memo(external_ui_component, ...) directly, but you can just wrap it in another component, and that isn't substantially more difficult.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, the con is minimal


## Recommendation

**Implement both options**, with Option B (`memo=`) as the primary API and Option A (`@ui.memo`) for advanced use cases.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say only B is my choice. Easier to maintain, much simpler to use, and I don't think these advanced use cases are really meaningful.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed

@mofojed mofojed changed the title feat: Add ui.memo component memoization feat: Add ui.component memoization and selective re-rendering Feb 13, 2026
@mofojed mofojed force-pushed the ui-component-memoization branch from c56be04 to 0241cc8 Compare February 13, 2026 13:24
@github-actions

Copy link
Copy Markdown

ui docs preview (Available for 14 days)

Comment on lines +261 to +298
items_bad = ["apple", "banana"]

# GOOD: Use use_memo to keep the same reference
items_good = ui.use_memo(lambda: ["apple", "banana"], [])

return ui.flex(
ui.button("Increment", on_press=lambda: set_count(count + 1)),
ui.text(f"Count: {count}"),
item_list(items_good), # Won't re-render unnecessarily
direction="column",
)


app_example = app()
```

### Passing Callback Functions

Lambda functions and inline function definitions create new references each render:

```python
from deephaven import ui


@ui.component(memo=True)
def button_row(on_click):
return ui.button("Click me", on_press=on_click)


@ui.component
def app():
count, set_count = ui.use_state(0)

# BAD: Creates a new function reference every render
# handle_click_bad = lambda: print("clicked")

# GOOD: Use use_callback to memoize the function
handle_click_good = ui.use_callback(lambda: print("clicked"), [])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent in presentation, line is left in, vs commented out

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opted to leave the line in, since it's assigning to a different variable name.

@mofojed mofojed changed the title feat: Add ui.component memoization and selective re-rendering feat: DH-21756: Add ui.component memoization and selective re-rendering Feb 23, 2026
@github-actions

Copy link
Copy Markdown

ui docs preview (Available for 14 days)

@mofojed mofojed force-pushed the ui-component-memoization branch from 17e0d28 to 1ff45d9 Compare March 27, 2026 18:26
@github-actions

Copy link
Copy Markdown

ui docs preview (Available for 14 days)

@mofojed mofojed force-pushed the ui-component-memoization branch from 1ff45d9 to 6188a0b Compare June 11, 2026 15:29
@github-actions

Copy link
Copy Markdown

ui docs preview (Available for 14 days)

2 similar comments
@github-actions

Copy link
Copy Markdown

ui docs preview (Available for 14 days)

@github-actions

Copy link
Copy Markdown

ui docs preview (Available for 14 days)

@mofojed mofojed force-pushed the ui-component-memoization branch from 2f3ed22 to 79da6df Compare June 12, 2026 14:19
@github-actions

Copy link
Copy Markdown

ui docs preview (Available for 14 days)

mofojed added 4 commits June 12, 2026 13:06
Two options for props-based memoization to skip re-renders:
- Option A: @ui.memo decorator (familiar to React devs)
- Option B: @ui.component(memo=True|compare_fn) parameter (cleaner)

Includes:
- API design and implementation details
- MemoizedFunctionElement and Renderer changes
- Unit tests for both options
- Performance benchmarks
- Comparison and recommendation (implement both)
- Checks props and if they're the same, just return the previously rendered node
- Still need to clean up the `_default_are_props_equal` and how children are handled, I think?
- Also need to add a bunch of unit tests. But it more or less works!

```
from deephaven import ui

def are_props_equal(old_props, new_props):
    print(f"Checking props {old_props} vs {new_props}")
    return old_props == new_props

@ui.component
def foo_component(name):
    value, set_value = ui.use_state(0)
    print(f"foo {name} render")
    return ui.button(f"foo {name} {value}", on_press=lambda: set_value(value+1))

@ui.memo(are_props_equal=are_props_equal)
@ui.component
def memo_foo_component(name):
    value, set_value = ui.use_state(0)
    print(f"memo_foo {name} render")
    return ui.button(f"foo {name} {value}", on_press=lambda: set_value(value+1))

memo_foo = ui.memo()(foo_component)

@ui.component
def bar_component():
    value, set_value = ui.use_state(0)

    return ui.flex(
        foo_component("A"),
        foo_component("B"),
        memo_foo_component("X"),
        memo_foo("Y"),
        ui.button(f"bar {value}", on_press=lambda: set_value(value+1))
    )

mf = memo_foo_component("mf")
b = bar_component()
```
mofojed added 19 commits June 12, 2026 13:06
- Allow @ui.memo syntax in addition to @ui.memo()
- Add tests for custom are_props_equal functions:
  - Deep equality comparison for object props
  - Always rerender (returns False)
  - Always skip (returns True)
  - Selective prop comparison
  - Threshold-based comparison
- 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
…nent

- Modified component.py to add memo parameter (True/False/callable)
- Removed standalone memo.py decorator
- Updated components/__init__.py exports
- Updated all tests to use @ui.component(memo=True) syntax
- Updated documentation in memoizing-components.md and render-cycle.md
- No special treatment for children
- Now it is optimized to only re-render when necessary
- Needed to fix up some existing tests that was relying on the previous non-optimized behaviour
- Added some unit tests
- Don't allow `GeneratorType` as a type for children
- Now `children` is a more specific type
- Otherwise we get errors when reloading widgets
- Changes PropsType to a Mapping instead of a Dict. Allows TypedDicts to be passed in as well then, and we don't need to modify props so it's more accurate
- Add `float` to NodeType
- Added a note to update a few spots where we pass a Table or ItemTableSource back directly, rather than wrapping them in an Element or something else
Wrap on_change/on_queue callbacks in a TestRoot via a thin shim to
match the new single-root RenderContext constructor signature.
The default memo comparison was changed to plain dict_shallow_equal, which
compares by identity. Positional args are bundled into a fresh 'children'
tuple every render, so memoization never matched and components always
re-rendered. Restore _default_are_props_equal to compare children by value
and shallow-compare remaining props, and revert the test expectation that
had been flipped to encode the buggy behavior.
…ering

Under selective re-rendering, a component only re-renders when a dirty render
is propagated or its own RenderContext is marked dirty. URL changes went
through _set_url_state, which updated the URL and set the stream-level dirty
flag but never marked any RenderContext dirty. As a result, components reading
the URL (e.g. use_path, use_query_params) hit the render cache and never
updated, breaking routing and query-param navigation.

Add a public RenderContext.mark_dirty() and call it from _set_url_state so the
root context (and its children) re-render when the URL changes, mirroring how
initial state import marks the root dirty.
@mofojed mofojed force-pushed the ui-component-memoization branch from 79da6df to fd1996f Compare June 12, 2026 17:07
@github-actions

Copy link
Copy Markdown

ui docs preview (Available for 14 days)

Comment on lines +439 to +441
# The URL is not tracked as component state, so mark the root context dirty
# to ensure components that read the URL (e.g. use_path) are re-rendered.
self._context.mark_dirty()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, we'd have something like ui.context that could be used for getting/setting the path, and then wire that up to handle marking consumers dirty. Not part of this change.

@mofojed mofojed marked this pull request as ready for review June 12, 2026 17:10
@github-actions github-actions Bot requested a review from margaretkennedy June 12, 2026 17:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants