Improve Reducer Hook's lazy init API (#14723)
* Improve Reducer Hook's lazy init API * Use generic type for initilizer input Still requires an `any` cast in the case where `init` function is not provided.
Andrew Clark committed
Jan 29, 2019 at 17:39 UTC
ba6477aa3cc0b189f4c9b805bf20edb2a2d94e91
9 files changed
+93
-93
packages/react-debug-tools/src/ReactDebugHooks.js
+9
-4
@@ -115,13 +115,18 @@ function useState<S>(
115
return [state, (action: BasicStateAction<S>) => {}];
116
}
117
118
-function useReducer<S, A>(
118
+function useReducer<S, I, A>(
119
reducer: (S, A) => S,
120
- initialState: S,
121
- initialAction: A | void | null,
120
+ initialArg: I,
121
+ init?: I => S,
122
): [S, Dispatch<A>] {
123
let hook = nextHook();
124
- let state = hook !== null ? hook.memoizedState : initialState;
124
+ let state;
125
+ if (hook !== null) {
126
+ state = hook.memoizedState;
127
+ } else {
128
+ state = init !== undefined ? init(initialArg) : ((initialArg: any): S);
129
+ }
130
hookLog.push({
131
primitive: 'Reducer',
132
stackError: new Error(),
packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.internal.js
+2
-2
@@ -208,12 +208,12 @@ describe('ReactDOMServerHooks', () => {
208
expect(domNode.textContent).toEqual('0');
209
});
210
211
- itRenders('lazy initialization with initialAction', async render => {
211
+ itRenders('lazy initialization', async render => {
212
function reducer(state, action) {
213
return action === 'increment' ? state + 1 : state;
214
}
215
function Counter() {
216
- let [count] = useReducer(reducer, 0, 'increment');
216
+ let [count] = useReducer(reducer, 0, c => c + 1);
217
yieldValue('Render: ' + count);
218
return <Text text={count} />;
219
}
packages/react-dom/src/server/ReactPartialRendererHooks.js
+11
-8
@@ -252,10 +252,10 @@ export function useState<S>(
252
);
253
}
254
255
-export function useReducer<S, A>(
255
+export function useReducer<S, I, A>(
256
reducer: (S, A) => S,
257
- initialState: S,
258
- initialAction: A | void | null,
257
+ initialArg: I,
258
+ init?: I => S,
259
): [S, Dispatch<A>] {
260
if (__DEV__) {
261
if (reducer !== basicStateReducer) {
@@ -301,13 +301,16 @@ export function useReducer<S, A>(
301
if (__DEV__) {
302
isInHookUserCodeInDev = true;
303
}
304
+ let initialState;
305
if (reducer === basicStateReducer) {
306
// Special case for `useState`.
306
- if (typeof initialState === 'function') {
307
- initialState = initialState();
308
- }
309
- } else if (initialAction !== undefined && initialAction !== null) {
310
- initialState = reducer(initialState, initialAction);
307
+ initialState =
308
+ typeof initialArg === 'function'
309
+ ? ((initialArg: any): () => S)()
310
+ : ((initialArg: any): S);
311
+ } else {
312
+ initialState =
313
+ init !== undefined ? init(initialArg) : ((initialArg: any): S);
314
}
315
if (__DEV__) {
316
isInHookUserCodeInDev = false;
packages/react-reconciler/src/ReactFiberHooks.js
+30
-30
@@ -49,10 +49,10 @@ export type Dispatcher = {
49
observedBits: void | number | boolean,
50
): T,
51
useState<S>(initialState: (() => S) | S): [S, Dispatch<BasicStateAction<S>>],
52
- useReducer<S, A>(
52
+ useReducer<S, I, A>(
53
reducer: (S, A) => S,
54
- initialState: S,
55
- initialAction: A | void | null,
54
+ initialArg: I,
55
+ init?: (I) => S,
56
): [S, Dispatch<A>],
57
useContext<T>(
58
context: ReactContext<T>,
@@ -591,16 +591,17 @@ function updateContext<T>(
591
return readContext(context, observedBits);
592
}
593
594
-function mountReducer<S, A>(
594
+function mountReducer<S, I, A>(
595
reducer: (S, A) => S,
596
- initialState: void | S,
597
- initialAction: void | null | A,
596
+ initialArg: I,
597
+ init?: I => S,
598
): [S, Dispatch<A>] {
599
const hook = mountWorkInProgressHook();
600
- // TODO: Lazy init API will change before release.
601
- if (initialAction !== undefined && initialAction !== null) {
602
- // $FlowFixMe - Must express with overloading.
603
- initialState = reducer(initialState, initialAction);
600
+ let initialState;
601
+ if (init !== undefined) {
602
+ initialState = init(initialArg);
603
+ } else {
604
+ initialState = ((initialArg: any): S);
605
}
606
hook.memoizedState = hook.baseState = initialState;
607
const queue = (hook.queue = {
@@ -618,10 +619,10 @@ function mountReducer<S, A>(
619
return [hook.memoizedState, dispatch];
620
}
621
621
-function updateReducer<S, A>(
622
+function updateReducer<S, I, A>(
623
reducer: (S, A) => S,
623
- initialState: void | S,
624
- initialAction: void | null | A,
624
+ initialArg: I,
625
+ init?: I => S,
626
): [S, Dispatch<A>] {
627
const hook = updateWorkInProgressHook();
628
const queue = hook.queue;
@@ -755,7 +756,6 @@ function mountState<S>(
756
initialState: (() => S) | S,
757
): [S, Dispatch<BasicStateAction<S>>] {
758
const hook = mountWorkInProgressHook();
758
- // TODO: Lazy init API will change before release.
759
if (typeof initialState === 'function') {
760
initialState = initialState();
761
}
@@ -1282,16 +1282,16 @@ if (__DEV__) {
1282
ReactCurrentDispatcher.current = prevDispatcher;
1283
}
1284
},
1285
- useReducer<S, A>(
1285
+ useReducer<S, I, A>(
1286
reducer: (S, A) => S,
1287
- initialState: S,
1288
- initialAction: A | void | null,
1287
+ initialArg: I,
1288
+ init?: I => S,
1289
): [S, Dispatch<A>] {
1290
currentHookNameInDev = 'useReducer';
1291
const prevDispatcher = ReactCurrentDispatcher.current;
1292
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
1293
try {
1294
- return mountReducer(reducer, initialState, initialAction);
1294
+ return mountReducer(reducer, initialArg, init);
1295
} finally {
1296
ReactCurrentDispatcher.current = prevDispatcher;
1297
}
@@ -1366,16 +1366,16 @@ if (__DEV__) {
1366
ReactCurrentDispatcher.current = prevDispatcher;
1367
}
1368
},
1369
- useReducer<S, A>(
1369
+ useReducer<S, I, A>(
1370
reducer: (S, A) => S,
1371
- initialState: S,
1372
- initialAction: A | void | null,
1371
+ initialArg: I,
1372
+ init?: I => S,
1373
): [S, Dispatch<A>] {
1374
currentHookNameInDev = 'useReducer';
1375
const prevDispatcher = ReactCurrentDispatcher.current;
1376
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
1377
try {
1378
- return updateReducer(reducer, initialState, initialAction);
1378
+ return updateReducer(reducer, initialArg, init);
1379
} finally {
1380
ReactCurrentDispatcher.current = prevDispatcher;
1381
}
@@ -1457,17 +1457,17 @@ if (__DEV__) {
1457
ReactCurrentDispatcher.current = prevDispatcher;
1458
}
1459
},
1460
- useReducer<S, A>(
1460
+ useReducer<S, I, A>(
1461
reducer: (S, A) => S,
1462
- initialState: S,
1463
- initialAction: A | void | null,
1462
+ initialArg: I,
1463
+ init?: I => S,
1464
): [S, Dispatch<A>] {
1465
currentHookNameInDev = 'useReducer';
1466
warnInvalidHookAccess();
1467
const prevDispatcher = ReactCurrentDispatcher.current;
1468
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
1469
try {
1470
- return mountReducer(reducer, initialState, initialAction);
1470
+ return mountReducer(reducer, initialArg, init);
1471
} finally {
1472
ReactCurrentDispatcher.current = prevDispatcher;
1473
}
@@ -1552,17 +1552,17 @@ if (__DEV__) {
1552
ReactCurrentDispatcher.current = prevDispatcher;
1553
}
1554
},
1555
- useReducer<S, A>(
1555
+ useReducer<S, I, A>(
1556
reducer: (S, A) => S,
1557
- initialState: S,
1558
- initialAction: A | void | null,
1557
+ initialArg: I,
1558
+ init?: I => S,
1559
): [S, Dispatch<A>] {
1560
currentHookNameInDev = 'useReducer';
1561
warnInvalidHookAccess();
1562
const prevDispatcher = ReactCurrentDispatcher.current;
1563
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
1564
try {
1565
- return updateReducer(reducer, initialState, initialAction);
1565
+ return updateReducer(reducer, initialArg, init);
1566
} finally {
1567
ReactCurrentDispatcher.current = prevDispatcher;
1568
}
packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js
+9
-9
@@ -752,19 +752,19 @@ describe('ReactHooks', () => {
752
753
const ThemeContext = createContext('light');
754
function App() {
755
- useReducer(
756
- () => {
757
- ReactCurrentDispatcher.current.readContext(ThemeContext);
758
- },
759
- null,
760
- {},
761
- );
755
+ const [state, dispatch] = useReducer((s, action) => {
756
+ ReactCurrentDispatcher.current.readContext(ThemeContext);
757
+ return action;
758
+ }, 0);
759
+ if (state === 0) {
760
+ dispatch(1);
761
+ }
762
return null;
763
}
764
765
- expect(() => ReactTestRenderer.create(<App />)).toWarnDev(
765
+ expect(() => ReactTestRenderer.create(<App />)).toWarnDev([
766
'Context can only be read while React is rendering',
767
- );
767
+ ]);
768
});
769
770
// Edge case.
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js
+9
-10
@@ -524,14 +524,12 @@ describe('ReactHooksWithNoopRenderer', () => {
524
expect(ReactNoop.getChildren()).toEqual([span('Count: -2')]);
525
});
526
527
- it('accepts an initial action', () => {
527
+ it('lazy init', () => {
528
const INCREMENT = 'INCREMENT';
529
const DECREMENT = 'DECREMENT';
530
531
function reducer(state, action) {
532
switch (action) {
533
- case 'INITIALIZE':
534
- return 10;
533
case 'INCREMENT':
534
return state + 1;
535
case 'DECREMENT':
@@ -541,27 +539,28 @@ describe('ReactHooksWithNoopRenderer', () => {
539
}
540
}
541
544
- const initialAction = 'INITIALIZE';
545
-
542
function Counter(props, ref) {
547
- const [count, dispatch] = useReducer(reducer, 0, initialAction);
543
+ const [count, dispatch] = useReducer(reducer, props, p => {
544
+ ReactNoop.yield('Init');
545
+ return p.initialCount;
546
+ });
547
useImperativeHandle(ref, () => ({dispatch}));
548
return <Text text={'Count: ' + count} />;
549
}
550
Counter = forwardRef(Counter);
551
const counter = React.createRef(null);
553
- ReactNoop.render(<Counter ref={counter} />);
554
- ReactNoop.flush();
552
+ ReactNoop.render(<Counter initialCount={10} ref={counter} />);
553
+ expect(ReactNoop.flush()).toEqual(['Init', 'Count: 10']);
554
expect(ReactNoop.getChildren()).toEqual([span('Count: 10')]);
555
556
counter.current.dispatch(INCREMENT);
558
- ReactNoop.flush();
557
+ expect(ReactNoop.flush()).toEqual(['Count: 11']);
558
expect(ReactNoop.getChildren()).toEqual([span('Count: 11')]);
559
560
counter.current.dispatch(DECREMENT);
561
counter.current.dispatch(DECREMENT);
562
counter.current.dispatch(DECREMENT);
564
- ReactNoop.flush();
563
+ expect(ReactNoop.flush()).toEqual(['Count: 8']);
564
expect(ReactNoop.getChildren()).toEqual([span('Count: 8')]);
565
});
566
packages/react-test-renderer/src/ReactShallowRenderer.js
+11
-8
@@ -223,10 +223,10 @@ class ReactShallowRenderer {
223
}
224
225
_createDispatcher(): DispatcherType {
226
- const useReducer = <S, A>(
226
+ const useReducer = <S, I, A>(
227
reducer: (S, A) => S,
228
- initialState: S,
229
- initialAction: A | void | null,
228
+ initialArg: I,
229
+ init?: I => S,
230
): [S, Dispatch<A>] => {
231
this._validateCurrentlyRenderingComponent();
232
this._createWorkInProgressHook();
@@ -259,13 +259,16 @@ class ReactShallowRenderer {
259
}
260
return [workInProgressHook.memoizedState, dispatch];
261
} else {
262
+ let initialState;
263
if (reducer === basicStateReducer) {
264
// Special case for `useState`.
264
- if (typeof initialState === 'function') {
265
- initialState = initialState();
266
- }
267
- } else if (initialAction !== undefined && initialAction !== null) {
268
- initialState = reducer(initialState, initialAction);
265
+ initialState =
266
+ typeof initialArg === 'function'
267
+ ? ((initialArg: any): () => S)()
268
+ : ((initialArg: any): S);
269
+ } else {
270
+ initialState =
271
+ init !== undefined ? init(initialArg) : ((initialArg: any): S);
272
}
273
workInProgressHook.memoizedState = initialState;
274
const queue: UpdateQueue<A> = (workInProgressHook.queue = {
packages/react-test-renderer/src/__tests__/ReactShallowRendererHooks-test.js
+8
-18
@@ -91,23 +91,19 @@ describe('ReactShallowRenderer with hooks', () => {
91
});
92
93
it('should work with useReducer', () => {
94
- const initialState = {count: 0};
95
-
94
function reducer(state, action) {
95
switch (action.type) {
98
- case 'reset':
99
- return initialState;
96
case 'increment':
97
return {count: state.count + 1};
98
case 'decrement':
99
return {count: state.count - 1};
104
- default:
105
- return state;
100
}
101
}
102
109
- function SomeComponent({initialCount}) {
110
- const [state] = React.useReducer(reducer, {count: initialCount});
103
+ function SomeComponent(props) {
104
+ const [state] = React.useReducer(reducer, props, p => ({
105
+ count: p.initialCount,
106
+ }));
107
108
return (
109
<div>
@@ -141,25 +137,19 @@ describe('ReactShallowRenderer with hooks', () => {
137
});
138
139
it('should work with a dispatched state change for a useReducer', () => {
144
- const initialState = {count: 0};
145
-
140
function reducer(state, action) {
141
switch (action.type) {
148
- case 'reset':
149
- return initialState;
142
case 'increment':
143
return {count: state.count + 1};
144
case 'decrement':
145
return {count: state.count - 1};
154
- default:
155
- return state;
146
}
147
}
148
159
- function SomeComponent({initialCount}) {
160
- const [state, dispatch] = React.useReducer(reducer, {
161
- count: initialCount,
162
- });
149
+ function SomeComponent(props) {
150
+ const [state, dispatch] = React.useReducer(reducer, props, p => ({
151
+ count: p.initialCount,
152
+ }));
153
154
if (state.count === 0) {
155
dispatch({type: 'increment'});
packages/react/src/ReactHooks.js
+4
-4
@@ -56,13 +56,13 @@ export function useState<S>(initialState: (() => S) | S) {
56
return dispatcher.useState(initialState);
57
}
58
59
-export function useReducer<S, A>(
59
+export function useReducer<S, I, A>(
60
reducer: (S, A) => S,
61
- initialState: S,
62
- initialAction: A | void | null,
61
+ initialArg: I,
62
+ init?: I => S,
63
) {
64
const dispatcher = resolveDispatcher();
65
- return dispatcher.useReducer(reducer, initialState, initialAction);
65
+ return dispatcher.useReducer(reducer, initialArg, init);
66
}
67
68
export function useRef<T>(initialValue: T): {current: T} {