Skip to content

Conversation

eps1lon
Copy link
Collaborator

@eps1lon eps1lon commented Jul 31, 2025

Stacked on #34082

We could create a separate Suspense store instead. But I found it easier to just reuse the existing operations for now.

@meta-cla meta-cla bot added the CLA Signed label Jul 31, 2025
@github-actions github-actions bot added the React Core Team Opened by a member of the React Core Team label Jul 31, 2025
// At least the root should have an associated SuspenseNode.
throw new Error('A SuspenseNode must exist containing this Fiber.');
}
recordResetSuspenseChildren(suspenseNode);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not safe to do this until you've popped back up to the same level as this suspenseNode since otherwise you could call this multiple times within the same parent and too early before all the children have settled into their new place.

One way to do this is to call it unconditionally every time fiberInstance.suspenseNode !== null here without the parent traversal above.

However, you'd have to call it even if shouldResetChildren is false. Otherwise you'd miss some cases where the change to the Instance tree is fully handled by some inner virtual or fiber instance. Since shouldResetChildren only accounts for changes to the DevToolsInstance tree.

It would be semantically correct to always call it. It would just be unnecessary slow since changes deep inside the tree would make the traversal and send the commands to front end every time. The shouldResetChildren flag only exists as an optimization.

We could return a bitmask that has two flags. shouldResetChildren and shouldResetSuspenseNodeChildren up the stack so that shouldResetSuspenseNodeChildren can bubble up the stack to the nearest SuspenseNode.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One way to do this is to call it unconditionally every time fiberInstance.suspenseNode !== null here without the parent traversal above.

I think we need do bubble up a flag for correctness as well. Right now, with the unconditional reset, it doesn't handle this reorder:

   <Suspense key="e" name="e">
      <Passthrough>
        <Suspense name="e-child-one">
          <p>e1</p>
        </Suspense>
      </Passthrough>
      <Passthrough>
        <Suspense name="e-child-two">
          <div>e2</div>
        </Suspense>
      </Passthrough>
    </Suspense>

to

   <Suspense key="e" name="e">
      <Passthrough>
        <Suspense name="e-child-two">
          <div>e2</div>
        </Suspense>
      </Passthrough>
      <Passthrough>
        <Suspense name="e-child-one">
          <p>e1</p>
        </Suspense>
      </Passthrough>
    </Suspense>

Copy link
Collaborator Author

@eps1lon eps1lon Aug 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented the additional flag. I haven't double checked yet if the "propagate it from everywhere" approach is correct or if I need to be more selected.

The current impl seems to reach parity with the Components tab.

@eps1lon eps1lon force-pushed the sebbie/suspenseops branch 5 times, most recently from 72926ca to 9a2ace4 Compare August 4, 2025 19:13
Comment on lines +3620 to +3888
updateFlags |=
ShouldResetChildren | ShouldResetSuspenseChildren;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mountVirtualInstanceRecursively can mount a Fiber. Was the flag previously not set on purpose or missing?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For previousVirtualInstanceWasMount to be true, we must have already set this below on 3941.

You do set ShouldResetSuspenseChildren here but you don't on 3941. It's not necessary to set in both places but they should be consistent.

You could maybe check if there was a SuspenseNode added to the previouslyReconciledSiblingSuspenseNode by checking if it changed before and after mountVirtualInstanceRecursively to know whether you need to set ShouldResetSuspenseChildren.

@eps1lon eps1lon force-pushed the sebbie/suspenseops branch from 4efe938 to c81b79d Compare August 5, 2025 18:15
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a mess at the moment for visual debugging of the store. A follow-up will make this a flat list that you can navigate through.

@eps1lon eps1lon force-pushed the sebbie/suspenseops branch from c81b79d to 6aff312 Compare August 5, 2025 18:34
@eps1lon eps1lon requested a review from sebmarkbage August 5, 2025 18:41
@eps1lon eps1lon marked this pull request as ready for review August 5, 2025 18:41
@eps1lon eps1lon force-pushed the sebbie/suspenseops branch 2 times, most recently from 1f88d92 to d10b52b Compare August 6, 2025 19:18
@eps1lon eps1lon force-pushed the sebbie/suspenseops branch from d10b52b to 28cf3ec Compare August 6, 2025 19:22
compiledWithForget: boolean,
};

export type Suspense = {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be an annoying name to work with since we'll have the name imported as <Suspense> in the same files this will be referenced.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I didn't use SuspenseNode to avoid confusing it with the backend type. Maybe that's a smaller issue than confusing it with React.Suspense?

): void {
const fiberInstance = suspenseInstance.instance;
if (fiberInstance.kind === FILTERED_FIBER_INSTANCE) {
throw new Error('Cannot record a mount for a filtered Fiber instance.');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't have to do it in this PR but as discussed, we probably should show filtered ones too. It's just that cross-linking to the Components tab would be disabled. That might have implications on how we load the side-panel too.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, just need to figure out a type pattern because filtered instances have their ID hidden by default.

let unfilteredParent = parentSuspenseInstance;
while (
unfilteredParent !== null &&
unfilteredParent.instance.kind === FILTERED_FIBER_INSTANCE
Copy link
Collaborator

@sebmarkbage sebmarkbage Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably include filtered in a follow up but what we should not include in the front end is boundaries without hasUniqueSuspenders. They're basically filtered from this tree.

That could potentially be done at the front end level but might be easier and smaller traffic if it's done by the backend just like filtered.

It would just have to reparent or unmount/remount when a previously false boundary becomes true.

Copy link
Collaborator

@sebmarkbage sebmarkbage left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some minor notes and follow ups.

@eps1lon eps1lon merged commit 98286cf into facebook:main Aug 10, 2025
241 checks passed
@eps1lon eps1lon deleted the sebbie/suspenseops branch August 10, 2025 08:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed React Core Team Opened by a member of the React Core Team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants