Skip to content

Conversation

@liuxiaopai-ai
Copy link

Summary

Fixes #53 — Renderer component causes infinite re-renders when given a static JSON tree in non-streaming scenarios.

Root Causes

The infinite re-render loop was caused by multiple interacting issues in the context providers:

1. Unstable get callback in DataProvider

useCallback for get depended on data state, so every data change created a new get → new context value → all consumers re-rendered. Fixed by using a ref to read current data, keeping callback identity stable.

2. Unstable execute callback in ActionProvider

execute depended on data, handlers, set, and navigate directly. Any data change recreated execute → new context value → consumers re-render → potential cascading updates. Fixed by reading frequently-changing values from refs.

3. Inline actionHandlers object in createRenderer

A new { __default__: ... } object was created on every render of CatalogRenderer, causing ActionProvider to receive new props each cycle. Fixed by memoizing with useMemo and using a ref for the onAction callback.

4. Missing bail-out in DataProvider sync effect

setData((prev) => ({ ...prev, ...initialData })) always created a new object reference even when values hadn't changed, triggering unnecessary re-renders. Added a JSON equality check to bail out when the merged result is identical.

Changes

  • packages/react/src/contexts/data.tsx — Stabilized get callback with ref; added bail-out in sync effect
  • packages/react/src/contexts/actions.tsx — Stabilized execute callback with refs; added handler sync effect
  • packages/react/src/renderer.tsx — Memoized actionHandlers in createRenderer
  • packages/react/src/contexts/rerender.test.tsx — Added 4 regression tests

Tests

All 186 tests pass (11 test files), including 4 new regression tests that verify:

  • DataProvider doesn't re-render when initialData reference changes but value stays the same
  • Empty initialData doesn't trigger infinite updates
  • Full provider stack (Data + Visibility + Action) stabilizes with static data
  • get callback identity remains stable across data changes

Root causes fixed:
1. DataProvider.get callback depended on data state, causing cascading
   memo invalidation through all consumers. Now uses a ref to read
   current data, keeping the callback identity stable.
2. ActionProvider.execute depended on data/handlers/set/navigate directly,
   so every data change recreated the callback and invalidated the context
   value. Now uses refs for frequently-changing values.
3. createRenderer created a new actionHandlers object on every render,
   causing ActionProvider to receive new props each cycle. Now memoized
   with useMemo.
4. DataProvider setData in the sync useEffect could produce a new object
   reference even when values hadn't changed. Added bail-out check.
5. Added useEffect to sync ActionProvider handlers when props change.

Closes vercel-labs#53
@vercel
Copy link
Contributor

vercel bot commented Feb 7, 2026

Someone is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: df503e26b0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// Only update if the handlers object reference actually changed and has entries
if (initialHandlers !== initialHandlersRef.current) {
initialHandlersRef.current = initialHandlers;
if (Object.keys(initialHandlers).length > 0) {

Choose a reason for hiding this comment

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

P1 Badge Sync handler state when handlers prop is cleared

The new synchronization effect only updates state when initialHandlers has keys, so if the parent later passes {}/undefined (for example, createRenderer toggling onAction off), the previous handler map is retained indefinitely. In that state, execute still sees actions as registered and can continue running downstream onSuccess/onError behavior, so disabling handlers no longer actually disables action execution. The sync path should also clear handlers when the prop becomes empty.

Useful? React with 👍 / 👎.

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.

Infinite Re-renders Caused By the Renderer Component?

1 participant