fix(render-props): keep render prop state alive across renders - #193
Open
rkaraivanov wants to merge 2 commits into
Open
fix(render-props): keep render prop state alive across renders#193rkaraivanov wants to merge 2 commits into
rkaraivanov wants to merge 2 commits into
Conversation
The render prop machinery kept its state in per-render locals while the
element held on to the patched templates indefinitely, so the two drifted
apart the moment the component re-rendered. Three defects came out of that:
* Templates rendered against a stale closure. The patched function handed
to the element was cached on first sight - but so was the callback it
invoked, so a template closing over React state kept rendering the value
it saw on the very first render.
* An `undefined` render prop spun the render loop. A conditional prop
(`headerTemplate={enabled ? tpl : undefined}`) was still treated as a
template: it installed an empty portal and recreated the patched function
on every render. Each new identity re-rendered the element, which
re-requested the template, at roughly 97 cycles a second. The churn also
blanked sibling templates on the same component.
* Portal updates were lost. The map of active slots was cloned per render,
so a request arriving from the element mutated its own copy and a later
render overwrote it.
That state now lives on a `TemplateBridge` owned by the component instance -
patched templates keyed by prop path, current callbacks keyed by renderer
name, the slots, and the nested prop containers. Callbacks are refreshed on
every render while the patched templates keep their identity, so the element
sees a stable prop and the template always runs the current closure. Portals
are built when the element requests a slot, and rebuilt only when the render
prop's identity changes.
Pruning is driven by a single predicate - is there still a function at this
prop path? - which covers both a removed prop and one that turned
`undefined`. It also drops the matching slots: the directive's remove request
travels through a `WeakRef` and may never arrive once the patched template is
gone, leaving the portal to linger for the component's lifetime.
Nested prop containers reuse their previous object while shallow-equal, so a
config object carrying a template stops changing identity every render.
The rest of the file follows the same split. Ref forwarding and the Angular
re-parenting effect move into `useForwardedRef` and `useReparenting`, leaving
`createComponent` with registration and a nine-line component.
In `render-props.ts`: `_renderNode` could return `undefined` behind an
`as Element` cast; requests now go through a helper that bails when there is
no node. `render()` is typed as `RendererCallback<T>` so `any` stops leaking
into `update()`. `_state.previous` resets on every disconnect, not just some.
Adds regression tests for the three defects, each confirmed to fail against
the previous code.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The render prop machinery kept its state in per-render locals while the
element held on to the patched templates indefinitely, so the two drifted
apart the moment the component re-rendered. Three defects came out of that:
Templates rendered against a stale closure. The patched function handed
to the element was cached on first sight - but so was the callback it
invoked, so a template closing over React state kept rendering the value
it saw on the very first render.
An
undefinedrender prop spun the render loop. A conditional prop(
headerTemplate={enabled ? tpl : undefined}) was still treated as atemplate: it installed an empty portal and recreated the patched function
on every render. Each new identity re-rendered the element, which
re-requested the template, at roughly 97 cycles a second. The churn also
blanked sibling templates on the same component.
Portal updates were lost. The map of active slots was cloned per render,
so a request arriving from the element mutated its own copy and a later
render overwrote it.
That state now lives on a
TemplateBridgeowned by the component instance -patched templates keyed by prop path, current callbacks keyed by renderer
name, the slots, and the nested prop containers. Callbacks are refreshed on
every render while the patched templates keep their identity, so the element
sees a stable prop and the template always runs the current closure. Portals
are built when the element requests a slot, and rebuilt only when the render
prop's identity changes.
Additional information (check all that apply):
Checklist: