Add experimental conditional hooks runtime - #74
Draft
aidenybai wants to merge 4 commits into
Draft
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
commit: |
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.
What this PR does
This PR makes ordinary React hooks work inside branches whose hook order changes between renders, without patching or forking React.
There is one consumer-facing model:
installConditionalHooks()before the application renders.React.useState,React.useReducer,React.useEffect, and related APIs.There are no public keyed-hook functions and no opt-in interception flag. Interception is automatic after installation. All callsite identity is derived internally.
Important
This only works with React development renderers. It relies on private renderer fields exposed to DevTools:
currentDispatcherRef,getCurrentFiber, andscheduleUpdate. It is experimental, unsupported, and not production-safe.Complete runnable example
The installer must execute before the module that renders the React application.
main.tsx
The dynamic import is intentional. It ensures Bippy is listening before ReactDOM injects its renderer and before the first component render.
app.tsx
The observable behavior is:
Try the playground
cd packages/conditional-hooks-playground nr devThe playground shows the full component, highlights the conditional block, and walks through the enable, update, disable, and re-enable sequence.
How the implementation works
The implementation lives in
packages/bippy/src/conditional-hooks.ts.1. Observe React renderers
Bippy attaches through the same global hook used by React DevTools.
Installation handles renderers that already exist and renderers injected later:
A renderer is supported only when it exposes:
These are development-only DevTools integration fields. No React source or bundle is modified.
2. Intercept the active hook dispatcher
Before React renders a function component, it assigns the dispatcher used by calls such as
React.useState().Bippy replaces the dispatcher property with a getter and setter:
The setter observes when React enters a render and captures the current Fiber:
The getter returns a proxy around React's active dispatcher.
3. Redirect normal React hook calls
The proxy redirects supported hook methods into private cell readers:
The application still calls
React.useState(0). React consults its active dispatcher, and the dispatcher proxy privately routes the call into Bippy.This does not mutate the exported React object.
The automatically handled APIs are:
useStateuseReduceruseRefuseMemouseCallbackuseEffectuseLayoutEffectuseContextis forwarded to React's context reader.useDebugValueis accepted as a no-op. Other hooks continue through React's dispatcher unchanged.4. Derive hook identity automatically
React normally identifies a hook by its position in the hook list. Bippy instead creates an internal identity from the application callsite.
The default resolver captures a stack and selects the first frame outside React, Bippy, and bundled React runtime files:
A source line can execute repeatedly, such as in a loop. A render-local occurrence counter distinguishes those calls:
Consumers do not pass these keys. They are an internal implementation detail.
5. Store cells beside the Fiber
React stores its positional hooks in
fiber.memoizedState. This runtime deliberately does not add anything to that linked list.Instead, each component Fiber owns a private scope:
A state cell contains the state value and stable dispatch function:
A reducer cell additionally contains its current reducer and queued actions. Ref and memo cells contain their corresponding values and dependencies.
React swaps between a current Fiber and a work-in-progress alternate. Both Fiber objects are associated with the same scope:
That preserves state across renders while keeping separate mounted component instances isolated.
6. Read or create a state cell
The private state path is:
Consequences:
Reducers follow the same lookup model and replay pending actions with the reducer from the render processing them.
7. Schedule the owning component
Because these cells are outside React's normal hook queue, a setter must explicitly schedule the owning Fiber:
The props changes work around React bailouts and
React.memo. They ensure React notices a side-table update even when ordinary props are referentially unchanged.8. Keep renders transactional
Each render writes into a temporary frame:
Only a successful commit promotes the frame:
If React abandons a render because it suspends, throws, or is replaced by newer work, that frame is never promoted. Its temporary state and effects cannot leak into the committed tree.
9. Reconcile effects after commit
Each successful render records the conditional effects that actually ran. Commit compares the new registrations with the previous committed effects:
This gives the expected branch lifecycle:
The runtime also handles Strict Mode replay and hidden Suspense/Activity subtrees.
Full event sequence
Public API
The public surface is intentionally small:
Normal usage needs no options:
supportedRenderersis a live count. The optional resolver customizes automatic callsite derivation globally; it does not add a per-hook keyed API.Test coverage
The 86 focused, stress, adversarial, and React-upstream-derived cases all exercise ordinary
React.use*calls. They cover:Validation:
Result: 642 tests passed, 1 expected failure, and 3 skipped.
Known boundaries