Skip to content

Commit c65a004

Browse files
committed
Update to match 16.4 gDSFP semantics
1 parent f457b78 commit c65a004

File tree

7 files changed

+1365
-61
lines changed

7 files changed

+1365
-61
lines changed

index.js

+139-40
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,111 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
function componentWillMount() {
9-
// Call this.constructor.gDSFP to support sub-classes.
10-
var state = this.constructor.getDerivedStateFromProps(this.props, this.state);
11-
if (state !== null && state !== undefined) {
12-
this.setState(state);
8+
// Conceptually, getDerivedStateFromProps is like a setState updater function.
9+
// But it needs to be called *after* all the other updates in the queue, and it
10+
// must be called exactly once, right before shouldComponentUpdate.
11+
//
12+
// Other than getDerivedStateFormProps itself, there's no lifecycle that meets
13+
// these requirements. componentWillReceiveProps fires only if the props have
14+
// changed. componentWillUpdate fires too late.
15+
//
16+
// This polyfill works by monkeypatching instance.updater. updater is an
17+
// internal-ish API that has stayed mostly stable across several major versions
18+
// of React. The polyfill intercepts updates before they are added to the native
19+
// update queue. Then it schedules a single update with the native update queue,
20+
// in the form of an updater function. Inside that updater function, the
21+
// intercepted updates are processed. Finally, getDerivedStateFromProps is
22+
// called and applied. This approach guarantees that getDerivedStateFromProps is
23+
// always called at the end.
24+
function processUpdateQueue(prevState, nextProps) {
25+
var queue = this.__updateQueue;
26+
this.__updateQueue = null;
27+
if (queue === null || queue === undefined) {
28+
return null;
1329
}
30+
// First process all the user-provided updates
31+
var nextState = prevState;
32+
var update = null;
33+
var payload = null;
34+
var callback = null;
35+
for (let i = 0; i < queue.length; i++) {
36+
update = queue[i];
37+
payload = update.payload;
38+
callback = update.callback;
39+
if (typeof payload === 'function') {
40+
payload = payload.call(this, prevState, nextProps);
41+
}
42+
if (payload !== null && payload !== undefined) {
43+
nextState = Object.assign({}, prevState, payload);
44+
}
45+
if (callback !== null) {
46+
let callbacks = this.__callbacks;
47+
if (callbacks === null || callbacks === undefined) {
48+
this.__callbacks = [callback];
49+
} else {
50+
callbacks.push(callback);
51+
}
52+
}
53+
}
54+
// Now that we've processed all the updates, we can call
55+
// `getDerivedStateFromProps`. This always comes last.
56+
var derivedState =
57+
this.constructor.getDerivedStateFromProps(nextProps, nextState);
58+
if (derivedState !== null && derivedState !== undefined) {
59+
nextState = Object.assign({}, prevState, derivedState);
60+
}
61+
return nextState;
1462
}
1563

16-
function componentWillReceiveProps(nextProps) {
17-
// Call this.constructor.gDSFP to support sub-classes.
18-
// Use the setState() updater to ensure state isn't stale in certain edge cases.
19-
function updater(prevState) {
20-
var state = this.constructor.getDerivedStateFromProps(nextProps, prevState);
21-
return state !== null && state !== undefined ? state : null;
22-
}
23-
// Binding "this" is important for shallow renderer support.
24-
this.setState(updater.bind(this));
64+
function componentWillMount() {
65+
const originalUpdater = this.updater;
66+
67+
this.updater = {
68+
enqueueSetState(instance, payload, callback) {
69+
var update = {
70+
payload: payload,
71+
callback: callback === undefined ? null : callback
72+
};
73+
let queue = instance.__updateQueue;
74+
if (queue === null || queue === undefined) {
75+
// Create a queue of updates. This will act as a polyfill for the native
76+
// update queue.
77+
queue = instance.__updateQueue = [update];
78+
// The native update queue should contain a single update function.
79+
// In that function, we will process all the updates in the polyfilled
80+
// queue. This allows us to call `getDerivedStateFromProps` after all
81+
// the other updates have been processed.
82+
originalUpdater.enqueueSetState(instance, processUpdateQueue);
83+
} else {
84+
// We already scheduled an update on the native queue. Push onto the
85+
// polyfilled queue.
86+
queue.push(update);
87+
}
88+
},
89+
enqueueReplaceState() {
90+
// Not worth implementing this since class components do no have a
91+
// `replaceState` method.
92+
throw new Error(
93+
'react-lifecycles-compat: enqueueReplaceState is not supported'
94+
);
95+
},
96+
// Note: Because forceUpdate does not accept an updater function, we can't
97+
// polyfill this correctly unless we're inside batchedUpdates. So gDSFP
98+
// will not fire unless we receive new props OR there's another update in
99+
// the same batch.
100+
enqueueForceUpdate: originalUpdater.enqueueForceUpdate,
101+
enqueueCallback(instance, callback) {
102+
instance.updater.enqueueSetState(instance, null, callback);
103+
}
104+
};
105+
106+
// Add an empty update to the queue to trigger getDerivedStateFromProps
107+
this.setState(null);
108+
}
109+
110+
function componentWillReceiveProps() {
111+
// Add an empty update to the queue to trigger getDerivedStateFromProps.
112+
this.setState(null);
25113
}
26114

27115
function componentWillUpdate(nextProps, nextState) {
@@ -109,33 +197,21 @@ export function polyfill(Component) {
109197
);
110198
}
111199

112-
// React <= 16.2 does not support static getDerivedStateFromProps.
113-
// As a workaround, use cWM and cWRP to invoke the new static lifecycle.
114-
// Newer versions of React will ignore these lifecycles if gDSFP exists.
115-
if (typeof Component.getDerivedStateFromProps === 'function') {
116-
prototype.componentWillMount = componentWillMount;
117-
prototype.componentWillReceiveProps = componentWillReceiveProps;
118-
}
119-
120-
// React <= 16.2 does not support getSnapshotBeforeUpdate.
121-
// As a workaround, use cWU to invoke the new lifecycle.
122-
// Newer versions of React will ignore that lifecycle if gSBU exists.
123-
if (typeof prototype.getSnapshotBeforeUpdate === 'function') {
124-
if (typeof prototype.componentDidUpdate !== 'function') {
125-
throw new Error(
126-
'Cannot polyfill getSnapshotBeforeUpdate() for components that do not define componentDidUpdate() on the prototype'
127-
);
200+
var componentDidUpdate = prototype.componentDidUpdate;
201+
function componentDidUpdatePolyfill(prevProps, prevState, maybeSnapshot) {
202+
if (typeof Component.getDerivedStateFromProps === 'function') {
203+
// If getDerivedStateFromProps is defined, we polyfilled the update queue.
204+
// Flush the callbacks here.
205+
var callbacks = this.__callbacks;
206+
if (callbacks !== null && callbacks !== undefined) {
207+
this.__callbacks = null;
208+
for (var i = 0; i < callbacks.length; i++) {
209+
callbacks[i].call(this);
210+
}
211+
}
128212
}
129213

130-
prototype.componentWillUpdate = componentWillUpdate;
131-
132-
var componentDidUpdate = prototype.componentDidUpdate;
133-
134-
prototype.componentDidUpdate = function componentDidUpdatePolyfill(
135-
prevProps,
136-
prevState,
137-
maybeSnapshot
138-
) {
214+
if (typeof componentDidUpdate === 'function') {
139215
// 16.3+ will not execute our will-update method;
140216
// It will pass a snapshot value to did-update though.
141217
// Older versions will require our polyfilled will-update value.
@@ -149,7 +225,30 @@ export function polyfill(Component) {
149225
: maybeSnapshot;
150226

151227
componentDidUpdate.call(this, prevProps, prevState, snapshot);
152-
};
228+
}
229+
}
230+
231+
// React <= 16.2 does not support static getDerivedStateFromProps.
232+
// As a workaround, use cWM and cWRP to invoke the new static lifecycle.
233+
// Newer versions of React will ignore these lifecycles if gDSFP exists.
234+
if (typeof Component.getDerivedStateFromProps === 'function') {
235+
prototype.componentWillMount = componentWillMount;
236+
prototype.componentWillReceiveProps = componentWillReceiveProps;
237+
prototype.componentDidUpdate = componentDidUpdatePolyfill;
238+
}
239+
240+
// React <= 16.2 does not support getSnapshotBeforeUpdate.
241+
// As a workaround, use cWU to invoke the new lifecycle.
242+
// Newer versions of React will ignore that lifecycle if gSBU exists.
243+
if (typeof prototype.getSnapshotBeforeUpdate === 'function') {
244+
prototype.componentWillUpdate = componentWillUpdate;
245+
if (typeof prototype.componentDidUpdate !== 'function') {
246+
throw new Error(
247+
'Cannot polyfill getSnapshotBeforeUpdate() for components that do ' +
248+
'not define componentDidUpdate() on the prototype'
249+
);
250+
}
251+
prototype.componentDidUpdate = componentDidUpdatePolyfill;
153252
}
154253

155254
return Component;

package.json

+7
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,14 @@
2121
"react-lifecycles-compat.js",
2222
"react-lifecycles-compat.min.js"
2323
],
24+
"babel": {
25+
"presets": [
26+
"env"
27+
]
28+
},
2429
"devDependencies": {
30+
"babel-jest": "^23.0.0",
31+
"babel-preset-env": "^1.7.0",
2532
"camelcase": "^5.0.0",
2633
"chalk": "^2.3.0",
2734
"eslint": "^4.16.0",

react/16.3/yarn.lock

+16-10
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,10 @@ fbjs@^0.8.16, fbjs@^0.8.9:
3737
ua-parser-js "^0.7.9"
3838

3939
iconv-lite@~0.4.13:
40-
version "0.4.19"
41-
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
40+
version "0.4.23"
41+
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63"
42+
dependencies:
43+
safer-buffer ">= 2.1.2 < 3"
4244

4345
is-stream@^1.0.1:
4446
version "1.1.0"
@@ -87,17 +89,17 @@ prop-types@^15.6.0:
8789
object-assign "^4.1.1"
8890

8991
90-
version "16.3.0"
91-
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.3.0.tgz#b318e52184188ecb5c3e81117420cca40618643e"
92+
version "16.3.2"
93+
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.3.2.tgz#cb90f107e09536d683d84ed5d4888e9640e0e4df"
9294
dependencies:
9395
fbjs "^0.8.16"
9496
loose-envify "^1.1.0"
9597
object-assign "^4.1.1"
9698
prop-types "^15.6.0"
9799

98100
react-is@^16.3.2:
99-
version "16.3.2"
100-
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.3.2.tgz#f4d3d0e2f5fbb6ac46450641eb2e25bf05d36b22"
101+
version "16.4.0"
102+
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.0.tgz#cc9fdc855ac34d2e7d9d2eb7059bbc240d35ffcf"
101103

102104
103105
version "16.3.2"
@@ -109,21 +111,25 @@ [email protected]:
109111
react-is "^16.3.2"
110112

111113
112-
version "16.3.0"
113-
resolved "https://registry.yarnpkg.com/react/-/react-16.3.0.tgz#fc5a01c68f91e9b38e92cf83f7b795ebdca8ddff"
114+
version "16.3.2"
115+
resolved "https://registry.yarnpkg.com/react/-/react-16.3.2.tgz#fdc8420398533a1e58872f59091b272ce2f91ea9"
114116
dependencies:
115117
fbjs "^0.8.16"
116118
loose-envify "^1.1.0"
117119
object-assign "^4.1.1"
118120
prop-types "^15.6.0"
119121

122+
"safer-buffer@>= 2.1.2 < 3":
123+
version "2.1.2"
124+
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
125+
120126
setimmediate@^1.0.5:
121127
version "1.0.5"
122128
resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
123129

124130
ua-parser-js@^0.7.9:
125-
version "0.7.17"
126-
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac"
131+
version "0.7.18"
132+
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.18.tgz#a7bfd92f56edfb117083b69e31d2aa8882d4b1ed"
127133

128134
whatwg-fetch@>=0.10.0:
129135
version "2.0.4"

react/16.4/package.json

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"private": true,
3+
"devDependencies": {
4+
"create-react-class": "^15.6.2",
5+
"react": "16.4",
6+
"react-dom": "16.4",
7+
"react-test-renderer": "16.4"
8+
},
9+
"scripts": {
10+
"test": "jest test.js"
11+
}
12+
}

0 commit comments

Comments
 (0)