[Fizz] Implement Classes (#21200)
* Legacy context * Port Classes from Fiber to Fizz
Sebastian Markbåge committed
Apr 8, 2021 at 13:42 UTC
cf45a623a1787cf57094eb7d2f66903d358647fa
3 files changed
+1042
-13
packages/react-server/src/ReactFizzClassComponent.js
new
+673
@@ -0,0 +1,673 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import {emptyContextObject} from './ReactFizzContext';
11
+
12
+import {disableLegacyContext} from 'shared/ReactFeatureFlags';
13
+import {get as getInstance, set as setInstance} from 'shared/ReactInstanceMap';
14
+import getComponentNameFromType from 'shared/getComponentNameFromType';
15
+import {REACT_CONTEXT_TYPE, REACT_PROVIDER_TYPE} from 'shared/ReactSymbols';
16
+import isArray from 'shared/isArray';
17
+
18
+const didWarnAboutNoopUpdateForComponent = {};
19
+
20
+let didWarnAboutUninitializedState;
21
+let didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate;
22
+let didWarnAboutLegacyLifecyclesAndDerivedState;
23
+let didWarnAboutUndefinedDerivedState;
24
+let warnOnUndefinedDerivedState;
25
+let warnOnInvalidCallback;
26
+let didWarnAboutDirectlyAssigningPropsToState;
27
+let didWarnAboutContextTypeAndContextTypes;
28
+let didWarnAboutInvalidateContextType;
29
+
30
+if (__DEV__) {
31
+ didWarnAboutUninitializedState = new Set();
32
+ didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate = new Set();
33
+ didWarnAboutLegacyLifecyclesAndDerivedState = new Set();
34
+ didWarnAboutDirectlyAssigningPropsToState = new Set();
35
+ didWarnAboutUndefinedDerivedState = new Set();
36
+ didWarnAboutContextTypeAndContextTypes = new Set();
37
+ didWarnAboutInvalidateContextType = new Set();
38
+
39
+ const didWarnOnInvalidCallback = new Set();
40
+
41
+ warnOnInvalidCallback = function(callback: mixed, callerName: string) {
42
+ if (callback === null || typeof callback === 'function') {
43
+ return;
44
+ }
45
+ const key = callerName + '_' + (callback: any);
46
+ if (!didWarnOnInvalidCallback.has(key)) {
47
+ didWarnOnInvalidCallback.add(key);
48
+ console.error(
49
+ '%s(...): Expected the last optional `callback` argument to be a ' +
50
+ 'function. Instead received: %s.',
51
+ callerName,
52
+ callback,
53
+ );
54
+ }
55
+ };
56
+
57
+ warnOnUndefinedDerivedState = function(type, partialState) {
58
+ if (partialState === undefined) {
59
+ const componentName = getComponentNameFromType(type) || 'Component';
60
+ if (!didWarnAboutUndefinedDerivedState.has(componentName)) {
61
+ didWarnAboutUndefinedDerivedState.add(componentName);
62
+ console.error(
63
+ '%s.getDerivedStateFromProps(): A valid state object (or null) must be returned. ' +
64
+ 'You have returned undefined.',
65
+ componentName,
66
+ );
67
+ }
68
+ }
69
+ };
70
+}
71
+
72
+function warnNoop(
73
+ publicInstance: React$Component<any, any>,
74
+ callerName: string,
75
+) {
76
+ if (__DEV__) {
77
+ const constructor = publicInstance.constructor;
78
+ const componentName =
79
+ (constructor && getComponentNameFromType(constructor)) || 'ReactClass';
80
+ const warningKey = componentName + '.' + callerName;
81
+ if (didWarnAboutNoopUpdateForComponent[warningKey]) {
82
+ return;
83
+ }
84
+
85
+ console.error(
86
+ '%s(...): Can only update a mounting component. ' +
87
+ 'This usually means you called %s() outside componentWillMount() on the server. ' +
88
+ 'This is a no-op.\n\nPlease check the code for the %s component.',
89
+ callerName,
90
+ callerName,
91
+ componentName,
92
+ );
93
+ didWarnAboutNoopUpdateForComponent[warningKey] = true;
94
+ }
95
+}
96
+
97
+type InternalInstance = {
98
+ queue: null | Array<Object>,
99
+ replace: boolean,
100
+};
101
+
102
+const classComponentUpdater = {
103
+ isMounted(inst) {
104
+ return false;
105
+ },
106
+ enqueueSetState(inst, payload, callback) {
107
+ const internals: InternalInstance = getInstance(inst);
108
+ if (internals.queue === null) {
109
+ warnNoop(inst, 'setState');
110
+ } else {
111
+ internals.queue.push(payload);
112
+ if (__DEV__) {
113
+ if (callback !== undefined && callback !== null) {
114
+ warnOnInvalidCallback(callback, 'setState');
115
+ }
116
+ }
117
+ }
118
+ },
119
+ enqueueReplaceState(inst, payload, callback) {
120
+ const internals: InternalInstance = getInstance(inst);
121
+ internals.replace = true;
122
+ internals.queue = [payload];
123
+ if (__DEV__) {
124
+ if (callback !== undefined && callback !== null) {
125
+ warnOnInvalidCallback(callback, 'setState');
126
+ }
127
+ }
128
+ },
129
+ enqueueForceUpdate(inst, callback) {
130
+ const internals: InternalInstance = getInstance(inst);
131
+ if (internals.queue === null) {
132
+ warnNoop(inst, 'forceUpdate');
133
+ } else {
134
+ if (__DEV__) {
135
+ if (callback !== undefined && callback !== null) {
136
+ warnOnInvalidCallback(callback, 'setState');
137
+ }
138
+ }
139
+ }
140
+ },
141
+};
142
+
143
+function applyDerivedStateFromProps(
144
+ instance: any,
145
+ ctor: any,
146
+ getDerivedStateFromProps: (props: any, state: any) => any,
147
+ prevState: any,
148
+ nextProps: any,
149
+) {
150
+ const partialState = getDerivedStateFromProps(nextProps, prevState);
151
+
152
+ if (__DEV__) {
153
+ warnOnUndefinedDerivedState(ctor, partialState);
154
+ }
155
+ // Merge the partial state and the previous state.
156
+ const newState =
157
+ partialState === null || partialState === undefined
158
+ ? prevState
159
+ : Object.assign({}, prevState, partialState);
160
+ return newState;
161
+}
162
+
163
+export function constructClassInstance(
164
+ ctor: any,
165
+ props: any,
166
+ maskedLegacyContext: any,
167
+): any {
168
+ let context = emptyContextObject;
169
+ const contextType = ctor.contextType;
170
+
171
+ if (__DEV__) {
172
+ if ('contextType' in ctor) {
173
+ const isValid =
174
+ // Allow null for conditional declaration
175
+ contextType === null ||
176
+ (contextType !== undefined &&
177
+ contextType.$$typeof === REACT_CONTEXT_TYPE &&
178
+ contextType._context === undefined); // Not a <Context.Consumer>
179
+
180
+ if (!isValid && !didWarnAboutInvalidateContextType.has(ctor)) {
181
+ didWarnAboutInvalidateContextType.add(ctor);
182
+
183
+ let addendum = '';
184
+ if (contextType === undefined) {
185
+ addendum =
186
+ ' However, it is set to undefined. ' +
187
+ 'This can be caused by a typo or by mixing up named and default imports. ' +
188
+ 'This can also happen due to a circular dependency, so ' +
189
+ 'try moving the createContext() call to a separate file.';
190
+ } else if (typeof contextType !== 'object') {
191
+ addendum = ' However, it is set to a ' + typeof contextType + '.';
192
+ } else if (contextType.$$typeof === REACT_PROVIDER_TYPE) {
193
+ addendum = ' Did you accidentally pass the Context.Provider instead?';
194
+ } else if (contextType._context !== undefined) {
195
+ // <Context.Consumer>
196
+ addendum = ' Did you accidentally pass the Context.Consumer instead?';
197
+ } else {
198
+ addendum =
199
+ ' However, it is set to an object with keys {' +
200
+ Object.keys(contextType).join(', ') +
201
+ '}.';
202
+ }
203
+ console.error(
204
+ '%s defines an invalid contextType. ' +
205
+ 'contextType should point to the Context object returned by React.createContext().%s',
206
+ getComponentNameFromType(ctor) || 'Component',
207
+ addendum,
208
+ );
209
+ }
210
+ }
211
+ }
212
+
213
+ if (typeof contextType === 'object' && contextType !== null) {
214
+ // TODO: Implement Context.
215
+ // context = readContext((contextType: any));
216
+ throw new Error('Context is not yet implemented.');
217
+ } else if (!disableLegacyContext) {
218
+ context = maskedLegacyContext;
219
+ }
220
+
221
+ const instance = new ctor(props, context);
222
+
223
+ if (__DEV__) {
224
+ if (
225
+ typeof ctor.getDerivedStateFromProps === 'function' &&
226
+ (instance.state === null || instance.state === undefined)
227
+ ) {
228
+ const componentName = getComponentNameFromType(ctor) || 'Component';
229
+ if (!didWarnAboutUninitializedState.has(componentName)) {
230
+ didWarnAboutUninitializedState.add(componentName);
231
+ console.error(
232
+ '`%s` uses `getDerivedStateFromProps` but its initial state is ' +
233
+ '%s. This is not recommended. Instead, define the initial state by ' +
234
+ 'assigning an object to `this.state` in the constructor of `%s`. ' +
235
+ 'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.',
236
+ componentName,
237
+ instance.state === null ? 'null' : 'undefined',
238
+ componentName,
239
+ );
240
+ }
241
+ }
242
+
243
+ // If new component APIs are defined, "unsafe" lifecycles won't be called.
244
+ // Warn about these lifecycles if they are present.
245
+ // Don't warn about react-lifecycles-compat polyfilled methods though.
246
+ if (
247
+ typeof ctor.getDerivedStateFromProps === 'function' ||
248
+ typeof instance.getSnapshotBeforeUpdate === 'function'
249
+ ) {
250
+ let foundWillMountName = null;
251
+ let foundWillReceivePropsName = null;
252
+ let foundWillUpdateName = null;
253
+ if (
254
+ typeof instance.componentWillMount === 'function' &&
255
+ instance.componentWillMount.__suppressDeprecationWarning !== true
256
+ ) {
257
+ foundWillMountName = 'componentWillMount';
258
+ } else if (typeof instance.UNSAFE_componentWillMount === 'function') {
259
+ foundWillMountName = 'UNSAFE_componentWillMount';
260
+ }
261
+ if (
262
+ typeof instance.componentWillReceiveProps === 'function' &&
263
+ instance.componentWillReceiveProps.__suppressDeprecationWarning !== true
264
+ ) {
265
+ foundWillReceivePropsName = 'componentWillReceiveProps';
266
+ } else if (
267
+ typeof instance.UNSAFE_componentWillReceiveProps === 'function'
268
+ ) {
269
+ foundWillReceivePropsName = 'UNSAFE_componentWillReceiveProps';
270
+ }
271
+ if (
272
+ typeof instance.componentWillUpdate === 'function' &&
273
+ instance.componentWillUpdate.__suppressDeprecationWarning !== true
274
+ ) {
275
+ foundWillUpdateName = 'componentWillUpdate';
276
+ } else if (typeof instance.UNSAFE_componentWillUpdate === 'function') {
277
+ foundWillUpdateName = 'UNSAFE_componentWillUpdate';
278
+ }
279
+ if (
280
+ foundWillMountName !== null ||
281
+ foundWillReceivePropsName !== null ||
282
+ foundWillUpdateName !== null
283
+ ) {
284
+ const componentName = getComponentNameFromType(ctor) || 'Component';
285
+ const newApiName =
286
+ typeof ctor.getDerivedStateFromProps === 'function'
287
+ ? 'getDerivedStateFromProps()'
288
+ : 'getSnapshotBeforeUpdate()';
289
+ if (!didWarnAboutLegacyLifecyclesAndDerivedState.has(componentName)) {
290
+ didWarnAboutLegacyLifecyclesAndDerivedState.add(componentName);
291
+ console.error(
292
+ 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' +
293
+ '%s uses %s but also contains the following legacy lifecycles:%s%s%s\n\n' +
294
+ 'The above lifecycles should be removed. Learn more about this warning here:\n' +
295
+ 'https://reactjs.org/link/unsafe-component-lifecycles',
296
+ componentName,
297
+ newApiName,
298
+ foundWillMountName !== null ? `\n ${foundWillMountName}` : '',
299
+ foundWillReceivePropsName !== null
300
+ ? `\n ${foundWillReceivePropsName}`
301
+ : '',
302
+ foundWillUpdateName !== null ? `\n ${foundWillUpdateName}` : '',
303
+ );
304
+ }
305
+ }
306
+ }
307
+ }
308
+
309
+ return instance;
310
+}
311
+
312
+function checkClassInstance(instance: any, ctor: any, newProps: any) {
313
+ if (__DEV__) {
314
+ const name = getComponentNameFromType(ctor) || 'Component';
315
+ const renderPresent = instance.render;
316
+
317
+ if (!renderPresent) {
318
+ if (ctor.prototype && typeof ctor.prototype.render === 'function') {
319
+ console.error(
320
+ '%s(...): No `render` method found on the returned component ' +
321
+ 'instance: did you accidentally return an object from the constructor?',
322
+ name,
323
+ );
324
+ } else {
325
+ console.error(
326
+ '%s(...): No `render` method found on the returned component ' +
327
+ 'instance: you may have forgotten to define `render`.',
328
+ name,
329
+ );
330
+ }
331
+ }
332
+
333
+ if (
334
+ instance.getInitialState &&
335
+ !instance.getInitialState.isReactClassApproved &&
336
+ !instance.state
337
+ ) {
338
+ console.error(
339
+ 'getInitialState was defined on %s, a plain JavaScript class. ' +
340
+ 'This is only supported for classes created using React.createClass. ' +
341
+ 'Did you mean to define a state property instead?',
342
+ name,
343
+ );
344
+ }
345
+ if (
346
+ instance.getDefaultProps &&
347
+ !instance.getDefaultProps.isReactClassApproved
348
+ ) {
349
+ console.error(
350
+ 'getDefaultProps was defined on %s, a plain JavaScript class. ' +
351
+ 'This is only supported for classes created using React.createClass. ' +
352
+ 'Use a static property to define defaultProps instead.',
353
+ name,
354
+ );
355
+ }
356
+ if (instance.propTypes) {
357
+ console.error(
358
+ 'propTypes was defined as an instance property on %s. Use a static ' +
359
+ 'property to define propTypes instead.',
360
+ name,
361
+ );
362
+ }
363
+ if (instance.contextType) {
364
+ console.error(
365
+ 'contextType was defined as an instance property on %s. Use a static ' +
366
+ 'property to define contextType instead.',
367
+ name,
368
+ );
369
+ }
370
+
371
+ if (disableLegacyContext) {
372
+ if (ctor.childContextTypes) {
373
+ console.error(
374
+ '%s uses the legacy childContextTypes API which is no longer supported. ' +
375
+ 'Use React.createContext() instead.',
376
+ name,
377
+ );
378
+ }
379
+ if (ctor.contextTypes) {
380
+ console.error(
381
+ '%s uses the legacy contextTypes API which is no longer supported. ' +
382
+ 'Use React.createContext() with static contextType instead.',
383
+ name,
384
+ );
385
+ }
386
+ } else {
387
+ if (instance.contextTypes) {
388
+ console.error(
389
+ 'contextTypes was defined as an instance property on %s. Use a static ' +
390
+ 'property to define contextTypes instead.',
391
+ name,
392
+ );
393
+ }
394
+
395
+ if (
396
+ ctor.contextType &&
397
+ ctor.contextTypes &&
398
+ !didWarnAboutContextTypeAndContextTypes.has(ctor)
399
+ ) {
400
+ didWarnAboutContextTypeAndContextTypes.add(ctor);
401
+ console.error(
402
+ '%s declares both contextTypes and contextType static properties. ' +
403
+ 'The legacy contextTypes property will be ignored.',
404
+ name,
405
+ );
406
+ }
407
+ }
408
+
409
+ if (typeof instance.componentShouldUpdate === 'function') {
410
+ console.error(
411
+ '%s has a method called ' +
412
+ 'componentShouldUpdate(). Did you mean shouldComponentUpdate()? ' +
413
+ 'The name is phrased as a question because the function is ' +
414
+ 'expected to return a value.',
415
+ name,
416
+ );
417
+ }
418
+ if (
419
+ ctor.prototype &&
420
+ ctor.prototype.isPureReactComponent &&
421
+ typeof instance.shouldComponentUpdate !== 'undefined'
422
+ ) {
423
+ console.error(
424
+ '%s has a method called shouldComponentUpdate(). ' +
425
+ 'shouldComponentUpdate should not be used when extending React.PureComponent. ' +
426
+ 'Please extend React.Component if shouldComponentUpdate is used.',
427
+ getComponentNameFromType(ctor) || 'A pure component',
428
+ );
429
+ }
430
+ if (typeof instance.componentDidUnmount === 'function') {
431
+ console.error(
432
+ '%s has a method called ' +
433
+ 'componentDidUnmount(). But there is no such lifecycle method. ' +
434
+ 'Did you mean componentWillUnmount()?',
435
+ name,
436
+ );
437
+ }
438
+ if (typeof instance.componentDidReceiveProps === 'function') {
439
+ console.error(
440
+ '%s has a method called ' +
441
+ 'componentDidReceiveProps(). But there is no such lifecycle method. ' +
442
+ 'If you meant to update the state in response to changing props, ' +
443
+ 'use componentWillReceiveProps(). If you meant to fetch data or ' +
444
+ 'run side-effects or mutations after React has updated the UI, use componentDidUpdate().',
445
+ name,
446
+ );
447
+ }
448
+ if (typeof instance.componentWillRecieveProps === 'function') {
449
+ console.error(
450
+ '%s has a method called ' +
451
+ 'componentWillRecieveProps(). Did you mean componentWillReceiveProps()?',
452
+ name,
453
+ );
454
+ }
455
+ if (typeof instance.UNSAFE_componentWillRecieveProps === 'function') {
456
+ console.error(
457
+ '%s has a method called ' +
458
+ 'UNSAFE_componentWillRecieveProps(). Did you mean UNSAFE_componentWillReceiveProps()?',
459
+ name,
460
+ );
461
+ }
462
+ const hasMutatedProps = instance.props !== newProps;
463
+ if (instance.props !== undefined && hasMutatedProps) {
464
+ console.error(
465
+ '%s(...): When calling super() in `%s`, make sure to pass ' +
466
+ "up the same props that your component's constructor was passed.",
467
+ name,
468
+ name,
469
+ );
470
+ }
471
+ if (instance.defaultProps) {
472
+ console.error(
473
+ 'Setting defaultProps as an instance property on %s is not supported and will be ignored.' +
474
+ ' Instead, define defaultProps as a static property on %s.',
475
+ name,
476
+ name,
477
+ );
478
+ }
479
+
480
+ if (
481
+ typeof instance.getSnapshotBeforeUpdate === 'function' &&
482
+ typeof instance.componentDidUpdate !== 'function' &&
483
+ !didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate.has(ctor)
484
+ ) {
485
+ didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate.add(ctor);
486
+ console.error(
487
+ '%s: getSnapshotBeforeUpdate() should be used with componentDidUpdate(). ' +
488
+ 'This component defines getSnapshotBeforeUpdate() only.',
489
+ getComponentNameFromType(ctor),
490
+ );
491
+ }
492
+
493
+ if (typeof instance.getDerivedStateFromProps === 'function') {
494
+ console.error(
495
+ '%s: getDerivedStateFromProps() is defined as an instance method ' +
496
+ 'and will be ignored. Instead, declare it as a static method.',
497
+ name,
498
+ );
499
+ }
500
+ if (typeof instance.getDerivedStateFromError === 'function') {
501
+ console.error(
502
+ '%s: getDerivedStateFromError() is defined as an instance method ' +
503
+ 'and will be ignored. Instead, declare it as a static method.',
504
+ name,
505
+ );
506
+ }
507
+ if (typeof ctor.getSnapshotBeforeUpdate === 'function') {
508
+ console.error(
509
+ '%s: getSnapshotBeforeUpdate() is defined as a static method ' +
510
+ 'and will be ignored. Instead, declare it as an instance method.',
511
+ name,
512
+ );
513
+ }
514
+ const state = instance.state;
515
+ if (state && (typeof state !== 'object' || isArray(state))) {
516
+ console.error('%s.state: must be set to an object or null', name);
517
+ }
518
+ if (
519
+ typeof instance.getChildContext === 'function' &&
520
+ typeof ctor.childContextTypes !== 'object'
521
+ ) {
522
+ console.error(
523
+ '%s.getChildContext(): childContextTypes must be defined in order to ' +
524
+ 'use getChildContext().',
525
+ name,
526
+ );
527
+ }
528
+ }
529
+}
530
+
531
+function callComponentWillMount(type, instance) {
532
+ const oldState = instance.state;
533
+
534
+ if (typeof instance.componentWillMount === 'function') {
535
+ instance.componentWillMount();
536
+ }
537
+ if (typeof instance.UNSAFE_componentWillMount === 'function') {
538
+ instance.UNSAFE_componentWillMount();
539
+ }
540
+
541
+ if (oldState !== instance.state) {
542
+ if (__DEV__) {
543
+ console.error(
544
+ '%s.componentWillMount(): Assigning directly to this.state is ' +
545
+ "deprecated (except inside a component's " +
546
+ 'constructor). Use setState instead.',
547
+ getComponentNameFromType(type) || 'Component',
548
+ );
549
+ }
550
+ classComponentUpdater.enqueueReplaceState(instance, instance.state, null);
551
+ }
552
+}
553
+
554
+function processUpdateQueue(
555
+ internalInstance: InternalInstance,
556
+ inst: any,
557
+ props: any,
558
+ maskedLegacyContext: any,
559
+): void {
560
+ if (internalInstance.queue !== null && internalInstance.queue.length > 0) {
561
+ const oldQueue = internalInstance.queue;
562
+ const oldReplace = internalInstance.replace;
563
+ internalInstance.queue = null;
564
+ internalInstance.replace = false;
565
+
566
+ if (oldReplace && oldQueue.length === 1) {
567
+ inst.state = oldQueue[0];
568
+ } else {
569
+ let nextState = oldReplace ? oldQueue[0] : inst.state;
570
+ let dontMutate = true;
571
+ for (let i = oldReplace ? 1 : 0; i < oldQueue.length; i++) {
572
+ const partial = oldQueue[i];
573
+ const partialState =
574
+ typeof partial === 'function'
575
+ ? partial.call(inst, nextState, props, maskedLegacyContext)
576
+ : partial;
577
+ if (partialState != null) {
578
+ if (dontMutate) {
579
+ dontMutate = false;
580
+ nextState = Object.assign({}, nextState, partialState);
581
+ } else {
582
+ Object.assign(nextState, partialState);
583
+ }
584
+ }
585
+ }
586
+ inst.state = nextState;
587
+ }
588
+ } else {
589
+ internalInstance.queue = null;
590
+ }
591
+}
592
+
593
+// Invokes the mount life-cycles on a previously never rendered instance.
594
+export function mountClassInstance(
595
+ instance: any,
596
+ ctor: any,
597
+ newProps: any,
598
+ maskedLegacyContext: any,
599
+): void {
600
+ if (__DEV__) {
601
+ checkClassInstance(instance, ctor, newProps);
602
+ }
603
+
604
+ const initialState = instance.state !== undefined ? instance.state : null;
605
+
606
+ instance.updater = classComponentUpdater;
607
+ instance.props = newProps;
608
+ instance.state = initialState;
609
+ // We don't bother initializing the refs object on the server, since we're not going to resolve them anyway.
610
+
611
+ // The internal instance will be used to manage updates that happen during this mount.
612
+ const internalInstance: InternalInstance = {
613
+ queue: null,
614
+ replace: false,
615
+ };
616
+ setInstance(instance, internalInstance);
617
+
618
+ const contextType = ctor.contextType;
619
+ if (typeof contextType === 'object' && contextType !== null) {
620
+ // TODO: Implement Context.
621
+ // instance.context = readContext(contextType);
622
+ throw new Error('Context is not yet implemented.');
623
+ } else if (disableLegacyContext) {
624
+ instance.context = emptyContextObject;
625
+ } else {
626
+ instance.context = maskedLegacyContext;
627
+ }
628
+
629
+ if (__DEV__) {
630
+ if (instance.state === newProps) {
631
+ const componentName = getComponentNameFromType(ctor) || 'Component';
632
+ if (!didWarnAboutDirectlyAssigningPropsToState.has(componentName)) {
633
+ didWarnAboutDirectlyAssigningPropsToState.add(componentName);
634
+ console.error(
635
+ '%s: It is not recommended to assign props directly to state ' +
636
+ "because updates to props won't be reflected in state. " +
637
+ 'In most cases, it is better to use props directly.',
638
+ componentName,
639
+ );
640
+ }
641
+ }
642
+ }
643
+
644
+ const getDerivedStateFromProps = ctor.getDerivedStateFromProps;
645
+ if (typeof getDerivedStateFromProps === 'function') {
646
+ instance.state = applyDerivedStateFromProps(
647
+ instance,
648
+ ctor,
649
+ getDerivedStateFromProps,
650
+ initialState,
651
+ newProps,
652
+ );
653
+ }
654
+
655
+ // In order to support react-lifecycles-compat polyfilled components,
656
+ // Unsafe lifecycles should not be invoked for components using the new APIs.
657
+ if (
658
+ typeof ctor.getDerivedStateFromProps !== 'function' &&
659
+ typeof instance.getSnapshotBeforeUpdate !== 'function' &&
660
+ (typeof instance.UNSAFE_componentWillMount === 'function' ||
661
+ typeof instance.componentWillMount === 'function')
662
+ ) {
663
+ callComponentWillMount(ctor, instance);
664
+ // If we had additional state updates during this life-cycle, let's
665
+ // process them now.
666
+ processUpdateQueue(
667
+ internalInstance,
668
+ instance,
669
+ newProps,
670
+ maskedLegacyContext,
671
+ );
672
+ }
673
+}
packages/react-server/src/ReactFizzContext.js
new
+94
@@ -0,0 +1,94 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import {disableLegacyContext} from 'shared/ReactFeatureFlags';
11
+import getComponentNameFromType from 'shared/getComponentNameFromType';
12
+import invariant from 'shared/invariant';
13
+import checkPropTypes from 'shared/checkPropTypes';
14
+
15
+let warnedAboutMissingGetChildContext;
16
+
17
+if (__DEV__) {
18
+ warnedAboutMissingGetChildContext = {};
19
+}
20
+
21
+export const emptyContextObject = {};
22
+if (__DEV__) {
23
+ Object.freeze(emptyContextObject);
24
+}
25
+
26
+export function getMaskedContext(type: any, unmaskedContext: Object): Object {
27
+ if (disableLegacyContext) {
28
+ return emptyContextObject;
29
+ } else {
30
+ const contextTypes = type.contextTypes;
31
+ if (!contextTypes) {
32
+ return emptyContextObject;
33
+ }
34
+
35
+ const context = {};
36
+ for (const key in contextTypes) {
37
+ context[key] = unmaskedContext[key];
38
+ }
39
+
40
+ if (__DEV__) {
41
+ const name = getComponentNameFromType(type) || 'Unknown';
42
+ checkPropTypes(contextTypes, context, 'context', name);
43
+ }
44
+
45
+ return context;
46
+ }
47
+}
48
+
49
+export function processChildContext(
50
+ type: any,
51
+ instance: any,
52
+ parentContext: Object,
53
+ childContextTypes: Object,
54
+): Object {
55
+ if (disableLegacyContext) {
56
+ return parentContext;
57
+ } else {
58
+ // TODO (bvaughn) Replace this behavior with an invariant() in the future.
59
+ // It has only been added in Fiber to match the (unintentional) behavior in Stack.
60
+ if (typeof instance.getChildContext !== 'function') {
61
+ if (__DEV__) {
62
+ const componentName = getComponentNameFromType(type) || 'Unknown';
63
+
64
+ if (!warnedAboutMissingGetChildContext[componentName]) {
65
+ warnedAboutMissingGetChildContext[componentName] = true;
66
+ console.error(
67
+ '%s.childContextTypes is specified but there is no getChildContext() method ' +
68
+ 'on the instance. You can either define getChildContext() on %s or remove ' +
69
+ 'childContextTypes from it.',
70
+ componentName,
71
+ componentName,
72
+ );
73
+ }
74
+ }
75
+ return parentContext;
76
+ }
77
+
78
+ const childContext = instance.getChildContext();
79
+ for (const contextKey in childContext) {
80
+ invariant(
81
+ contextKey in childContextTypes,
82
+ '%s.getChildContext(): key "%s" is not defined in childContextTypes.',
83
+ getComponentNameFromType(type) || 'Unknown',
84
+ contextKey,
85
+ );
86
+ }
87
+ if (__DEV__) {
88
+ const name = getComponentNameFromType(type) || 'Unknown';
89
+ checkPropTypes(childContextTypes, childContext, 'child context', name);
90
+ }
91
+
92
+ return {...parentContext, ...childContext};
93
+ }
94
+}
packages/react-server/src/ReactFizzServer.js
+275
-13
@@ -47,14 +47,33 @@ import {
47
createSuspenseBoundaryID,
48
getChildFormatContext,
49
} from './ReactServerFormatConfig';
50
+import {
51
+ constructClassInstance,
52
+ mountClassInstance,
53
+} from './ReactFizzClassComponent';
54
+import {
55
+ getMaskedContext,
56
+ processChildContext,
57
+ emptyContextObject,
58
+} from './ReactFizzContext';
59
import {REACT_ELEMENT_TYPE, REACT_SUSPENSE_TYPE} from 'shared/ReactSymbols';
60
import ReactSharedInternals from 'shared/ReactSharedInternals';
61
+import {
62
+ disableLegacyContext,
63
+ disableModulePatternComponents,
64
+ warnAboutDefaultPropsOnFunctionComponents,
65
+} from 'shared/ReactFeatureFlags';
66
67
+import getComponentNameFromType from 'shared/getComponentNameFromType';
68
import invariant from 'shared/invariant';
69
import isArray from 'shared/isArray';
70
71
const ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;
72
73
+type LegacyContext = {
74
+ [key: string]: any,
75
+};
76
+
77
type SuspenseBoundary = {
78
+id: SuspenseBoundaryID,
79
rootSegmentID: number,
@@ -72,6 +91,7 @@ type Task = {
91
blockedBoundary: Root | SuspenseBoundary,
92
blockedSegment: Segment, // the segment we'll write to
93
abortSet: Set<Task>, // the abortable set that this task belongs to
94
+ legacyContext: LegacyContext, // the current legacy context that this task is executing in
95
assignID: null | SuspenseBoundaryID, // id to assign to the content
96
};
97
@@ -151,7 +171,7 @@ export function createRequest(
171
children: ReactNodeList,
172
destination: Destination,
173
responseState: ResponseState,
154
- rootContext: FormatContext,
174
+ rootFormatContext: FormatContext,
175
progressiveChunkSize: number = DEFAULT_PROGRESSIVE_CHUNK_SIZE,
176
onError: (error: mixed) => void = defaultErrorHandler,
177
onCompleteAll: () => void = noop,
@@ -178,7 +198,7 @@ export function createRequest(
198
onReadyToStream,
199
};
200
// This segment represents the root fallback.
181
- const rootSegment = createPendingSegment(request, 0, null, rootContext);
201
+ const rootSegment = createPendingSegment(request, 0, null, rootFormatContext);
202
// There is no parent so conceptually, we're unblocked to flush this segment.
203
rootSegment.parentFlushed = true;
204
const rootTask = createTask(
@@ -187,6 +207,7 @@ export function createRequest(
207
null,
208
rootSegment,
209
abortSet,
210
+ emptyContextObject,
211
null,
212
);
213
pingedTasks.push(rootTask);
@@ -223,6 +244,7 @@ function createTask(
244
blockedBoundary: Root | SuspenseBoundary,
245
blockedSegment: Segment,
246
abortSet: Set<Task>,
247
+ legacyContext: LegacyContext,
248
assignID: null | SuspenseBoundaryID,
249
): Task {
250
request.allPendingTasks++;
@@ -237,6 +259,7 @@ function createTask(
259
blockedBoundary,
260
blockedSegment,
261
abortSet,
262
+ legacyContext,
263
assignID,
264
};
265
abortSet.add(task);
@@ -350,7 +373,7 @@ function renderSuspenseBoundary(
373
task.blockedSegment = parentSegment;
374
}
375
353
- // We create suspended task for the fallback because we don't want to actually task
376
+ // We create suspended task for the fallback because we don't want to actually work
377
// on it yet in case we finish the main content, so we queue for later.
378
const suspendedFallbackTask = createTask(
379
request,
@@ -358,9 +381,10 @@ function renderSuspenseBoundary(
381
parentBoundary,
382
boundarySegment,
383
fallbackAbortSet,
384
+ task.legacyContext,
385
newBoundary.id, // This is the ID we want to give this fallback so we can replace it later.
386
);
363
- // TODO: This should be queued at a separate lower priority queue so that we only task
387
+ // TODO: This should be queued at a separate lower priority queue so that we only work
388
// on preparing fallbacks if we don't have any more main content to task on.
389
request.pingedTasks.push(suspendedFallbackTask);
390
}
@@ -393,16 +417,247 @@ function renderHostElement(
417
pushEndInstance(segment.chunks, type, props);
418
}
419
396
-function renderFunctionComponent(
420
+function shouldConstruct(Component) {
421
+ return Component.prototype && Component.prototype.isReactComponent;
422
+}
423
+
424
+function renderWithHooks<Props, SecondArg>(
425
+ request: Request,
426
+ task: Task,
427
+ Component: (p: Props, arg: SecondArg) => any,
428
+ props: Props,
429
+ secondArg: SecondArg,
430
+): any {
431
+ // TODO: Set up Hooks etc.
432
+ const children = Component(props, secondArg);
433
+ return children;
434
+}
435
+
436
+function finishClassComponent(
437
request: Request,
438
task: Task,
399
- type: (props: any) => ReactNodeList,
439
+ instance: Object,
440
+ Component: any,
441
+ props: any,
442
+): ReactNodeList {
443
+ const nextChildren = instance.render();
444
+
445
+ if (__DEV__) {
446
+ if (instance.props !== props) {
447
+ if (!didWarnAboutReassigningProps) {
448
+ console.error(
449
+ 'It looks like %s is reassigning its own `this.props` while rendering. ' +
450
+ 'This is not supported and can lead to confusing bugs.',
451
+ getComponentNameFromType(Component) || 'a component',
452
+ );
453
+ }
454
+ didWarnAboutReassigningProps = true;
455
+ }
456
+ }
457
+
458
+ if (!disableLegacyContext) {
459
+ const childContextTypes = Component.childContextTypes;
460
+ if (childContextTypes !== null && childContextTypes !== undefined) {
461
+ const previousContext = task.legacyContext;
462
+ const mergedContext = processChildContext(
463
+ instance,
464
+ Component,
465
+ previousContext,
466
+ childContextTypes,
467
+ );
468
+ task.legacyContext = mergedContext;
469
+ renderNodeDestructive(request, task, nextChildren);
470
+ task.legacyContext = previousContext;
471
+ return;
472
+ }
473
+ }
474
+
475
+ renderNodeDestructive(request, task, nextChildren);
476
+}
477
+
478
+function renderClassComponent(
479
+ request: Request,
480
+ task: Task,
481
+ Component: any,
482
props: any,
483
): void {
402
- const result = type(props);
403
- // We're now successfully past this task, and we don't have to pop back to
404
- // the previous task every again, so we can use the destructive recursive form.
405
- renderNodeDestructive(request, task, result);
484
+ const unmaskedContext = !disableLegacyContext
485
+ ? task.legacyContext
486
+ : undefined;
487
+ const instance = constructClassInstance(Component, props, unmaskedContext);
488
+ mountClassInstance(instance, Component, props, unmaskedContext);
489
+ finishClassComponent(request, task, Component);
490
+}
491
+
492
+const didWarnAboutBadClass = {};
493
+const didWarnAboutModulePatternComponent = {};
494
+const didWarnAboutContextTypeOnFunctionComponent = {};
495
+const didWarnAboutGetDerivedStateOnFunctionComponent = {};
496
+let didWarnAboutReassigningProps = false;
497
+const didWarnAboutDefaultPropsOnFunctionComponent = {};
498
+
499
+// This would typically be a function component but we still support module pattern
500
+// components for some reason.
501
+function renderIndeterminateComponent(
502
+ request: Request,
503
+ task: Task,
504
+ Component: any,
505
+ props: any,
506
+): void {
507
+ let legacyContext;
508
+ if (!disableLegacyContext) {
509
+ legacyContext = getMaskedContext(Component, task.legacyContext);
510
+ }
511
+
512
+ if (__DEV__) {
513
+ if (
514
+ Component.prototype &&
515
+ typeof Component.prototype.render === 'function'
516
+ ) {
517
+ const componentName = getComponentNameFromType(Component) || 'Unknown';
518
+
519
+ if (!didWarnAboutBadClass[componentName]) {
520
+ console.error(
521
+ "The <%s /> component appears to have a render method, but doesn't extend React.Component. " +
522
+ 'This is likely to cause errors. Change %s to extend React.Component instead.',
523
+ componentName,
524
+ componentName,
525
+ );
526
+ didWarnAboutBadClass[componentName] = true;
527
+ }
528
+ }
529
+ }
530
+
531
+ const value = renderWithHooks(request, task, Component, props, legacyContext);
532
+
533
+ if (__DEV__) {
534
+ // Support for module components is deprecated and is removed behind a flag.
535
+ // Whether or not it would crash later, we want to show a good message in DEV first.
536
+ if (
537
+ typeof value === 'object' &&
538
+ value !== null &&
539
+ typeof value.render === 'function' &&
540
+ value.$$typeof === undefined
541
+ ) {
542
+ const componentName = getComponentNameFromType(Component) || 'Unknown';
543
+ if (!didWarnAboutModulePatternComponent[componentName]) {
544
+ console.error(
545
+ 'The <%s /> component appears to be a function component that returns a class instance. ' +
546
+ 'Change %s to a class that extends React.Component instead. ' +
547
+ "If you can't use a class try assigning the prototype on the function as a workaround. " +
548
+ "`%s.prototype = React.Component.prototype`. Don't use an arrow function since it " +
549
+ 'cannot be called with `new` by React.',
550
+ componentName,
551
+ componentName,
552
+ componentName,
553
+ );
554
+ didWarnAboutModulePatternComponent[componentName] = true;
555
+ }
556
+ }
557
+ }
558
+
559
+ if (
560
+ // Run these checks in production only if the flag is off.
561
+ // Eventually we'll delete this branch altogether.
562
+ !disableModulePatternComponents &&
563
+ typeof value === 'object' &&
564
+ value !== null &&
565
+ typeof value.render === 'function' &&
566
+ value.$$typeof === undefined
567
+ ) {
568
+ if (__DEV__) {
569
+ const componentName = getComponentNameFromType(Component) || 'Unknown';
570
+ if (!didWarnAboutModulePatternComponent[componentName]) {
571
+ console.error(
572
+ 'The <%s /> component appears to be a function component that returns a class instance. ' +
573
+ 'Change %s to a class that extends React.Component instead. ' +
574
+ "If you can't use a class try assigning the prototype on the function as a workaround. " +
575
+ "`%s.prototype = React.Component.prototype`. Don't use an arrow function since it " +
576
+ 'cannot be called with `new` by React.',
577
+ componentName,
578
+ componentName,
579
+ componentName,
580
+ );
581
+ didWarnAboutModulePatternComponent[componentName] = true;
582
+ }
583
+ }
584
+
585
+ mountClassInstance(value, Component, props, legacyContext);
586
+ finishClassComponent(request, task, value, Component);
587
+ } else {
588
+ // Proceed under the assumption that this is a function component
589
+ if (__DEV__) {
590
+ if (disableLegacyContext && Component.contextTypes) {
591
+ console.error(
592
+ '%s uses the legacy contextTypes API which is no longer supported. ' +
593
+ 'Use React.createContext() with React.useContext() instead.',
594
+ getComponentNameFromType(Component) || 'Unknown',
595
+ );
596
+ }
597
+ }
598
+ if (__DEV__) {
599
+ validateFunctionComponentInDev(Component);
600
+ }
601
+ // We're now successfully past this task, and we don't have to pop back to
602
+ // the previous task every again, so we can use the destructive recursive form.
603
+ renderNodeDestructive(request, task, value);
604
+ }
605
+}
606
+
607
+function validateFunctionComponentInDev(Component: any): void {
608
+ if (__DEV__) {
609
+ if (Component) {
610
+ if (Component.childContextTypes) {
611
+ console.error(
612
+ '%s(...): childContextTypes cannot be defined on a function component.',
613
+ Component.displayName || Component.name || 'Component',
614
+ );
615
+ }
616
+ }
617
+
618
+ if (
619
+ warnAboutDefaultPropsOnFunctionComponents &&
620
+ Component.defaultProps !== undefined
621
+ ) {
622
+ const componentName = getComponentNameFromType(Component) || 'Unknown';
623
+
624
+ if (!didWarnAboutDefaultPropsOnFunctionComponent[componentName]) {
625
+ console.error(
626
+ '%s: Support for defaultProps will be removed from function components ' +
627
+ 'in a future major release. Use JavaScript default parameters instead.',
628
+ componentName,
629
+ );
630
+ didWarnAboutDefaultPropsOnFunctionComponent[componentName] = true;
631
+ }
632
+ }
633
+
634
+ if (typeof Component.getDerivedStateFromProps === 'function') {
635
+ const componentName = getComponentNameFromType(Component) || 'Unknown';
636
+
637
+ if (!didWarnAboutGetDerivedStateOnFunctionComponent[componentName]) {
638
+ console.error(
639
+ '%s: Function components do not support getDerivedStateFromProps.',
640
+ componentName,
641
+ );
642
+ didWarnAboutGetDerivedStateOnFunctionComponent[componentName] = true;
643
+ }
644
+ }
645
+
646
+ if (
647
+ typeof Component.contextType === 'object' &&
648
+ Component.contextType !== null
649
+ ) {
650
+ const componentName = getComponentNameFromType(Component) || 'Unknown';
651
+
652
+ if (!didWarnAboutContextTypeOnFunctionComponent[componentName]) {
653
+ console.error(
654
+ '%s: Function components do not support contextType.',
655
+ componentName,
656
+ );
657
+ didWarnAboutContextTypeOnFunctionComponent[componentName] = true;
658
+ }
659
+ }
660
+ }
661
}
662
663
function renderElement(
@@ -413,7 +668,11 @@ function renderElement(
668
node: ReactNodeList,
669
): void {
670
if (typeof type === 'function') {
416
- renderFunctionComponent(request, task, type, props);
671
+ if (shouldConstruct(type)) {
672
+ renderClassComponent(request, task, type, props);
673
+ } else {
674
+ renderIndeterminateComponent(request, task, type, props);
675
+ }
676
} else if (typeof type === 'string') {
677
renderHostElement(request, task, type, props);
678
} else if (type === REACT_SUSPENSE_TYPE) {
@@ -505,6 +764,7 @@ function spawnNewSuspendedTask(
764
task.blockedBoundary,
765
newSegment,
766
task.abortSet,
767
+ task.legacyContext,
768
task.assignID,
769
);
770
// We've delegated the assignment.
@@ -521,7 +781,8 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
781
782
// Snapshot the current context in case something throws to interrupt the
783
// process.
524
- const previousContext = task.blockedSegment.formatContext;
784
+ const previousFormatContext = task.blockedSegment.formatContext;
785
+ const previousLegacyContext = task.legacyContext;
786
try {
787
return renderNodeDestructive(request, task, node);
788
} catch (x) {
@@ -529,7 +790,8 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
790
spawnNewSuspendedTask(request, task, x);
791
// Restore the context. We assume that this will be restored by the inner
792
// functions in case nothing throws so we don't use "finally" here.
532
- task.blockedSegment.formatContext = previousContext;
793
+ task.blockedSegment.formatContext = previousFormatContext;
794
+ task.legacyContext = previousLegacyContext;
795
} else {
796
// We assume that we don't need the correct context.
797
// Let's terminate the rest of the tree and don't render any siblings.