Skip to content

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

Draft
mofojed wants to merge 17 commits intodeephaven:mainfrom
mofojed:ui-component-memoization
Draft

feat: Add ui.component memoization and selective re-rendering#1296
mofojed wants to merge 17 commits intodeephaven:mainfrom
mofojed:ui-component-memoization

Conversation

@mofojed
Copy link
Member

@mofojed mofojed commented Feb 6, 2026

  • 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
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
Copy link
Collaborator

@jnumainville jnumainville left a comment

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
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
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
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
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
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
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
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
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
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()
```
- 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
- BREAKING CHANGE: No longer allowing GeneratorType to be returned as children
  - Doesn't really make sense anyways, as we consume it all immediately so no real savings over a `list`...
  - Should update typing to match
- Don't allow `GeneratorType` as a type for children
- Now `children` is a more specific type
@mofojed mofojed force-pushed the ui-component-memoization branch from c56be04 to 0241cc8 Compare February 13, 2026 13:24
- 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
@github-actions
Copy link

ui docs preview (Available for 14 days)

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.

3 participants