-
Notifications
You must be signed in to change notification settings - Fork 50.9k
Comparing changes
Open a pull request
base repository: facebook/react
base: 4a3d993e
head repository: facebook/react
compare: bef88f7c
- 12 commits
- 34 files changed
- 5 contributors
Commits on Jan 15, 2026
-
Improve the detection of changed hooks (#35123)
## Summary cc @hoxyq Fixes #28584. Follow up to PR: #34547 This PR updates getChangedHooksIndices to account for the fact that `useSyncExternalStore`, `useTransition`, `useActionState`, `useFormState` internally mounts more than one hook while DevTools should treat it as a single user-facing hook. Approach idea came from [this](#34547 (comment)) comment 😄 Before: https://github.com/user-attachments/assets/6bd5ce80-8b52-4bb8-8bb1-5e91b1e65043 After: https://github.com/user-attachments/assets/47f56898-ab34-46b6-be7a-a54024dcefee ## How did you test this change? I used this component to reproduce this issue locally (I followed instructions in `packages/react-devtools/CONTRIBUTING.md`). <details><summary>Details</summary> ```ts import * as React from 'react'; function useDeepNestedHook() { React.useState(0); // 1 return React.useState(1); // 2 } function useNestedHook() { const deepState = useDeepNestedHook(); React.useState(2); // 3 React.useState(3); // 4 return deepState; } // Create a simple store for useSyncExternalStore function createStore(initialValue) { let value = initialValue; const listeners = new Set(); return { getSnapshot: () => value, subscribe: listener => { listeners.add(listener); return () => { listeners.delete(listener); }; }, update: newValue => { value = newValue; listeners.forEach(listener => listener()); }, }; } const syncExternalStore = createStore(0); export default function InspectableElements(): React.Node { const [nestedState, setNestedState] = useNestedHook(); // 5 const syncExternalValue = React.useSyncExternalStore( syncExternalStore.subscribe, syncExternalStore.getSnapshot, ); // 6 const [isPending, startTransition] = React.useTransition(); // 7 const [formState, formAction, formPending] = React.useActionState( async (prevState, formData) => { return {count: (prevState?.count || 0) + 1}; }, {count: 0}, ); const handleTransition = () => { startTransition(() => { setState(Math.random()); }); }; // 8 const [state, setState] = React.useState('test'); return ( <> <div style={{ padding: '20px', display: 'flex', flexDirection: 'column', gap: '10px', }}> <div onClick={() => setNestedState(Math.random())} style={{backgroundColor: 'red', padding: '10px', cursor: 'pointer'}}> State: {nestedState} </div> <button onClick={handleTransition} style={{padding: '10px'}}> Trigger Transition {isPending ? '(pending...)' : ''} </button> <div style={{display: 'flex', gap: '10px', alignItems: 'center'}}> <button onClick={() => syncExternalStore.update(syncExternalValue + 1)} style={{padding: '10px'}}> Trigger useSyncExternalStore </button> <span>Value: {syncExternalValue}</span> </div> <form action={formAction} style={{display: 'flex', gap: '10px', alignItems: 'center'}}> <button type="submit" style={{padding: '10px'}} disabled={formPending}> Trigger useFormState {formPending ? '(pending...)' : ''} </button> <span>Count: {formState.count}</span> </form> <div onClick={() => setState(Math.random())} style={{backgroundColor: 'red', padding: '10px', cursor: 'pointer'}}> State: {state} </div> </div> </> ); } ``` </details> --------- Co-authored-by: Ruslan Lesiutin <28902667+hoxyq@users.noreply.github.com>
Configuration menu - View commit details
-
Copy full SHA for 53daaf5 - Browse repository at this point
Copy the full SHA 53daaf5View commit details -
Configuration menu - View commit details
-
Copy full SHA for fae15df - Browse repository at this point
Copy the full SHA fae15dfView commit details -
Configuration menu - View commit details
-
Copy full SHA for bb8a76c - Browse repository at this point
Copy the full SHA bb8a76cView commit details
Commits on Jan 16, 2026
-
[Fiber] fix useId tracking on replay (#35518)
When Fiber replays work after suspending and resolving in a microtask it stripped the Forked flag from Fibers because this flag type was not considered a Static flag. The Forked nature of a Fiber is not render dependent and should persist after unwinding work. By making this change the replay correctly generates the necessary tree context.
Configuration menu - View commit details
-
Copy full SHA for f0fbb0d - Browse repository at this point
Copy the full SHA f0fbb0dView commit details -
Commit the Gesture lane if a gesture ends closer to the target state (#…
…35486) Stacked on #35485. Before this PR, the `startGestureTransition` API would itself never commit its state. After the gesture releases it stops the animation in the next commit which just leaves the DOM tree in the original state. If there's an actual state change from the Action then that's committed as the new DOM tree. To avoid animating from the original state to the new state again, this is DOM without an animation. However, this means that you can't have the actual action committing be in a slightly different state and animate between the final gesture state and into the new action. Instead, we now actually keep the render tree around and commit it in the end. Basically we assume that if the Timeline was closer to the end then visually you're already there and we can commit into that state. Most of the time this will be at the actual end state when you release but if you have something else cancelling the gesture (e.g. `touchcancel`) it can still commit this state even though your gesture recognizer might not consider this an Action. I think this is ok and keeps it simple. When the gesture lane commits, it'll leave a Transition behind as work from the revert lanes on the Optimistic updates. This means that if you don't do anything in the Action this will cause another commit right after which reverts. This revert can animate the snap back. There's a few fixes needed in follow up PRs: - Fixed in #35487. ~To support unentangled Transitions we need to explicitly entangle the revert lane with the Action to avoid committing a revert followed by a forward instead of committing the forward entangled with the revert. This just works now since everything is entangled but won't work with #35392.~ - Fixed in #35510. ~This currently rerenders the gesture lane once before committing if it was already completed but blocked. We should be able to commit the already completed tree as is.~
Configuration menu - View commit details
-
Copy full SHA for 4028aaa - Browse repository at this point
Copy the full SHA 4028aaaView commit details -
Entangle Gesture revert commit with the corresponding Action commit (#…
…35487) Stacked on #35486. When a Gesture commits, it leaves behind work on a Transition lane (`revertLane`). This entangles that lane with whatever lane we're using in the event that cancels the Gesture. This ensures that the revert and the result of any resulting Action commits as one batch. Typically the Action would apply a new state that is similar or the same as the revert of the Gesture. This makes it resilient to unbatching in #35392.
Configuration menu - View commit details
-
Copy full SHA for 35a81ce - Browse repository at this point
Copy the full SHA 35a81ceView commit details -
Defer useDeferredValue updates in Gestures (#35511)
If an initial value is specified, then it's always used regardless as part of the gesture render. If a gesture render causes an update, then previously that was not treated as deferred and could therefore be blocking the render. However, a gesture is supposed to flush synchronously ideally. Therefore we should consider these as urgent. The effect is that useDeferredValue renders the previous state.
Configuration menu - View commit details
-
Copy full SHA for eac3c95 - Browse repository at this point
Copy the full SHA eac3c95View commit details -
Optimize gesture by allowing the original work in progress tree to be…
… a suspended commit (#35510) Stacked on #35487. This is slightly different because the first suspended commit is on blockers that prevent us from committing which still needs to be resolved first. If a gesture lane has to be rerendered while the gesture is happening then it reenters this state with a new tree. (Currently this doesn't happen for a ping I think which is not really how it usually works but better in this case.)
Configuration menu - View commit details
-
Copy full SHA for 4cf9063 - Browse repository at this point
Copy the full SHA 4cf9063View commit details -
[Fiber] Instrument the lazy initializer thenable in all cases (#35521)
When a lazy element or component is initialized a thenable is returned which was only be conditionally instrumented in dev when asyncDebugInfo was enabled. When instrumented these thenables can be used in conjunction with the SuspendOnImmediate optimization where if a thenable resolves before the stack unwinds we can continue rendering from the last suspended fiber. Without this change a recent fix to the useId implementation cannot be easily tested in production because this optimization pathway isn't available to regular React.lazy thenables. To land the prior PR I changed the thenables to a custom type so I could instrument manually in the test. WIth this change we can just use a regular Promise since ReactLazy will instrument in all environments/flags now
Configuration menu - View commit details
-
Copy full SHA for db71391 - Browse repository at this point
Copy the full SHA db71391View commit details -
Configuration menu - View commit details
-
Copy full SHA for cbc4d40 - Browse repository at this point
Copy the full SHA cbc4d40View commit details -
[DevTools] Clear element inspection if host element not owned by any …
…renderer is selected (#35504)
Configuration menu - View commit details
-
Copy full SHA for 01c4d03 - Browse repository at this point
Copy the full SHA 01c4d03View commit details -
Configuration menu - View commit details
-
Copy full SHA for bef88f7 - Browse repository at this point
Copy the full SHA bef88f7View commit details
This comparison is taking too long to generate.
Unfortunately it looks like we can’t render this comparison for you right now. It might be too big, or there might be something weird with your repository.
You can try running this command locally to see the comparison on your machine:
git diff 4a3d993e...bef88f7c