Skip to content

Commit

Permalink
Add regression test for stuck pending state
Browse files Browse the repository at this point in the history
Introduces a regression test for a bug where the pending state of a
useTransition hook is not set back to `false` if it comes after a
`use` that suspended.

Co-authored-by: Andrew Clark <git@andrewclark.io>
  • Loading branch information
eps1lon and acdlite committed May 31, 2024
1 parent ec6fe57 commit c13ea86
Showing 1 changed file with 80 additions and 0 deletions.
80 changes: 80 additions & 0 deletions packages/react-reconciler/src/__tests__/ReactUse-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ let act;
let use;
let useDebugValue;
let useState;
let useTransition;
let useMemo;
let useEffect;
let useOptimistic;
let Suspense;
let startTransition;
let pendingTextRequests;
Expand All @@ -38,8 +40,10 @@ describe('ReactUse', () => {
use = React.use;
useDebugValue = React.useDebugValue;
useState = React.useState;
useTransition = React.useTransition;
useMemo = React.useMemo;
useEffect = React.useEffect;
useOptimistic = React.useOptimistic;
Suspense = React.Suspense;
startTransition = React.startTransition;

Expand Down Expand Up @@ -1915,4 +1919,80 @@ describe('ReactUse', () => {
assertLog(['Hi', 'World']);
expect(root).toMatchRenderedOutput(<div>Hi World</div>);
});

it(
'regression: does not get stuck in pending state after `use` suspends ' +
'(when `use` comes before all hooks)',
async () => {
// This is a regression test. The root cause was an issue where we failed to
// switch from the "re-render" dispatcher back to the "update" dispatcher
// after a `use` suspends and triggers a replay.
let update;
function App({promise}) {
const value = use(promise);

const [isPending, startLocalTransition] = useTransition();
update = () => {
startLocalTransition(() => {
root.render(<App promise={getAsyncText('Updated')} />);
});
};

return <Text text={value + (isPending ? ' (pending...)' : '')} />;
}

const root = ReactNoop.createRoot();
await act(() => {
root.render(<App promise={Promise.resolve('Initial')} />);
});
assertLog(['Initial']);
expect(root).toMatchRenderedOutput('Initial');

await act(() => update());
assertLog(['Async text requested [Updated]', 'Initial (pending...)']);

await act(() => resolveTextRequests('Updated'));
assertLog(['Updated']);
expect(root).toMatchRenderedOutput('Updated');
},
);

it(
'regression: does not get stuck in pending state after `use` suspends ' +
'(when `use` in in the middle of hook list)',
async () => {
// Same as previous test but `use` comes in between two hooks.
let update;
function App({promise}) {
// This hook is only here to test that `use` resumes correctly after
// suspended even if it comes in between other hooks.
useState(false);

const value = use(promise);

const [isPending, startLocalTransition] = useTransition();
update = () => {
startLocalTransition(() => {
root.render(<App promise={getAsyncText('Updated')} />);
});
};

return <Text text={value + (isPending ? ' (pending...)' : '')} />;
}

const root = ReactNoop.createRoot();
await act(() => {
root.render(<App promise={Promise.resolve('Initial')} />);
});
assertLog(['Initial']);
expect(root).toMatchRenderedOutput('Initial');

await act(() => update());
assertLog(['Async text requested [Updated]', 'Initial (pending...)']);

await act(() => resolveTextRequests('Updated'));
assertLog(['Updated']);
expect(root).toMatchRenderedOutput('Updated');
},
);
});

0 comments on commit c13ea86

Please sign in to comment.