@samitouri / QOS-React / commits / ba6477aa3c

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} {