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)',