mirror of
https://github.com/facebook/react.git
synced 2026-02-24 12:43:00 +00:00
## Summary cc @hoxyq Fixes https://github.com/facebook/react/issues/28584. Follow up to PR: https://github.com/facebook/react/pull/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](https://github.com/facebook/react/pull/34547#issuecomment-3504113776) 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>