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

Flush all passive destroy fns before calling create fns #17947

Merged
merged 2 commits into from
Feb 11, 2020

Conversation

bvaughn
Copy link
Contributor

@bvaughn bvaughn commented Jan 31, 2020

Previously we only flushed destroy functions for a single fiber.

The reason this is important is that interleaving destroy/create effects between sibling components might cause components to interfere with each other (e.g. a destroy function in one component may unintentionally override a ref value set by a create function in another component).

This PR builds on top of the recently added deferPassiveEffectCleanupDuringUnmount kill switch to separate passive effects flushing into two separate phases (similar to layout effects). It also carries forward a behavior of not running mount functions for a Fiber that's had an unmount function error. Open to discussion on whether this behavior is something we want to carry forward.

The deferPassiveEffectCleanupDuringUnmount flag still remains off in master. We'll want to flip it on for Facebook during an upcoming sync.

Resolves issue #17945. Builds on top of PR #17925.

@facebook-github-bot facebook-github-bot added CLA Signed React Core Team Opened by a member of the React Core Team labels Jan 31, 2020
effect.nextEffect = null;
effect = nextNextEffect;
}
} else {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the old code path. If we flip the deferPassiveEffectCleanupDuringUnmount killswitch off, it will restore the previous behavior.

@codesandbox-ci
Copy link

codesandbox-ci bot commented Jan 31, 2020

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 9ca1b2d:

Sandbox Source
bitter-snow-0q7b2 Configuration

@sizebot
Copy link

sizebot commented Jan 31, 2020

Details of bundled changes.

Comparing: 529e58a...9ca1b2d

react-art

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-art.development.js +0.5% +0.4% 608.88 KB 611.85 KB 128.58 KB 129.09 KB NODE_DEV
react-art.production.min.js 0.0% -0.0% 69.98 KB 69.98 KB 20.99 KB 20.99 KB NODE_PROD
react-art.development.js +0.4% +0.3% 678.21 KB 681.17 KB 146 KB 146.51 KB UMD_DEV
react-art.production.min.js 0.0% -0.0% 104.98 KB 104.98 KB 31.84 KB 31.84 KB UMD_PROD

react-native-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactNativeRenderer-dev.js +0.4% +0.3% 750.23 KB 753.13 KB 157.8 KB 158.3 KB RN_OSS_DEV
ReactFabric-profiling.js 0.0% 0.0% 278.23 KB 278.25 KB 47.87 KB 47.87 KB RN_OSS_PROFILING
ReactFabric-dev.js +0.4% +0.3% 740.85 KB 743.74 KB 155.6 KB 156.1 KB RN_OSS_DEV
ReactNativeRenderer-profiling.js 0.0% 0.0% 285.96 KB 285.98 KB 49.21 KB 49.21 KB RN_OSS_PROFILING

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom-test-utils.development.js 0.0% -0.0% 53.65 KB 53.65 KB 15.27 KB 15.27 KB NODE_DEV
react-dom.production.min.js 0.0% -0.0% 116.08 KB 116.08 KB 37.27 KB 37.27 KB UMD_PROD
react-dom-testing.development.js +0.3% +0.2% 957.45 KB 960.41 KB 214.51 KB 215.03 KB NODE_DEV
react-dom.profiling.min.js 0.0% -0.0% 119.61 KB 119.61 KB 38.45 KB 38.44 KB UMD_PROFILING
react-dom-testing.production.min.js 0.0% -0.0% 117.22 KB 117.22 KB 37.09 KB 37.09 KB NODE_PROD
react-dom.development.js +0.3% +0.2% 960.03 KB 962.99 KB 215.51 KB 216.02 KB NODE_DEV
react-dom-testing.profiling.min.js 0.0% -0.0% 120.9 KB 120.9 KB 38.2 KB 38.2 KB NODE_PROFILING
react-dom.production.min.js 0.0% -0.0% 116.15 KB 116.15 KB 36.61 KB 36.61 KB NODE_PROD
react-dom-unstable-fizz.browser.production.min.js 0.0% -0.1% 1.2 KB 1.2 KB 703 B 702 B UMD_PROD
react-dom.profiling.min.js 0.0% -0.0% 119.82 KB 119.82 KB 37.74 KB 37.74 KB NODE_PROFILING
react-dom-server.browser.development.js 0.0% -0.0% 139.3 KB 139.3 KB 36.94 KB 36.94 KB UMD_DEV
react-dom-server.browser.production.min.js 0.0% -0.0% 20 KB 20 KB 7.4 KB 7.4 KB UMD_PROD
react-dom-unstable-fizz.browser.production.min.js 0.0% -0.2% 1.04 KB 1.04 KB 634 B 633 B NODE_PROD
react-dom-server.browser.development.js 0.0% -0.0% 135.23 KB 135.23 KB 35.91 KB 35.91 KB NODE_DEV
react-dom-server.browser.production.min.js 0.0% -0.0% 19.93 KB 19.93 KB 7.39 KB 7.39 KB NODE_PROD
react-dom-test-utils.production.min.js 0.0% -0.1% 10.97 KB 10.97 KB 4.09 KB 4.09 KB NODE_PROD
react-dom-unstable-native-dependencies.production.min.js 0.0% -0.0% 10.24 KB 10.24 KB 3.46 KB 3.46 KB UMD_PROD
react-dom-server.node.development.js 0.0% -0.0% 136.34 KB 136.34 KB 36.14 KB 36.14 KB NODE_DEV
react-dom-testing.development.js +0.3% +0.2% 963.36 KB 966.33 KB 216.2 KB 216.72 KB UMD_DEV
react-dom-server.node.production.min.js 0.0% -0.0% 20.34 KB 20.34 KB 7.54 KB 7.54 KB NODE_PROD
react-dom-testing.production.min.js 0.0% -0.0% 117.12 KB 117.12 KB 37.75 KB 37.75 KB UMD_PROD
react-dom-unstable-native-dependencies.development.js 0.0% -0.0% 60.62 KB 60.62 KB 15.99 KB 15.99 KB NODE_DEV
react-dom.development.js +0.3% +0.2% 965.94 KB 968.91 KB 217.17 KB 217.69 KB UMD_DEV
react-dom-testing.profiling.min.js 0.0% -0.0% 120.64 KB 120.64 KB 38.91 KB 38.91 KB UMD_PROFILING
react-dom-unstable-native-dependencies.production.min.js 0.0% -0.0% 9.98 KB 9.98 KB 3.37 KB 3.37 KB NODE_PROD

react-reconciler

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-reconciler-reflection.development.js 0.0% 0.0% 19.93 KB 19.93 KB 6.56 KB 6.56 KB NODE_DEV
react-reconciler-persistent.development.js +0.5% +0.4% 611.79 KB 614.75 KB 127.82 KB 128.34 KB NODE_DEV
react-reconciler-persistent.production.min.js 0.0% -0.0% 73.75 KB 73.75 KB 21.78 KB 21.78 KB NODE_PROD
react-reconciler.development.js +0.5% +0.4% 615.01 KB 617.97 KB 129.16 KB 129.68 KB NODE_DEV
react-reconciler.production.min.js 0.0% -0.0% 73.73 KB 73.73 KB 21.77 KB 21.77 KB NODE_PROD

react-test-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-test-renderer-shallow.development.js 0.0% -0.0% 37.88 KB 37.88 KB 9.83 KB 9.83 KB UMD_DEV
react-test-renderer-shallow.production.min.js 0.0% -0.0% 11.64 KB 11.64 KB 3.59 KB 3.59 KB UMD_PROD
react-test-renderer-shallow.development.js 0.0% -0.0% 32.42 KB 32.42 KB 8.52 KB 8.52 KB NODE_DEV
react-test-renderer-shallow.production.min.js 0.0% 🔺+0.1% 11.78 KB 11.78 KB 3.7 KB 3.7 KB NODE_PROD
react-test-renderer.development.js +0.5% +0.4% 622.58 KB 625.54 KB 131.34 KB 131.85 KB UMD_DEV
react-test-renderer.production.min.js 0.0% -0.0% 72.13 KB 72.13 KB 21.94 KB 21.94 KB UMD_PROD
react-test-renderer.development.js +0.5% +0.4% 617.85 KB 620.81 KB 130.15 KB 130.66 KB NODE_DEV
react-test-renderer.production.min.js 0.0% -0.0% 71.91 KB 71.91 KB 21.61 KB 21.61 KB NODE_PROD

ReactDOM: size: 0.0%, gzip: -0.0%

Size changes (stable)

Generated by 🚫 dangerJS against 9ca1b2d

@sizebot
Copy link

sizebot commented Jan 31, 2020

Details of bundled changes.

Comparing: 529e58a...9ca1b2d

react-native-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactFabric-dev.js +0.4% +0.3% 740.86 KB 743.75 KB 155.61 KB 156.11 KB RN_OSS_DEV
ReactNativeRenderer-dev.js +0.4% +0.3% 750.25 KB 753.14 KB 157.81 KB 158.31 KB RN_OSS_DEV
ReactFabric-dev.js +0.4% +0.3% 741.04 KB 743.94 KB 155.69 KB 156.19 KB RN_FB_DEV
ReactNativeRenderer-profiling.js 0.0% 0.0% 285.98 KB 285.99 KB 49.22 KB 49.22 KB RN_OSS_PROFILING
ReactFabric-profiling.js 0.0% 0.0% 278.59 KB 278.6 KB 47.94 KB 47.95 KB RN_FB_PROFILING
ReactNativeRenderer-dev.js +0.4% +0.3% 750.42 KB 753.31 KB 157.9 KB 158.41 KB RN_FB_DEV
ReactFabric-profiling.js 0.0% 0.0% 278.24 KB 278.26 KB 47.88 KB 47.88 KB RN_OSS_PROFILING
ReactNativeRenderer-profiling.js 0.0% 0.0% 286.36 KB 286.38 KB 49.28 KB 49.29 KB RN_FB_PROFILING

react-test-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-test-renderer.development.js +0.5% +0.4% 622.61 KB 625.57 KB 131.36 KB 131.86 KB UMD_DEV
react-test-renderer.production.min.js 0.0% -0.0% 72.15 KB 72.15 KB 21.96 KB 21.95 KB UMD_PROD
react-test-renderer.development.js +0.5% +0.4% 617.87 KB 620.83 KB 130.17 KB 130.67 KB NODE_DEV
react-test-renderer.production.min.js 0.0% -0.0% 71.93 KB 71.93 KB 21.63 KB 21.63 KB NODE_PROD
ReactTestRenderer-dev.js +0.5% +0.4% 633.31 KB 636.27 KB 130.96 KB 131.48 KB FB_WWW_DEV
react-test-renderer-shallow.development.js 0.0% -0.0% 37.89 KB 37.89 KB 9.84 KB 9.84 KB UMD_DEV
react-test-renderer-shallow.production.min.js 0.0% -0.0% 11.66 KB 11.66 KB 3.6 KB 3.6 KB UMD_PROD
react-test-renderer-shallow.production.min.js 0.0% 🔺+0.1% 11.79 KB 11.79 KB 3.71 KB 3.71 KB NODE_PROD

react-reconciler

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-reconciler-persistent.development.js +0.5% +0.4% 611.8 KB 614.76 KB 127.83 KB 128.34 KB NODE_DEV
react-reconciler-reflection.production.min.js 0.0% -0.1% 2.86 KB 2.86 KB 1.24 KB 1.24 KB NODE_PROD
react-reconciler.development.js +0.5% +0.4% 615.02 KB 617.98 KB 129.16 KB 129.68 KB NODE_DEV
react-reconciler.production.min.js 0.0% -0.0% 76.51 KB 76.51 KB 22.44 KB 22.44 KB NODE_PROD

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom.profiling.min.js 0.0% -0.0% 123.87 KB 123.87 KB 38.77 KB 38.77 KB NODE_PROFILING
ReactDOMServer-prod.js 0.0% -0.0% 48.99 KB 48.99 KB 11.18 KB 11.18 KB FB_WWW_PROD
react-dom-unstable-fizz.browser.development.js 0.0% -0.1% 3.88 KB 3.88 KB 1.55 KB 1.55 KB UMD_DEV
react-dom-unstable-fizz.browser.production.min.js 0.0% -0.1% 1.21 KB 1.21 KB 711 B 710 B UMD_PROD
react-dom-test-utils.development.js 0.0% -0.0% 55.39 KB 55.39 KB 15.59 KB 15.59 KB UMD_DEV
react-dom-testing.profiling.min.js 0.0% -0.0% 121.42 KB 121.42 KB 38.35 KB 38.35 KB NODE_PROFILING
ReactDOMTesting-prod.js 0.0% -0.0% 341.24 KB 341.24 KB 62.28 KB 62.28 KB FB_WWW_PROD
react-dom-server.browser.development.js 0.0% -0.0% 139.32 KB 139.32 KB 36.94 KB 36.94 KB UMD_DEV
react-dom-test-utils.production.min.js 0.0% -0.0% 11.21 KB 11.21 KB 4.17 KB 4.17 KB UMD_PROD
ReactDOMTesting-profiling.js 0.0% 0.0% 352.03 KB 352.04 KB 64.22 KB 64.23 KB FB_WWW_PROFILING
react-dom-server.browser.development.js 0.0% -0.0% 135.26 KB 135.26 KB 35.92 KB 35.91 KB NODE_DEV
react-dom-test-utils.production.min.js 0.0% -0.1% 10.99 KB 10.99 KB 4.1 KB 4.1 KB NODE_PROD
react-dom-server.browser.production.min.js 0.0% -0.0% 20.39 KB 20.39 KB 7.47 KB 7.47 KB NODE_PROD
react-dom.development.js +0.3% +0.2% 965.97 KB 968.93 KB 217.19 KB 217.71 KB UMD_DEV
react-dom.production.min.js 0.0% -0.0% 120 KB 120 KB 38.39 KB 38.39 KB UMD_PROD
react-dom-unstable-native-dependencies.production.min.js 0.0% -0.0% 10.25 KB 10.25 KB 3.47 KB 3.47 KB UMD_PROD
react-dom.profiling.min.js 0.0% -0.0% 123.63 KB 123.63 KB 39.58 KB 39.58 KB UMD_PROFILING
react-dom.development.js +0.3% +0.2% 960.05 KB 963.01 KB 215.52 KB 216.04 KB NODE_DEV
react-dom-unstable-native-dependencies.development.js 0.0% -0.0% 60.63 KB 60.63 KB 16 KB 16 KB NODE_DEV
react-dom.production.min.js 0.0% -0.0% 120.09 KB 120.09 KB 37.65 KB 37.65 KB NODE_PROD
react-dom-unstable-native-dependencies.production.min.js 0.0% -0.0% 9.99 KB 9.99 KB 3.38 KB 3.38 KB NODE_PROD
ReactDOMServer-dev.js 0.0% -0.0% 140.41 KB 140.41 KB 35.54 KB 35.54 KB FB_WWW_DEV
react-dom-unstable-fizz.node.production.min.js 0.0% -0.1% 1.21 KB 1.21 KB 698 B 697 B NODE_PROD
ReactDOM-dev.js +0.3% +0.2% 987.4 KB 990.36 KB 218.44 KB 218.95 KB FB_WWW_DEV
react-dom-unstable-fizz.browser.production.min.js 0.0% -0.2% 1.05 KB 1.05 KB 642 B 641 B NODE_PROD
ReactDOM-prod.js 🔺+0.5% 🔺+0.4% 393.72 KB 395.74 KB 71.97 KB 72.29 KB FB_WWW_PROD
react-dom-testing.development.js +0.3% +0.2% 963.39 KB 966.35 KB 216.21 KB 216.73 KB UMD_DEV
ReactDOM-profiling.js +0.5% +0.4% 404.98 KB 407.08 KB 74.15 KB 74.44 KB FB_WWW_PROFILING
react-dom-testing.production.min.js 0.0% -0.0% 117.65 KB 117.65 KB 37.91 KB 37.9 KB UMD_PROD
react-dom-testing.profiling.min.js 0.0% -0.0% 121.17 KB 121.17 KB 39.04 KB 39.04 KB UMD_PROFILING
react-dom-testing.development.js +0.3% +0.2% 957.47 KB 960.43 KB 214.52 KB 215.04 KB NODE_DEV
react-dom-server.node.development.js 0.0% -0.0% 136.37 KB 136.37 KB 36.14 KB 36.14 KB NODE_DEV
react-dom-testing.production.min.js 0.0% -0.0% 117.75 KB 117.75 KB 37.24 KB 37.23 KB NODE_PROD
ReactDOMTesting-dev.js +0.3% +0.2% 986.07 KB 989.03 KB 217.59 KB 218.09 KB FB_WWW_DEV

react-art

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-art.development.js +0.4% +0.3% 678.24 KB 681.2 KB 146.01 KB 146.52 KB UMD_DEV
react-art.production.min.js 0.0% -0.0% 107.54 KB 107.54 KB 32.53 KB 32.53 KB UMD_PROD
react-art.development.js +0.5% +0.4% 608.91 KB 611.87 KB 128.58 KB 129.09 KB NODE_DEV
react-art.production.min.js 0.0% -0.0% 72.5 KB 72.5 KB 21.66 KB 21.66 KB NODE_PROD
ReactART-dev.js +0.5% +0.4% 621.72 KB 624.68 KB 128.47 KB 128.99 KB FB_WWW_DEV
ReactART-prod.js 🔺+0.7% 🔺+0.5% 238.39 KB 240.07 KB 40.2 KB 40.4 KB FB_WWW_PROD

ReactDOM: size: 0.0%, gzip: -0.1%

Size changes (experimental)

Generated by 🚫 dangerJS against 9ca1b2d

@bvaughn
Copy link
Contributor Author

bvaughn commented Feb 3, 2020

Rebased after merging PR #17925.

// Don't run create effects for a Fiber that errored during destroy.
// This check is in place to match previous behavior.
// TODO: Rethink whether we want to carry this behavior forward.
if (effectWithErrorDuringUnmount !== effect) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

If multiple fibers errored, this check only works for the last one. So it doesn't seem worth it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fair enough. I would prefer just not carrying this behavior forward anyway. I'll remove it.

@bvaughn bvaughn requested a review from acdlite February 3, 2020 22:11
@acdlite
Copy link
Collaborator

acdlite commented Feb 3, 2020

I was thinking the way you could do this is by maintaining two arrays: one for destroy effects, another for create/init effects.

During the sync commit phase, traverse over the hook effect list and push to the appropriate array.

Then during commitPassiveEffects, you would iterate over the arrays without touching the fiber effect list at all.

The advantage of this approach is it's less total work because in the passive phase, you no longer have to visit the entire effect list. The disadvantage is it sometimes shifts more work to the layout phase, because you can't skip over a fiber that only has passive effects scheduled on it.

But the other advantage is that it saves on code size, which is why I'm leaning toward always pushing to an array. cc @sebmarkbage for input.

@bvaughn
Copy link
Contributor Author

bvaughn commented Feb 3, 2020

That could bring things more inline with the way I'm deferring destroy functions on unmount... (That could just be handled by this mechanism as well.)

I'm open to making this change, but I'm going to hold off to see what @sebmarkbage thinks since it would require a bit of code juggling (especially since we want to keep everything behind a feature flag for now). Hm...I think we'd need to track the effect's corresponding fiber too, for error tracking purposes.

@bvaughn
Copy link
Contributor Author

bvaughn commented Feb 3, 2020

@acdlite I took a pass at this locally (diff below).

It has an added benefit of calling more unmount functions in the case of one throwing, but in order to maintain the expected error behavior- I think it would also be a bit more code than maybe you expected? (We'd have to track the hook effect + Fiber, unless I'm overlooking something.)

(Happy to push this commit though if you think the approach is better or if it would be easier to review than a diff.)

diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js
index c6fc36be3..5eeb7d290 100644
--- a/packages/react-reconciler/src/ReactFiberCommitWork.js
+++ b/packages/react-reconciler/src/ReactFiberCommitWork.js
@@ -110,7 +110,8 @@ import {
   captureCommitPhaseError,
   resolveRetryThenable,
   markCommitTimeOfFallback,
-  enqueuePendingPassiveEffectDestroyFn,
+  enqueuePendingPassiveHookEffectMount,
+  enqueuePendingPassiveHookEffectUnmount,
 } from './ReactFiberWorkLoop';
 import {
   NoEffect as NoHookEffect,
@@ -396,49 +397,39 @@ function commitHookEffectListMount(tag: number, finishedWork: Fiber) {
   }
 }
 
-export function commitPassiveHookEffects(finishedWork: Fiber): void {
-  if ((finishedWork.effectTag & Passive) !== NoEffect) {
-    switch (finishedWork.tag) {
-      case FunctionComponent:
-      case ForwardRef:
-      case SimpleMemoComponent:
-      case Chunk: {
-        // TODO (#17945) We should call all passive destroy functions (for all fibers)
-        // before calling any create functions. The current approach only serializes
-        // these for a single fiber.
-        commitHookEffectListUnmount(HookPassive | HookHasEffect, finishedWork);
-        commitHookEffectListMount(HookPassive | HookHasEffect, finishedWork);
-        break;
-      }
-      default:
-        break;
+function schedulePassiveEffects(finishedWork: Fiber) {
+  if (deferPassiveEffectCleanupDuringUnmount) {
+    const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
+    let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
+    if (lastEffect !== null) {
+      const firstEffect = lastEffect.next;
+      let effect = firstEffect;
+      do {
+        const {next, tag} = effect;
+        if (
+          (tag & HookPassive) !== NoHookEffect &&
+          (tag & HookHasEffect) !== NoHookEffect
+        ) {
+          enqueuePendingPassiveHookEffectUnmount(finishedWork, effect);
+          enqueuePendingPassiveHookEffectMount(finishedWork, effect);
+        }
+        effect = next;
+      } while (effect !== firstEffect);
     }
   }
 }
 
-export function commitPassiveHookUnmountEffects(finishedWork: Fiber): void {
+export function commitPassiveHookEffects(finishedWork: Fiber): void {
   if ((finishedWork.effectTag & Passive) !== NoEffect) {
     switch (finishedWork.tag) {
       case FunctionComponent:
       case ForwardRef:
       case SimpleMemoComponent:
       case Chunk: {
+        // TODO (#17945) We should call all passive destroy functions (for all fibers)
+        // before calling any create functions. The current approach only serializes
+        // these for a single fiber.
         commitHookEffectListUnmount(HookPassive | HookHasEffect, finishedWork);
-        break;
-      }
-      default:
-        break;
-    }
-  }
-}
-
-export function commitPassiveHookMountEffects(finishedWork: Fiber): void {
-  if ((finishedWork.effectTag & Passive) !== NoEffect) {
-    switch (finishedWork.tag) {
-      case FunctionComponent:
-      case ForwardRef:
-      case SimpleMemoComponent:
-      case Chunk: {
         commitHookEffectListMount(HookPassive | HookHasEffect, finishedWork);
         break;
       }
@@ -464,6 +455,10 @@ function commitLifeCycles(
       // e.g. a destroy function in one component should never override a ref set
       // by a create function in another component during the same commit.
       commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
+
+      if (deferPassiveEffectCleanupDuringUnmount) {
+        schedulePassiveEffects(finishedWork);
+      }
       return;
     }
     case ClassComponent: {
@@ -806,7 +801,7 @@ function commitUnmount(
               const {destroy, tag} = effect;
               if (destroy !== undefined) {
                 if ((tag & HookPassive) !== NoHookEffect) {
-                  enqueuePendingPassiveEffectDestroyFn(destroy);
+                  enqueuePendingPassiveHookEffectUnmount(current, effect);
                 } else {
                   safelyCallDestroy(current, destroy);
                 }
diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js
index fdb0875f8..851e22f8f 100644
--- a/packages/react-reconciler/src/ReactFiberHooks.js
+++ b/packages/react-reconciler/src/ReactFiberHooks.js
@@ -144,7 +144,7 @@ export type Hook = {|
   next: Hook | null,
 |};
 
-type Effect = {|
+export type Effect = {|
   tag: HookEffectTag,
   create: () => (() => void) | void,
   destroy: (() => void) | void,
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js
index ae4db267b..b015e63de 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js
@@ -14,7 +14,7 @@ import type {ReactPriorityLevel} from './SchedulerWithReactIntegration';
 import type {Interaction} from 'scheduler/src/Tracing';
 import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
 import type {SuspenseState} from './ReactFiberSuspenseComponent';
-import type {Hook} from './ReactFiberHooks';
+import type {Effect as HookEffect, Hook} from './ReactFiberHooks';
 
 import {
   warnAboutDeprecatedLifecycles,
@@ -133,8 +133,6 @@ import {
   commitBeforeMutationLifeCycles as commitBeforeMutationEffectOnFiber,
   commitLifeCycles as commitLayoutEffectOnFiber,
   commitPassiveHookEffects,
-  commitPassiveHookUnmountEffects,
-  commitPassiveHookMountEffects,
   commitPlacement,
   commitWork,
   commitDeletion,
@@ -260,7 +258,8 @@ let rootDoesHavePassiveEffects: boolean = false;
 let rootWithPendingPassiveEffects: FiberRoot | null = null;
 let pendingPassiveEffectsRenderPriority: ReactPriorityLevel = NoPriority;
 let pendingPassiveEffectsExpirationTime: ExpirationTime = NoWork;
-let pendingUnmountedPassiveEffectDestroyFunctions: Array<() => void> = [];
+let pendingPassiveHookEffectsMount: Array<HookEffect | Fiber> = [];
+let pendingPassiveHookEffectsUnmount: Array<HookEffect | Fiber> = [];
 
 let rootsWithPendingDiscreteUpdates: Map<
   FiberRoot,
@@ -2180,11 +2179,28 @@ export function flushPassiveEffects() {
   }
 }
 
-export function enqueuePendingPassiveEffectDestroyFn(
-  destroy: () => void,
+export function enqueuePendingPassiveHookEffectMount(
+  fiber: Fiber,
+  effect: HookEffect,
+): void {
+  if (deferPassiveEffectCleanupDuringUnmount) {
+    pendingPassiveHookEffectsMount.push(effect, fiber);
+    if (!rootDoesHavePassiveEffects) {
+      rootDoesHavePassiveEffects = true;
+      scheduleCallback(NormalPriority, () => {
+        flushPassiveEffects();
+        return null;
+      });
+    }
+  }
+}
+
+export function enqueuePendingPassiveHookEffectUnmount(
+  fiber: Fiber,
+  effect: HookEffect,
 ): void {
   if (deferPassiveEffectCleanupDuringUnmount) {
-    pendingUnmountedPassiveEffectDestroyFunctions.push(destroy);
+    pendingPassiveHookEffectsUnmount.push(effect, fiber);
     if (!rootDoesHavePassiveEffects) {
       rootDoesHavePassiveEffects = true;
       scheduleCallback(NormalPriority, () => {
@@ -2195,6 +2211,11 @@ export function enqueuePendingPassiveEffectDestroyFn(
   }
 }
 
+function invokePassiveEffectCreate(effect: HookEffect): void {
+  const create = effect.create;
+  effect.destroy = create();
+}
+
 function flushPassiveEffectsImpl() {
   if (rootWithPendingPassiveEffects === null) {
     return false;
@@ -2213,18 +2234,6 @@ function flushPassiveEffectsImpl() {
   const prevInteractions = pushInteractions(root);
 
   if (deferPassiveEffectCleanupDuringUnmount) {
-    // Flush any pending passive effect destroy functions that belong to
-    // components that were unmounted during the most recent commit.
-    for (
-      let i = 0;
-      i < pendingUnmountedPassiveEffectDestroyFunctions.length;
-      i++
-    ) {
-      const destroy = pendingUnmountedPassiveEffectDestroyFunctions[i];
-      invokeGuardedCallback(null, destroy, null);
-    }
-    pendingUnmountedPassiveEffectDestroyFunctions.length = 0;
-
     // It's important that ALL pending passive effect destroy functions are called
     // before ANY passive effect create functions are called.
     // Otherwise effects in sibling components might interfere with each other.
@@ -2233,70 +2242,60 @@ function flushPassiveEffectsImpl() {
     // Layout effects have the same constraint.
 
     // First pass: Destroy stale passive effects.
-    //
-    // Note: This currently assumes there are no passive effects on the root fiber
-    // because the root is not part of its own effect list.
-    // This could change in the future.
-    let effect = root.current.firstEffect;
-    while (effect !== null) {
-      if (__DEV__) {
-        setCurrentDebugFiberInDEV(effect);
-        invokeGuardedCallback(
-          null,
-          commitPassiveHookUnmountEffects,
-          null,
-          effect,
-        );
-        if (hasCaughtError()) {
-          invariant(effect !== null, 'Should be working on an effect.');
-          const error = clearCaughtError();
-          captureCommitPhaseError(effect, error);
-        }
-        resetCurrentDebugFiberInDEV();
-      } else {
-        try {
-          commitPassiveHookUnmountEffects(effect);
-        } catch (error) {
-          invariant(effect !== null, 'Should be working on an effect.');
-          captureCommitPhaseError(effect, error);
+    let unmountEffects = pendingPassiveHookEffectsUnmount;
+    pendingPassiveHookEffectsUnmount = [];
+    for (let i = 0; i < unmountEffects.length; i += 2) {
+      const effect = ((unmountEffects[i]: any): HookEffect);
+      const fiber = ((unmountEffects[i + 1]: any): Fiber);
+      const destroy = effect.destroy;
+      if (typeof destroy === 'function') {
+        if (__DEV__) {
+          setCurrentDebugFiberInDEV(fiber);
+          invokeGuardedCallback(null, destroy, null);
+          effect.destroy = undefined;
+          if (hasCaughtError()) {
+            invariant(fiber !== null, 'Should be working on an effect.');
+            const error = clearCaughtError();
+            captureCommitPhaseError(fiber, error);
+          }
+          resetCurrentDebugFiberInDEV();
+        } else {
+          try {
+            destroy();
+          } catch (error) {
+            invariant(fiber !== null, 'Should be working on an effect.');
+            captureCommitPhaseError(fiber, error);
+          } finally {
+            effect.destroy = undefined;
+          }
         }
       }
-      effect = effect.nextEffect;
     }
 
     // Second pass: Create new passive effects.
-    //
-    // Note: This currently assumes there are no passive effects on the root fiber
-    // because the root is not part of its own effect list.
-    // This could change in the future.
-    effect = root.current.firstEffect;
-    while (effect !== null) {
+    let mountEffects = pendingPassiveHookEffectsMount;
+    pendingPassiveHookEffectsMount = [];
+    for (let i = 0; i < mountEffects.length; i += 2) {
+      const effect = ((mountEffects[i]: any): HookEffect);
+      const fiber = ((mountEffects[i + 1]: any): Fiber);
       if (__DEV__) {
-        setCurrentDebugFiberInDEV(effect);
-        invokeGuardedCallback(
-          null,
-          commitPassiveHookMountEffects,
-          null,
-          effect,
-        );
+        setCurrentDebugFiberInDEV(fiber);
+        invokeGuardedCallback(null, invokePassiveEffectCreate, null, effect);
         if (hasCaughtError()) {
-          invariant(effect !== null, 'Should be working on an effect.');
+          invariant(fiber !== null, 'Should be working on an effect.');
           const error = clearCaughtError();
-          captureCommitPhaseError(effect, error);
+          captureCommitPhaseError(fiber, error);
         }
         resetCurrentDebugFiberInDEV();
       } else {
         try {
-          commitPassiveHookMountEffects(effect);
+          const create = effect.create;
+          effect.destroy = create();
         } catch (error) {
-          invariant(effect !== null, 'Should be working on an effect.');
-          captureCommitPhaseError(effect, error);
+          invariant(fiber !== null, 'Should be working on an effect.');
+          captureCommitPhaseError(fiber, error);
         }
       }
-      const nextNextEffect = effect.nextEffect;
-      // Remove nextEffect pointer to assist GC
-      effect.nextEffect = null;
-      effect = nextNextEffect;
     }
   } else {
     // Note: This currently assumes there are no passive effects on the root fiber
diff --git a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js
index cc3eee28d..438a748fb 100644
--- a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js
@@ -1753,6 +1753,7 @@ describe('ReactHooksWithNoopRenderer', () => {
         // not block the subsequent create functions from being run.
         expect(Scheduler).toHaveYielded([
           'Oops!',
+          'Unmount B [0]',
           'Mount A [1]',
           'Mount B [1]',
         ]);

@sebmarkbage
Copy link
Collaborator

I'm confused about the review process here. The previous PR includes everything from this PR and is merged. #17925

@bvaughn
Copy link
Contributor Author

bvaughn commented Feb 4, 2020

I'm confused about the review process here. The previous PR includes everything from this PR and is merged. #17925

Ugh, the rebase history got messed up somehow.

This PR builds on top of the previous PR (not the other way around). I'll clean up the history now, but the file change/delta is accurate here.

@bvaughn
Copy link
Contributor Author

bvaughn commented Feb 4, 2020

Okay @sebmarkbage @acdlite

I manually cleaned up the branch history. (Sorry for the rebase confusion.)

There are two commits here now:

  • 0769c1f: This is my original proposed change for this PR.
  • 41e0752: This is my understanding of Andrew's suggestion, although I think it may be less clean than he pictured (because of error tracking). I pushed it anyway for discussion purposes.

effect: HookEffect,
): void {
if (deferPassiveEffectCleanupDuringUnmount) {
pendingPassiveHookEffectsMount.push(effect, fiber);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Retaining both the effect and Fiber is kind of gross, particularly in the same array. I think we'd need to track them both somewhere though because of how we track and report errors.

// not block the subsequent create functions from being run.
expect(Scheduler).toHaveYielded([
'Oops!',
'Unmount B [0]',
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the main positive change wrt 41e0752.

@bvaughn
Copy link
Contributor Author

bvaughn commented Feb 10, 2020

Ping~

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.

Just a heads up, we might want to refactor several parts of the commit phase but those are unobservable differences that we can do later. So I'm not too concerned about the implementation details here as long as we can test the semantics.

@bvaughn
Copy link
Contributor Author

bvaughn commented Feb 11, 2020

Thanks for the heads up, Seb. Andrew and I chatted a little offline about this yesterday too, so I think I'm at least aware of some of the planned changes. Happy to help with the refactoring when it's time.

Brian Vaughn added 2 commits February 11, 2020 09:32
Previously we only flushed destroy functions for a single fiber.

The reason this is important is that interleaving destroy/create effects between sibling components might cause components to interfere with each other (e.g. a destroy function in one component may unintentionally override a ref value set by a create function in another component).

This PR builds on top of the recently added deferPassiveEffectCleanupDuringUnmount kill switch to separate passive effects flushing into two separate phases (similar to layout effects).
This change offers a small advantage over the way we did things previous: it continues invoking destroy functions even after a previous one errored.
This was referenced Mar 15, 2021
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.

5 participants