Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reuse hooks when replaying a suspended component #25620

Merged
merged 7 commits into from
Nov 17, 2022

Conversation

acdlite
Copy link
Collaborator

@acdlite acdlite commented Nov 3, 2022

Based on #25632

When a component suspends, under some conditions, we can wait for the data to resolve and replay the component without unwinding the stack or showing a fallback in the interim. When we do this, we reuse the promises that were unwrapped during the previous attempts, so that if they aren't memoized, the result can still be used.

We should do the same for all hooks. That way, if you do memoize an async function call with useMemo, it won't be called again during the replay. This effectively gives you a local version of the functionality provided by cache, using the normal memoization patterns that have long existed in React.

@facebook-github-bot facebook-github-bot added CLA Signed React Core Team Opened by a member of the React Core Team labels Nov 3, 2022
@sizebot
Copy link

sizebot commented Nov 3, 2022

Comparing: 44c4e6f...e0790b4

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.min.js +0.41% 153.75 kB 154.39 kB +0.38% 48.87 kB 49.05 kB
oss-experimental/react-dom/cjs/react-dom.production.min.js +0.41% 155.67 kB 156.31 kB +0.35% 49.49 kB 49.67 kB
facebook-www/ReactDOM-prod.classic.js +0.56% 531.04 kB 534.01 kB +0.36% 94.21 kB 94.56 kB
facebook-www/ReactDOM-prod.modern.js +0.55% 516.29 kB 519.11 kB +0.37% 92.02 kB 92.37 kB
facebook-www/ReactDOMForked-prod.classic.js +0.56% 531.04 kB 534.01 kB +0.36% 94.21 kB 94.56 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
facebook-react-native/react-test-renderer/cjs/ReactTestRenderer-prod.js +0.98% 287.54 kB 290.37 kB +0.89% 50.72 kB 51.17 kB
facebook-react-native/react-test-renderer/cjs/ReactTestRenderer-profiling.js +0.91% 303.15 kB 305.92 kB +0.89% 53.06 kB 53.54 kB
react-native/implementations/ReactNativeRenderer-prod.js +0.88% 321.00 kB 323.84 kB +0.75% 56.61 kB 57.04 kB
react-native/implementations/ReactFabric-prod.js +0.88% 314.85 kB 317.61 kB +0.73% 55.67 kB 56.08 kB
react-native/implementations/ReactFabric-prod.fb.js +0.87% 325.59 kB 328.43 kB +0.67% 57.81 kB 58.20 kB
facebook-www/ReactART-prod.classic.js +0.86% 334.07 kB 336.94 kB +0.65% 56.63 kB 56.99 kB
react-native/implementations/ReactNativeRenderer-prod.fb.js +0.85% 331.74 kB 334.58 kB +0.66% 58.79 kB 59.18 kB
react-native/implementations/ReactFabric-profiling.js +0.85% 334.06 kB 336.91 kB +0.74% 58.86 kB 59.29 kB
facebook-react-native/react-test-renderer/cjs/ReactTestRenderer-dev.js +0.84% 760.81 kB 767.22 kB +0.82% 163.00 kB 164.34 kB
facebook-www/ReactART-prod.modern.js +0.84% 323.28 kB 326.00 kB +0.68% 54.76 kB 55.13 kB
react-native/implementations/ReactNativeRenderer-profiling.js +0.84% 340.30 kB 343.14 kB +0.72% 59.84 kB 60.27 kB
oss-stable-semver/react-test-renderer/cjs/react-test-renderer.development.js +0.83% 746.57 kB 752.78 kB +0.79% 161.40 kB 162.67 kB
oss-stable/react-test-renderer/cjs/react-test-renderer.development.js +0.83% 746.59 kB 752.80 kB +0.79% 161.43 kB 162.70 kB
oss-experimental/react-test-renderer/cjs/react-test-renderer.development.js +0.83% 746.70 kB 752.90 kB +0.79% 161.46 kB 162.73 kB
react-native/implementations/ReactFabric-profiling.fb.js +0.83% 352.41 kB 355.33 kB +0.67% 61.91 kB 62.33 kB
oss-stable-semver/react-test-renderer/umd/react-test-renderer.development.js +0.83% 782.07 kB 788.55 kB +0.79% 163.10 kB 164.39 kB
oss-stable/react-test-renderer/umd/react-test-renderer.development.js +0.83% 782.09 kB 788.57 kB +0.79% 163.12 kB 164.41 kB
oss-experimental/react-test-renderer/umd/react-test-renderer.development.js +0.83% 782.19 kB 788.68 kB +0.79% 163.16 kB 164.45 kB
facebook-www/ReactTestRenderer-dev.modern.js +0.82% 777.21 kB 783.62 kB +0.84% 166.26 kB 167.66 kB
facebook-www/ReactTestRenderer-dev.classic.js +0.82% 777.21 kB 783.62 kB +0.84% 166.26 kB 167.65 kB
react-native/implementations/ReactNativeRenderer-profiling.fb.js +0.82% 358.62 kB 361.55 kB +0.65% 62.96 kB 63.37 kB
oss-stable-semver/react-art/cjs/react-art.development.js +0.79% 767.14 kB 773.24 kB +0.75% 165.50 kB 166.74 kB
oss-stable/react-art/cjs/react-art.development.js +0.79% 767.17 kB 773.26 kB +0.75% 165.53 kB 166.77 kB
oss-experimental/react-art/cjs/react-art.development.js +0.79% 774.97 kB 781.06 kB +0.73% 166.95 kB 168.17 kB
react-native/implementations/ReactFabric-dev.js +0.75% 839.43 kB 845.71 kB +0.75% 181.96 kB 183.31 kB
react-native/implementations/ReactNativeRenderer-dev.js +0.74% 848.97 kB 855.25 kB +0.74% 184.39 kB 185.76 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.development.js +0.74% 846.80 kB 853.05 kB +0.71% 179.81 kB 181.08 kB
oss-stable/react-reconciler/cjs/react-reconciler.development.js +0.74% 846.82 kB 853.08 kB +0.71% 179.83 kB 181.11 kB
oss-experimental/react-reconciler/cjs/react-reconciler.development.js +0.73% 854.63 kB 860.88 kB +0.70% 181.25 kB 182.52 kB
oss-stable-semver/react-art/umd/react-art.development.js +0.73% 874.40 kB 880.78 kB +0.71% 183.74 kB 185.05 kB
oss-stable/react-art/umd/react-art.development.js +0.73% 874.42 kB 880.80 kB +0.71% 183.76 kB 185.07 kB
facebook-www/ReactART-dev.classic.js +0.72% 878.33 kB 884.69 kB +0.71% 185.29 kB 186.61 kB
oss-experimental/react-art/umd/react-art.development.js +0.72% 882.67 kB 889.05 kB +0.73% 185.15 kB 186.51 kB
react-native/implementations/ReactFabric-dev.fb.js +0.72% 876.16 kB 882.46 kB +0.68% 189.13 kB 190.41 kB
facebook-www/ReactART-dev.modern.js +0.71% 868.02 kB 874.23 kB +0.71% 183.21 kB 184.52 kB
react-native/implementations/ReactNativeRenderer-dev.fb.js +0.71% 885.68 kB 891.98 kB +0.67% 191.61 kB 192.89 kB
oss-stable-semver/react-art/cjs/react-art.production.min.js +0.67% 91.70 kB 92.31 kB +0.61% 28.24 kB 28.42 kB
oss-stable/react-art/cjs/react-art.production.min.js +0.67% 91.72 kB 92.34 kB +0.61% 28.25 kB 28.42 kB
oss-experimental/react-art/cjs/react-art.production.min.js +0.66% 93.14 kB 93.75 kB +0.60% 28.65 kB 28.82 kB
oss-stable-semver/react-test-renderer/cjs/react-test-renderer.production.min.js +0.62% 99.52 kB 100.14 kB +0.58% 30.58 kB 30.76 kB
oss-stable/react-test-renderer/cjs/react-test-renderer.production.min.js +0.62% 99.55 kB 100.16 kB +0.57% 30.59 kB 30.76 kB
oss-experimental/react-test-renderer/cjs/react-test-renderer.production.min.js +0.62% 99.61 kB 100.22 kB +0.56% 30.62 kB 30.79 kB
oss-stable-semver/react-test-renderer/umd/react-test-renderer.production.min.js +0.62% 99.77 kB 100.39 kB +0.57% 30.99 kB 31.17 kB
oss-stable/react-test-renderer/umd/react-test-renderer.production.min.js +0.62% 99.80 kB 100.41 kB +0.57% 31.00 kB 31.17 kB
oss-experimental/react-test-renderer/umd/react-test-renderer.production.min.js +0.62% 99.86 kB 100.47 kB +0.57% 31.02 kB 31.20 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.production.min.js +0.60% 105.53 kB 106.16 kB +0.47% 32.20 kB 32.35 kB
oss-stable/react-reconciler/cjs/react-reconciler.production.min.js +0.60% 105.55 kB 106.19 kB +0.47% 32.22 kB 32.37 kB
facebook-www/ReactDOMTesting-prod.classic.js +0.60% 494.46 kB 497.41 kB +0.42% 90.34 kB 90.72 kB
oss-experimental/react-reconciler/cjs/react-reconciler.production.min.js +0.60% 106.97 kB 107.60 kB +0.53% 32.64 kB 32.81 kB
facebook-www/ReactDOMTesting-prod.modern.js +0.58% 478.66 kB 481.45 kB +0.41% 87.97 kB 88.34 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.profiling.min.js +0.56% 114.38 kB 115.03 kB +0.60% 34.34 kB 34.54 kB
oss-stable/react-reconciler/cjs/react-reconciler.profiling.min.js +0.56% 114.41 kB 115.05 kB +0.60% 34.36 kB 34.56 kB
facebook-www/ReactDOM-prod.classic.js +0.56% 531.04 kB 534.01 kB +0.36% 94.21 kB 94.56 kB
facebook-www/ReactDOMForked-prod.classic.js +0.56% 531.04 kB 534.01 kB +0.36% 94.21 kB 94.56 kB
facebook-www/ReactDOM-profiling.classic.js +0.56% 561.11 kB 564.24 kB +0.40% 98.67 kB 99.06 kB
facebook-www/ReactDOMForked-profiling.classic.js +0.56% 561.11 kB 564.24 kB +0.39% 98.67 kB 99.06 kB
oss-experimental/react-reconciler/cjs/react-reconciler.profiling.min.js +0.56% 115.82 kB 116.47 kB +0.55% 34.78 kB 34.97 kB
facebook-www/ReactDOM-prod.modern.js +0.55% 516.29 kB 519.11 kB +0.37% 92.02 kB 92.37 kB
facebook-www/ReactDOMForked-prod.modern.js +0.55% 516.29 kB 519.11 kB +0.37% 92.02 kB 92.37 kB
facebook-www/ReactDOM-profiling.modern.js +0.54% 546.28 kB 549.25 kB +0.39% 96.49 kB 96.86 kB
facebook-www/ReactDOMForked-profiling.modern.js +0.54% 546.28 kB 549.25 kB +0.39% 96.49 kB 96.86 kB
oss-experimental/react-dom/cjs/react-dom-unstable_testing.development.js +0.54% 1,199.67 kB 1,206.14 kB +0.50% 265.97 kB 267.29 kB
facebook-www/ReactDOMTesting-dev.modern.js +0.53% 1,200.36 kB 1,206.69 kB +0.48% 265.69 kB 266.96 kB
facebook-www/ReactDOMTesting-dev.classic.js +0.53% 1,230.24 kB 1,236.72 kB +0.48% 271.44 kB 272.74 kB
oss-stable-semver/react-dom/cjs/react-dom.development.js +0.52% 1,196.69 kB 1,202.95 kB +0.47% 265.47 kB 266.73 kB
oss-stable/react-dom/cjs/react-dom.development.js +0.52% 1,196.72 kB 1,202.97 kB +0.47% 265.49 kB 266.75 kB
oss-stable-semver/react-dom/umd/react-dom.development.js +0.52% 1,254.84 kB 1,261.39 kB +0.51% 268.35 kB 269.71 kB
oss-stable/react-dom/umd/react-dom.development.js +0.52% 1,254.87 kB 1,261.41 kB +0.51% 268.37 kB 269.73 kB
oss-experimental/react-dom/cjs/react-dom.development.js +0.52% 1,206.77 kB 1,213.02 kB +0.47% 267.46 kB 268.73 kB
oss-experimental/react-dom/umd/react-dom.development.js +0.52% 1,265.47 kB 1,272.01 kB +0.50% 270.31 kB 271.67 kB
oss-stable-semver/react-art/umd/react-art.production.min.js +0.48% 127.53 kB 128.15 kB +0.54% 39.42 kB 39.63 kB
oss-stable/react-art/umd/react-art.production.min.js +0.48% 127.56 kB 128.17 kB +0.53% 39.42 kB 39.63 kB
facebook-www/ReactDOMForked-dev.classic.js +0.48% 1,351.31 kB 1,357.83 kB +0.45% 293.06 kB 294.39 kB
facebook-www/ReactDOM-dev.classic.js +0.48% 1,351.31 kB 1,357.83 kB +0.45% 293.06 kB 294.39 kB
facebook-www/ReactDOMForked-dev.modern.js +0.48% 1,326.47 kB 1,332.83 kB +0.46% 288.45 kB 289.78 kB
facebook-www/ReactDOM-dev.modern.js +0.48% 1,326.47 kB 1,332.83 kB +0.46% 288.45 kB 289.79 kB
oss-experimental/react-art/umd/react-art.production.min.js +0.48% 128.97 kB 129.59 kB +0.44% 39.92 kB 40.10 kB
oss-stable-semver/react-dom/cjs/react-dom.production.min.js +0.42% 153.73 kB 154.36 kB +0.38% 48.87 kB 49.05 kB
oss-stable/react-dom/cjs/react-dom.production.min.js +0.41% 153.75 kB 154.39 kB +0.38% 48.87 kB 49.05 kB
oss-stable-semver/react-dom/umd/react-dom.production.min.js +0.41% 153.75 kB 154.39 kB +0.37% 49.59 kB 49.78 kB
oss-stable/react-dom/umd/react-dom.production.min.js +0.41% 153.77 kB 154.41 kB +0.37% 49.59 kB 49.78 kB
oss-experimental/react-dom/cjs/react-dom.production.min.js +0.41% 155.67 kB 156.31 kB +0.35% 49.49 kB 49.67 kB
oss-experimental/react-dom/umd/react-dom.production.min.js +0.41% 155.68 kB 156.32 kB +0.36% 50.23 kB 50.41 kB
oss-experimental/react-dom/cjs/react-dom-unstable_testing.production.min.js +0.40% 160.07 kB 160.71 kB +0.31% 51.31 kB 51.47 kB
oss-stable-semver/react-dom/umd/react-dom.profiling.min.js +0.40% 162.56 kB 163.21 kB +0.30% 51.84 kB 52.00 kB
oss-stable/react-dom/umd/react-dom.profiling.min.js +0.40% 162.59 kB 163.23 kB +0.30% 51.84 kB 52.00 kB
oss-stable-semver/react-dom/cjs/react-dom.profiling.min.js +0.40% 163.20 kB 163.85 kB +0.40% 51.36 kB 51.57 kB
oss-stable/react-dom/cjs/react-dom.profiling.min.js +0.40% 163.23 kB 163.87 kB +0.40% 51.36 kB 51.57 kB
oss-experimental/react-dom/umd/react-dom.profiling.min.js +0.39% 164.48 kB 165.12 kB +0.45% 52.52 kB 52.75 kB
oss-experimental/react-dom/cjs/react-dom.profiling.min.js +0.39% 165.15 kB 165.80 kB +0.38% 51.98 kB 52.17 kB

Generated by 🚫 dangerJS against cc16827

@acdlite acdlite force-pushed the use-memo-async-function branch 2 times, most recently from eab57ff to 513f382 Compare November 3, 2022 03:08
@acdlite acdlite marked this pull request as ready for review November 3, 2022 03:11
case IndeterminateComponent: {
// Because it suspended with `use`, we can assume it's a
// function component.
unitOfWork.tag = FunctionComponent;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I thought this was a safe assumption but after thinking about it more, we probably need to add the new suspended work loop behavior to regular "throw a promise" Suspense, too, until that's fully deprecated. As of now, if you were to mix the two styles, you could fall into a loop because a component that throws a promise will cause a component that uses use to unwind and lose its resolved state.

And if that's the case, then we need to account for class components, because those are allowed to throw a promise.

There's also the case of suspending during reconciliation, though I think that will be special anyhow since we don't want to replay the parent component.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Nvm, I thought about this more and I don't think the loop I described in the previous comment can happen.

@acdlite acdlite force-pushed the use-memo-async-function branch 2 times, most recently from 38c520b to 1c59fbf Compare November 4, 2022 04:33
Before suspending, check if there are other pending updates that might
possibly unblock the suspended component. If so, interrupt the current
render and switch to working on that.

This logic was already implemented for the old "throw a Promise"
Suspense but has to be replicated for `use` because it suspends the
work loop much earlier.

I'm getting a little anxious about the divergence between the two
Suspense patterns. I'm going to look into enabling the new behavior for
the old pattern so that we can unify the implementations.
When replaying a suspended function components, we want to reuse the
hooks that were computed during the original render.

Currently we reset the state of the hooks right after the component 
suspends (or throws an error). This is too early because it doesn't 
give us an opportunity to wait for the promise to resolve.

This refactors the work loop to reset the hooks right before unwinding
instead of right after throwing. It doesn't include any other changes
yet, so there should be no observable behavioral change.
Tiny refactor to refine the work loop variable so Flow knows it's not
null when we access it in replaySuspendedUnitOfWork.
Currently, if you call setState in render, you must render the exact
same hooks as during the first render pass.

I'm about to add a behavior where if something suspends, we can reuse
the hooks from the previous attempt. That means during initial render,
if something suspends, we should be able to reuse the hooks that were
already created and continue adding more after that. This will error
in the current implementation because of the expectation that every
render produces the same list of hooks.

In this commit, I've changed the logic to allow more hooks to be added
when replaying. But only during a mount — if there's already a current
fiber, then the logic is unchanged, because we shouldn't add any
additional hooks that aren't in the current fiber's list. Mounts are
special because there's no current fiber to compare to.

I haven't change any other behavior yet. The reason I've put this into
its own step is there are a couple tests that intentionally break the
Hook rule, to assert that React errors in these cases, and those happen
to be coupled to the behavior. This is undefined behavior that is always
accompanied by a warning and/or error. So the change should be safe.
When a component suspends, under some conditions, we can wait for the
data to resolve and replay the component without unwinding the stack or
showing a fallback in the interim. When we do this, we reuse the
promises that were unwrapped during the previous attempts, so that if
they aren't memoized, the result can still be used.

We should do the same for all hooks. That way, if you _do_ memoize an
async function call with useMemo, it won't be called again during the
replay. This effectively gives you a local version of the functionality
provided by `cache`, using the normal memoization patterns that have
long existed in React.
Now that hook state is preserved while the work loop is suspended, we
don't need to track the thenable state in the work loop. We can track
it alongside the rest of the hook state.

Before deleting the thenable state variable from the work loop, I need
to remove the other places where it's referenced.

One of them is `isThenableStateResolved`. This grabs the last thenable
from the array and checks if it has resolved.

This was a pointless indirection anyway. The thenable is already stored
as `workInProgressThrownValue`. So we can check that directly.
Now that hook state is preserved while the work loop is suspended, we
don't need to track the thenable state in the work loop. We can track
it alongside the rest of the hook state.

This is a nice simplification and also aligns better with how it works
in Fizz and Flight.

The promises will still be cleared when the component finishes rendering
(either complete or unwind). In the future, we could stash the promises
on the fiber and reuse them during an update. However, this would only
work for `use` calls that occur before an prop/state/context is
processed, because `use` calls can only be assumed to execute in the
same order if no other props/state/context have changed. So it might not
be worth doing until we have finer grained memoization.
@acdlite acdlite closed this Nov 17, 2022
@acdlite acdlite reopened this Nov 17, 2022
@acdlite acdlite merged commit f284d9f into facebook:main Nov 17, 2022
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.

4 participants