Skip to content

Commit

Permalink
Bugfix: Effect clean up when deleting suspended tree (#19752)
Browse files Browse the repository at this point in the history
* Bug: Effect clean up when deleting suspended tree

Adds a failing unit test.

* Re-use static flags from suspended primary tree

When switching to a Suspense boundary's fallback, we need to be sure
to preserve static subtree flags from the primary tree.
  • Loading branch information
acdlite committed Sep 4, 2020
1 parent 7baf9d4 commit 4f3f7ee
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 0 deletions.
7 changes: 7 additions & 0 deletions packages/react-reconciler/src/ReactFiberBeginWork.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import {
Ref,
Deletion,
ForceUpdateForLegacySuspense,
StaticMask,
} from './ReactFiberFlags';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import {
Expand Down Expand Up @@ -2131,6 +2132,12 @@ function updateSuspenseFallbackChildren(
currentPrimaryChildFragment,
primaryChildProps,
);

// Since we're reusing a current tree, we need to reuse the flags, too.
// (We don't do this in legacy mode, because in legacy mode we don't re-use
// the current tree; see previous branch.)
primaryChildFragment.subtreeFlags =
currentPrimaryChildFragment.subtreeFlags & StaticMask;
}
let fallbackChildFragment;
if (currentFallbackChildFragment !== null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3956,4 +3956,101 @@ describe('ReactSuspenseWithNoopRenderer', () => {
</>,
);
});

it('should fire effect clean-up when deleting suspended tree', async () => {
const {useEffect} = React;

function App({show}) {
return (
<Suspense fallback={<Text text="Loading..." />}>
<Child />
{show && <AsyncText text="Async" />}
</Suspense>
);
}

function Child() {
useEffect(() => {
Scheduler.unstable_yieldValue('Mount Child');
return () => {
Scheduler.unstable_yieldValue('Unmount Child');
};
}, []);
return <span prop="Child" />;
}

const root = ReactNoop.createRoot();

await ReactNoop.act(async () => {
root.render(<App show={false} />);
});
expect(Scheduler).toHaveYielded(['Mount Child']);
expect(root).toMatchRenderedOutput(<span prop="Child" />);

await ReactNoop.act(async () => {
root.render(<App show={true} />);
});
// TODO: `act` should have already flushed the placeholder, so this
// runAllTimers call should be unnecessary.
jest.runAllTimers();
expect(Scheduler).toHaveYielded(['Suspend! [Async]', 'Loading...']);
expect(root).toMatchRenderedOutput(
<>
<span hidden={true} prop="Child" />
<span prop="Loading..." />
</>,
);

await ReactNoop.act(async () => {
root.render(null);
});
expect(Scheduler).toHaveYielded(['Unmount Child']);
});

it('should fire effect clean-up when deleting suspended tree (legacy)', async () => {
const {useEffect} = React;

function App({show}) {
return (
<Suspense fallback={<Text text="Loading..." />}>
<Child />
{show && <AsyncText text="Async" />}
</Suspense>
);
}

function Child() {
useEffect(() => {
Scheduler.unstable_yieldValue('Mount Child');
return () => {
Scheduler.unstable_yieldValue('Unmount Child');
};
}, []);
return <span prop="Child" />;
}

const root = ReactNoop.createLegacyRoot();

await ReactNoop.act(async () => {
root.render(<App show={false} />);
});
expect(Scheduler).toHaveYielded(['Mount Child']);
expect(root).toMatchRenderedOutput(<span prop="Child" />);

await ReactNoop.act(async () => {
root.render(<App show={true} />);
});
expect(Scheduler).toHaveYielded(['Suspend! [Async]', 'Loading...']);
expect(root).toMatchRenderedOutput(
<>
<span hidden={true} prop="Child" />
<span prop="Loading..." />
</>,
);

await ReactNoop.act(async () => {
root.render(null);
});
expect(Scheduler).toHaveYielded(['Unmount Child']);
});
});

0 comments on commit 4f3f7ee

Please sign in to comment.