@samitouri / QOS-React / commits / 55a4b1f377

memo supports Hooks

Sophie Alpert committed Oct 11, 2018 at 13:54 UTC 55a4b1f37799d1dd5b009ad22931be4131ca2c69
3 files changed +107 -29
packages/react-reconciler/src/ReactFiberCommitWork.js
+15 -5
@@ -36,6 +36,8 @@ import {
36 Profiler,
37 SuspenseComponent,
38 IncompleteClassComponent,
39 + MemoComponent,
40 + SimpleMemoComponent,
41 } from 'shared/ReactWorkTags';
42 import {
43 invokeGuardedCallback,
@@ -216,7 +218,8 @@ function commitBeforeMutationLifeCycles(
218 ): void {
219 switch (finishedWork.tag) {
220 case FunctionComponent:
219 - case ForwardRef: {
221 + case ForwardRef:
222 + case SimpleMemoComponent: {
223 commitHookEffectList(UnmountSnapshot, NoHookEffect, finishedWork);
224 return;
225 }
@@ -313,7 +316,8 @@ function commitLifeCycles(
316 ): void {
317 switch (finishedWork.tag) {
318 case FunctionComponent:
316 - case ForwardRef: {
319 + case ForwardRef:
320 + case SimpleMemoComponent: {
321 commitHookEffectList(UnmountLayout, MountLayout, finishedWork);
322 const newUpdateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
323 if (newUpdateQueue !== null) {
@@ -588,7 +592,9 @@ function commitUnmount(current: Fiber): void {
592
593 switch (current.tag) {
594 case FunctionComponent:
591 - case ForwardRef: {
595 + case ForwardRef:
596 + case MemoComponent:
597 + case SimpleMemoComponent: {
598 const updateQueue: FunctionComponentUpdateQueue | null = (current.updateQueue: any);
599 if (updateQueue !== null) {
600 const lastEffect = updateQueue.lastEffect;
@@ -981,7 +987,9 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
987 if (!supportsMutation) {
988 switch (finishedWork.tag) {
989 case FunctionComponent:
984 - case ForwardRef: {
990 + case ForwardRef:
991 + case MemoComponent:
992 + case SimpleMemoComponent: {
993 commitHookEffectList(UnmountMutation, MountMutation, finishedWork);
994 return;
995 }
@@ -993,7 +1001,9 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
1001
1002 switch (finishedWork.tag) {
1003 case FunctionComponent:
996 - case ForwardRef: {
1004 + case ForwardRef:
1005 + case MemoComponent:
1006 + case SimpleMemoComponent: {
1007 commitHookEffectList(UnmountMutation, MountMutation, finishedWork);
1008 return;
1009 }
packages/react-reconciler/src/ReactFiberScheduler.js
+14 -24
@@ -52,7 +52,8 @@ import {
52 FunctionComponent,
53 HostPortal,
54 HostRoot,
55 - PureComponent,
55 + MemoComponent,
56 + SimpleMemoComponent,
57 } from 'shared/ReactWorkTags';
58 import {
59 enableSchedulerTracing,
@@ -270,8 +271,8 @@ let nextEffect: Fiber | null = null;
271
272 let isCommitting: boolean = false;
273 let rootWithPendingPassiveEffects: FiberRoot | null = null;
273 -let firstPassiveEffect: Fiber | null = null;
274 let passiveEffectCallbackHandle: * = null;
275 +let passiveEffectCallback: * = null;
276
277 let legacyErrorBoundariesThatAlreadyFailed: Set<mixed> | null = null;
278
@@ -521,6 +522,7 @@ function commitAllLifeCycles(
522 function commitPassiveEffects(root: FiberRoot, firstEffect: Fiber): void {
523 rootWithPendingPassiveEffects = null;
524 passiveEffectCallbackHandle = null;
525 + passiveEffectCallback = null;
526
527 // Set this to true to prevent re-entrancy
528 const previousIsRendering = isRendering;
@@ -600,15 +602,13 @@ function flushPassiveEffectsBeforeSchedulingUpdateOnFiber(fiber: Fiber) {
602
603 function flushPassiveEffects(root: FiberRoot) {
604 if (
603 - passiveEffectCallbackHandle !== null &&
605 + passiveEffectCallback !== null &&
606 root === rootWithPendingPassiveEffects
607 ) {
608 Schedule_cancelCallback(passiveEffectCallbackHandle);
607 - passiveEffectCallbackHandle = null;
608 - rootWithPendingPassiveEffects = null;
609 - if (firstPassiveEffect !== null) {
610 - commitPassiveEffects(root, firstPassiveEffect);
611 - }
609 + // We call the scheduled callback instead of commitPassiveEffects directly
610 + // to ensure tracing works correctly.
611 + passiveEffectCallback();
612 }
613 }
614
@@ -807,26 +807,15 @@ function commitRoot(root: FiberRoot, finishedWork: Fiber): void {
807 // after the next paint. Schedule an callback to fire them in an async
808 // event. To ensure serial execution, the callback will be flushed early if
809 // we enter rootWithPendingPassiveEffects commit phase before then.
810 - const resolvedFirstEffect = (firstPassiveEffect = firstEffect);
811 -
812 - let passiveEffectCallback;
810 + let callback = commitPassiveEffects.bind(null, root, firstEffect);
811 if (enableSchedulerTracing) {
812 // TODO: Avoid this extra callback by mutating the tracing ref directly,
813 // like we do at the beginning of commitRoot. I've opted not to do that
814 // here because that code is still in flux.
817 - passiveEffectCallback = Schedule_tracing_wrap(() => {
818 - commitPassiveEffects(root, resolvedFirstEffect);
819 - });
820 - } else {
821 - passiveEffectCallback = commitPassiveEffects.bind(
822 - null,
823 - root,
824 - resolvedFirstEffect,
825 - );
815 + callback = Schedule_tracing_wrap(callback);
816 }
827 - passiveEffectCallbackHandle = Schedule_scheduleCallback(
828 - passiveEffectCallback,
829 - );
817 + passiveEffectCallbackHandle = Schedule_scheduleCallback(callback);
818 + passiveEffectCallback = callback;
819 }
820
821 isCommitting = false;
@@ -1798,7 +1787,8 @@ function scheduleWorkToRoot(fiber: Fiber, expirationTime): FiberRoot | null {
1787 break;
1788 case FunctionComponent:
1789 case ForwardRef:
1801 - case PureComponent:
1790 + case MemoComponent:
1791 + case SimpleMemoComponent:
1792 warnAboutUpdateOnUnmounted(fiber, false);
1793 break;
1794 }
packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js
+78
@@ -15,6 +15,7 @@
15 let React;
16 let ReactFeatureFlags;
17 let ReactNoop;
18 +let SchedulerTracing;
19 let useState;
20 let useReducer;
21 let useEffect;
@@ -26,6 +27,7 @@ let useRef;
27 let useAPI;
28 let forwardRef;
29 let flushPassiveEffects;
30 +let memo;
31
32 describe('ReactHooks', () => {
33 beforeEach(() => {
@@ -56,8 +58,10 @@ describe('ReactHooks', () => {
58 ReactFeatureFlags = require('shared/ReactFeatureFlags');
59 ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
60 ReactFeatureFlags.enableHooks = true;
61 + ReactFeatureFlags.enableSchedulerTracing = true;
62 React = require('react');
63 ReactNoop = require('react-noop-renderer');
64 + SchedulerTracing = require('scheduler/tracing');
65 useState = React.useState;
66 useReducer = React.useReducer;
67 useEffect = React.useEffect;
@@ -68,6 +72,7 @@ describe('ReactHooks', () => {
72 useRef = React.useRef;
73 useAPI = React.useAPI;
74 forwardRef = React.forwardRef;
75 + memo = React.memo;
76 });
77
78 function span(prop) {
@@ -330,6 +335,28 @@ describe('ReactHooks', () => {
335 ' in Counter (at **)',
336 );
337 });
338 +
339 + it('works with memo', () => {
340 + let _updateCount;
341 + function Counter(props) {
342 + const [count, updateCount] = useState(0);
343 + _updateCount = updateCount;
344 + return <Text text={'Count: ' + count} />;
345 + }
346 + Counter = memo(Counter);
347 +
348 + ReactNoop.render(<Counter />);
349 + expect(ReactNoop.flush()).toEqual(['Count: 0']);
350 + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
351 +
352 + ReactNoop.render(<Counter />);
353 + expect(ReactNoop.flush()).toEqual([]);
354 + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
355 +
356 + _updateCount(1);
357 + expect(ReactNoop.flush()).toEqual(['Count: 1']);
358 + expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
359 + });
360 });
361
362 describe('updates during the render phase', () => {
@@ -797,6 +824,7 @@ describe('ReactHooks', () => {
824 }, []);
825 return <Text text={'Count: ' + count} />;
826 }
827 +
828 ReactNoop.render(<Counter count={0} />);
829 expect(ReactNoop.flush()).toEqual(['Count: 0']);
830 expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
@@ -808,6 +836,56 @@ describe('ReactHooks', () => {
836 expect(ReactNoop.getChildren()).toEqual([span('Count: 2')]);
837 });
838
839 + it('flushes serial effects before enqueueing work (with tracing)', () => {
840 + const onInteractionScheduledWorkCompleted = jest.fn();
841 + const onWorkCanceled = jest.fn();
842 + SchedulerTracing.unstable_subscribe({
843 + onInteractionScheduledWorkCompleted,
844 + onInteractionTraced: jest.fn(),
845 + onWorkCanceled,
846 + onWorkScheduled: jest.fn(),
847 + onWorkStarted: jest.fn(),
848 + onWorkStopped: jest.fn(),
849 + });
850 +
851 + let _updateCount;
852 + function Counter(props) {
853 + const [count, updateCount] = useState(0);
854 + _updateCount = updateCount;
855 + useEffect(() => {
856 + expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([
857 + tracingEvent,
858 + ]);
859 + ReactNoop.yield(`Will set count to 1`);
860 + updateCount(1);
861 + }, []);
862 + return <Text text={'Count: ' + count} />;
863 + }
864 +
865 + const tracingEvent = {id: 0, name: 'hello', timestamp: 0};
866 + SchedulerTracing.unstable_trace(
867 + tracingEvent.name,
868 + tracingEvent.timestamp,
869 + () => {
870 + ReactNoop.render(<Counter count={0} />);
871 + },
872 + );
873 + expect(ReactNoop.flush()).toEqual(['Count: 0']);
874 + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
875 +
876 + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(0);
877 +
878 + // Enqueuing this update forces the passive effect to be flushed --
879 + // updateCount(1) happens first, so 2 wins.
880 + _updateCount(2);
881 + expect(ReactNoop.flush()).toEqual(['Will set count to 1', 'Count: 2']);
882 + expect(ReactNoop.getChildren()).toEqual([span('Count: 2')]);
883 +
884 + flushPassiveEffects();
885 + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
886 + expect(onWorkCanceled).toHaveBeenCalledTimes(0);
887 + });
888 +
889 it(
890 'in sync mode, useEffect is deferred and updates finish synchronously ' +
891 '(in a single batch)',