Skip to content

Commit f68a77b

Browse files
committed
Initial pass at getSnapshotBeforeUpdate. More to do.
1 parent dc48326 commit f68a77b

File tree

9 files changed

+266
-55
lines changed

9 files changed

+266
-55
lines changed

packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,7 @@ describe('ReactComponentLifeCycle', () => {
591591
}
592592
componentDidMount = logger('outer componentDidMount');
593593
shouldComponentUpdate = logger('outer shouldComponentUpdate');
594+
getSnapshotBeforeUpdate = logger('outer getSnapshotBeforeUpdate');
594595
componentDidUpdate = logger('outer componentDidUpdate');
595596
componentWillUnmount = logger('outer componentWillUnmount');
596597
render() {
@@ -610,6 +611,7 @@ describe('ReactComponentLifeCycle', () => {
610611
}
611612
componentDidMount = logger('inner componentDidMount');
612613
shouldComponentUpdate = logger('inner shouldComponentUpdate');
614+
getSnapshotBeforeUpdate = logger('inner getSnapshotBeforeUpdate');
613615
componentDidUpdate = logger('inner componentDidUpdate');
614616
componentWillUnmount = logger('inner componentWillUnmount');
615617
render() {
@@ -635,6 +637,8 @@ describe('ReactComponentLifeCycle', () => {
635637
'outer shouldComponentUpdate',
636638
'inner getDerivedStateFromProps',
637639
'inner shouldComponentUpdate',
640+
'inner getSnapshotBeforeUpdate',
641+
'outer getSnapshotBeforeUpdate',
638642
'inner componentDidUpdate',
639643
'outer componentDidUpdate',
640644
]);
@@ -875,4 +879,80 @@ describe('ReactComponentLifeCycle', () => {
875879
ReactTestUtils.Simulate.click(divRef.current);
876880
expect(divRef.current.textContent).toBe('remote:2, local:2');
877881
});
882+
883+
it('should pass the return value from getSnapshotBeforeUpdate to componentDidUpdate', () => {
884+
const log = [];
885+
886+
class MyComponent extends React.Component {
887+
state = {
888+
value: 0,
889+
};
890+
static getDerivedStateFromProps(nextProps, prevState) {
891+
return {
892+
value: prevState.value + 1,
893+
};
894+
}
895+
getSnapshotBeforeUpdate(prevProps, prevState) {
896+
log.push(
897+
`getSnapshotBeforeUpdate() prevProps:${prevProps.value} prevState:${
898+
prevState.value
899+
}`,
900+
);
901+
return 'abc';
902+
}
903+
componentDidUpdate(prevProps, prevState, snapshot) {
904+
log.push(
905+
`componentDidUpdate() prevProps:${prevProps.value} prevState:${
906+
prevState.value
907+
} snapshot:${snapshot}`,
908+
);
909+
}
910+
render() {
911+
log.push('render');
912+
return null;
913+
}
914+
}
915+
916+
const div = document.createElement('div');
917+
ReactDOM.render(
918+
<div>
919+
<MyComponent value="foo" />
920+
</div>,
921+
div,
922+
);
923+
expect(log).toEqual(['render']);
924+
log.length = 0;
925+
926+
ReactDOM.render(
927+
<div>
928+
<MyComponent value="bar" />
929+
</div>,
930+
div,
931+
);
932+
expect(log).toEqual([
933+
'render',
934+
'getSnapshotBeforeUpdate() prevProps:foo prevState:1',
935+
'componentDidUpdate() prevProps:foo prevState:1 snapshot:abc',
936+
]);
937+
log.length = 0;
938+
939+
ReactDOM.render(
940+
<div>
941+
<MyComponent value="baz" />
942+
</div>,
943+
div,
944+
);
945+
expect(log).toEqual([
946+
'render',
947+
'getSnapshotBeforeUpdate() prevProps:bar prevState:2',
948+
'componentDidUpdate() prevProps:bar prevState:2 snapshot:abc',
949+
]);
950+
log.length = 0;
951+
952+
ReactDOM.render(<div />, div);
953+
expect(log).toEqual([]);
954+
955+
// De-duped
956+
ReactDOM.render(<MyComponent />, div);
957+
});
878958
});

packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2081,4 +2081,44 @@ describe('ReactErrorBoundaries', () => {
20812081
});
20822082
}).toThrow('foo error');
20832083
});
2084+
2085+
it('handles errors that occur in before-mutation commit hook', () => {
2086+
const errors = [];
2087+
let caughtError;
2088+
class Parent extends React.Component {
2089+
getSnapshotBeforeUpdate() {
2090+
errors.push('parent sad');
2091+
throw new Error('parent sad');
2092+
}
2093+
componentDidUpdate() {}
2094+
render() {
2095+
return <Child {...this.props} />;
2096+
}
2097+
}
2098+
class Child extends React.Component {
2099+
getSnapshotBeforeUpdate() {
2100+
errors.push('child sad');
2101+
throw new Error('child sad');
2102+
}
2103+
componentDidUpdate() {}
2104+
render() {
2105+
return <div />;
2106+
}
2107+
}
2108+
2109+
const container = document.createElement('div');
2110+
ReactDOM.render(<Parent value={1} />, container);
2111+
try {
2112+
ReactDOM.render(<Parent value={2} />, container);
2113+
} catch (e) {
2114+
if (e.message !== 'parent sad' && e.message !== 'child sad') {
2115+
throw e;
2116+
}
2117+
caughtError = e;
2118+
}
2119+
2120+
expect(errors).toEqual(['child sad', 'parent sad']);
2121+
// Error should be the first thrown
2122+
expect(caughtError.message).toBe('child sad');
2123+
});
20842124
});

packages/react-reconciler/src/ReactDebugFiberPerf.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ type MeasurementPhase =
3131
| 'componentWillUpdate'
3232
| 'componentDidUpdate'
3333
| 'componentDidMount'
34-
| 'getChildContext';
34+
| 'getChildContext'
35+
| 'getSnapshotBeforeUpdate';
3536

3637
// Prefix measurements so that it's possible to filter them.
3738
// Longer prefixes are hard to read in DevTools.

packages/react-reconciler/src/ReactFiber.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@ export type Fiber = {|
150150
// memory if we need to.
151151
alternate: Fiber | null,
152152

153+
// Stores getSnapshotBeforeUpdate return value to be passed to componentDidUpdate
154+
snapshot: any | null,
155+
153156
// Conceptual aliases
154157
// workInProgress : Fiber -> alternate The alternate used for reuse happens
155158
// to be the same as work in progress.
@@ -204,6 +207,8 @@ function FiberNode(
204207

205208
this.alternate = null;
206209

210+
this.snapshot = null;
211+
207212
if (__DEV__) {
208213
this._debugID = debugCounter++;
209214
this._debugSource = null;

packages/react-reconciler/src/ReactFiberClassComponent.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type {ExpirationTime} from './ReactFiberExpirationTime';
1212
import type {LegacyContext} from './ReactFiberContext';
1313
import type {CapturedValue} from './ReactCapturedValue';
1414

15-
import {Update} from 'shared/ReactTypeOfSideEffect';
15+
import {Update, Snapshot} from 'shared/ReactTypeOfSideEffect';
1616
import {
1717
enableGetDerivedStateFromCatch,
1818
debugRenderPhaseSideEffects,
@@ -873,6 +873,7 @@ export default function(
873873
// TODO: Previous state can be null.
874874
let newState;
875875
let derivedStateFromCatch;
876+
876877
if (workInProgress.updateQueue !== null) {
877878
newState = processUpdateQueue(
878879
current,
@@ -954,6 +955,14 @@ export default function(
954955
workInProgress.effectTag |= Update;
955956
}
956957
}
958+
if (typeof instance.getSnapshotBeforeUpdate === 'function') {
959+
if (
960+
oldProps !== current.memoizedProps ||
961+
oldState !== current.memoizedState
962+
) {
963+
workInProgress.effectTag |= Snapshot;
964+
}
965+
}
957966
return false;
958967
}
959968

@@ -986,6 +995,9 @@ export default function(
986995
if (typeof instance.componentDidUpdate === 'function') {
987996
workInProgress.effectTag |= Update;
988997
}
998+
if (typeof instance.getSnapshotBeforeUpdate === 'function') {
999+
workInProgress.effectTag |= Snapshot;
1000+
}
9891001
} else {
9901002
// If an update was already in progress, we should schedule an Update
9911003
// effect even though we're bailing out, so that cWU/cDU are called.
@@ -997,6 +1009,14 @@ export default function(
9971009
workInProgress.effectTag |= Update;
9981010
}
9991011
}
1012+
if (typeof instance.getSnapshotBeforeUpdate === 'function') {
1013+
if (
1014+
oldProps !== current.memoizedProps ||
1015+
oldState !== current.memoizedState
1016+
) {
1017+
workInProgress.effectTag |= Snapshot;
1018+
}
1019+
}
10001020

10011021
// If shouldComponentUpdate returned false, we should still update the
10021022
// memoized props/state to indicate that this work can be reused.

packages/react-reconciler/src/ReactFiberCommitWork.js

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,12 @@ import {
2727
CallComponent,
2828
} from 'shared/ReactTypeOfWork';
2929
import ReactErrorUtils from 'shared/ReactErrorUtils';
30-
import {Placement, Update, ContentReset} from 'shared/ReactTypeOfSideEffect';
30+
import {
31+
Placement,
32+
Update,
33+
ContentReset,
34+
Snapshot,
35+
} from 'shared/ReactTypeOfSideEffect';
3136
import invariant from 'fbjs/lib/invariant';
3237
import warning from 'fbjs/lib/warning';
3338

@@ -153,6 +158,47 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
153158
}
154159
}
155160

161+
function commitBeforeMutationLifeCycles(
162+
current: Fiber | null,
163+
finishedWork: Fiber,
164+
): void {
165+
switch (finishedWork.tag) {
166+
case ClassComponent: {
167+
const instance = finishedWork.stateNode;
168+
if (finishedWork.effectTag & Snapshot) {
169+
if (current !== null) {
170+
const prevProps = current.memoizedProps;
171+
const prevState = current.memoizedState;
172+
startPhaseTimer(finishedWork, 'getSnapshotBeforeUpdate');
173+
instance.props = finishedWork.memoizedProps;
174+
instance.state = finishedWork.memoizedState;
175+
const snapshot = instance.getSnapshotBeforeUpdate(
176+
prevProps,
177+
prevState,
178+
);
179+
// TODO Warn about undefined return value
180+
current.snapshot = snapshot != null ? snapshot : null;
181+
stopPhaseTimer();
182+
}
183+
}
184+
return;
185+
}
186+
case HostRoot:
187+
case HostComponent:
188+
case HostText:
189+
case HostPortal:
190+
// Nothing to do for these component types
191+
return;
192+
default: {
193+
invariant(
194+
false,
195+
'This unit of work tag should not have side-effects. This error is ' +
196+
'likely caused by a bug in React. Please file an issue.',
197+
);
198+
}
199+
}
200+
}
201+
156202
function commitLifeCycles(
157203
finishedRoot: FiberRoot,
158204
current: Fiber | null,
@@ -176,7 +222,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
176222
startPhaseTimer(finishedWork, 'componentDidUpdate');
177223
instance.props = finishedWork.memoizedProps;
178224
instance.state = finishedWork.memoizedState;
179-
instance.componentDidUpdate(prevProps, prevState);
225+
instance.componentDidUpdate(prevProps, prevState, current.snapshot);
180226
stopPhaseTimer();
181227
}
182228
}
@@ -494,6 +540,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
494540
commitContainer(finishedWork);
495541
},
496542
commitLifeCycles,
543+
commitBeforeMutationLifeCycles,
497544
commitErrorLogging,
498545
commitAttachRef,
499546
commitDetachRef,
@@ -816,6 +863,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
816863

817864
if (enableMutatingReconciler) {
818865
return {
866+
commitBeforeMutationLifeCycles,
819867
commitResetTextContent,
820868
commitPlacement,
821869
commitDeletion,

0 commit comments

Comments
 (0)