Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: facebook/react
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 4a3d993e
Choose a base ref
...
head repository: facebook/react
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: bef88f7c
Choose a head ref
  • 12 commits
  • 34 files changed
  • 5 contributors

Commits on Jan 15, 2026

  1. 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>
    blazejkustra and hoxyq authored Jan 15, 2026
    Configuration menu
    Copy the full SHA
    53daaf5 View commit details
    Browse the repository at this point in the history
  2. Configuration menu
    Copy the full SHA
    fae15df View commit details
    Browse the repository at this point in the history
  3. Configuration menu
    Copy the full SHA
    bb8a76c View commit details
    Browse the repository at this point in the history

Commits on Jan 16, 2026

  1. [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.
    gnoff authored Jan 16, 2026
    Configuration menu
    Copy the full SHA
    f0fbb0d View commit details
    Browse the repository at this point in the history
  2. 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.~
    sebmarkbage authored Jan 16, 2026
    Configuration menu
    Copy the full SHA
    4028aaa View commit details
    Browse the repository at this point in the history
  3. 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.
    sebmarkbage authored Jan 16, 2026
    Configuration menu
    Copy the full SHA
    35a81ce View commit details
    Browse the repository at this point in the history
  4. 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.
    sebmarkbage authored Jan 16, 2026
    Configuration menu
    Copy the full SHA
    eac3c95 View commit details
    Browse the repository at this point in the history
  5. 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.)
    sebmarkbage authored Jan 16, 2026
    Configuration menu
    Copy the full SHA
    4cf9063 View commit details
    Browse the repository at this point in the history
  6. [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
    gnoff authored Jan 16, 2026
    Configuration menu
    Copy the full SHA
    db71391 View commit details
    Browse the repository at this point in the history
  7. Configuration menu
    Copy the full SHA
    cbc4d40 View commit details
    Browse the repository at this point in the history
  8. Configuration menu
    Copy the full SHA
    01c4d03 View commit details
    Browse the repository at this point in the history
  9. Configuration menu
    Copy the full SHA
    bef88f7 View commit details
    Browse the repository at this point in the history
Loading