Skip to content

Commit ba6477a

Browse files
authored
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.
1 parent cb1ff43 commit ba6477a

File tree

9 files changed

+93
-93
lines changed

9 files changed

+93
-93
lines changed

packages/react-debug-tools/src/ReactDebugHooks.js

+9-4
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,18 @@ function useState<S>(
115115
return [state, (action: BasicStateAction<S>) => {}];
116116
}
117117

118-
function useReducer<S, A>(
118+
function useReducer<S, I, A>(
119119
reducer: (S, A) => S,
120-
initialState: S,
121-
initialAction: A | void | null,
120+
initialArg: I,
121+
init?: I => S,
122122
): [S, Dispatch<A>] {
123123
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+
}
125130
hookLog.push({
126131
primitive: 'Reducer',
127132
stackError: new Error(),

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -208,12 +208,12 @@ describe('ReactDOMServerHooks', () => {
208208
expect(domNode.textContent).toEqual('0');
209209
});
210210

211-
itRenders('lazy initialization with initialAction', async render => {
211+
itRenders('lazy initialization', async render => {
212212
function reducer(state, action) {
213213
return action === 'increment' ? state + 1 : state;
214214
}
215215
function Counter() {
216-
let [count] = useReducer(reducer, 0, 'increment');
216+
let [count] = useReducer(reducer, 0, c => c + 1);
217217
yieldValue('Render: ' + count);
218218
return <Text text={count} />;
219219
}

packages/react-dom/src/server/ReactPartialRendererHooks.js

+11-8
Original file line numberDiff line numberDiff line change
@@ -252,10 +252,10 @@ export function useState<S>(
252252
);
253253
}
254254

255-
export function useReducer<S, A>(
255+
export function useReducer<S, I, A>(
256256
reducer: (S, A) => S,
257-
initialState: S,
258-
initialAction: A | void | null,
257+
initialArg: I,
258+
init?: I => S,
259259
): [S, Dispatch<A>] {
260260
if (__DEV__) {
261261
if (reducer !== basicStateReducer) {
@@ -301,13 +301,16 @@ export function useReducer<S, A>(
301301
if (__DEV__) {
302302
isInHookUserCodeInDev = true;
303303
}
304+
let initialState;
304305
if (reducer === basicStateReducer) {
305306
// 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);
311314
}
312315
if (__DEV__) {
313316
isInHookUserCodeInDev = false;

packages/react-reconciler/src/ReactFiberHooks.js

+30-30
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,10 @@ export type Dispatcher = {
4949
observedBits: void | number | boolean,
5050
): T,
5151
useState<S>(initialState: (() => S) | S): [S, Dispatch<BasicStateAction<S>>],
52-
useReducer<S, A>(
52+
useReducer<S, I, A>(
5353
reducer: (S, A) => S,
54-
initialState: S,
55-
initialAction: A | void | null,
54+
initialArg: I,
55+
init?: (I) => S,
5656
): [S, Dispatch<A>],
5757
useContext<T>(
5858
context: ReactContext<T>,
@@ -591,16 +591,17 @@ function updateContext<T>(
591591
return readContext(context, observedBits);
592592
}
593593

594-
function mountReducer<S, A>(
594+
function mountReducer<S, I, A>(
595595
reducer: (S, A) => S,
596-
initialState: void | S,
597-
initialAction: void | null | A,
596+
initialArg: I,
597+
init?: I => S,
598598
): [S, Dispatch<A>] {
599599
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);
604605
}
605606
hook.memoizedState = hook.baseState = initialState;
606607
const queue = (hook.queue = {
@@ -618,10 +619,10 @@ function mountReducer<S, A>(
618619
return [hook.memoizedState, dispatch];
619620
}
620621

621-
function updateReducer<S, A>(
622+
function updateReducer<S, I, A>(
622623
reducer: (S, A) => S,
623-
initialState: void | S,
624-
initialAction: void | null | A,
624+
initialArg: I,
625+
init?: I => S,
625626
): [S, Dispatch<A>] {
626627
const hook = updateWorkInProgressHook();
627628
const queue = hook.queue;
@@ -755,7 +756,6 @@ function mountState<S>(
755756
initialState: (() => S) | S,
756757
): [S, Dispatch<BasicStateAction<S>>] {
757758
const hook = mountWorkInProgressHook();
758-
// TODO: Lazy init API will change before release.
759759
if (typeof initialState === 'function') {
760760
initialState = initialState();
761761
}
@@ -1282,16 +1282,16 @@ if (__DEV__) {
12821282
ReactCurrentDispatcher.current = prevDispatcher;
12831283
}
12841284
},
1285-
useReducer<S, A>(
1285+
useReducer<S, I, A>(
12861286
reducer: (S, A) => S,
1287-
initialState: S,
1288-
initialAction: A | void | null,
1287+
initialArg: I,
1288+
init?: I => S,
12891289
): [S, Dispatch<A>] {
12901290
currentHookNameInDev = 'useReducer';
12911291
const prevDispatcher = ReactCurrentDispatcher.current;
12921292
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
12931293
try {
1294-
return mountReducer(reducer, initialState, initialAction);
1294+
return mountReducer(reducer, initialArg, init);
12951295
} finally {
12961296
ReactCurrentDispatcher.current = prevDispatcher;
12971297
}
@@ -1366,16 +1366,16 @@ if (__DEV__) {
13661366
ReactCurrentDispatcher.current = prevDispatcher;
13671367
}
13681368
},
1369-
useReducer<S, A>(
1369+
useReducer<S, I, A>(
13701370
reducer: (S, A) => S,
1371-
initialState: S,
1372-
initialAction: A | void | null,
1371+
initialArg: I,
1372+
init?: I => S,
13731373
): [S, Dispatch<A>] {
13741374
currentHookNameInDev = 'useReducer';
13751375
const prevDispatcher = ReactCurrentDispatcher.current;
13761376
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
13771377
try {
1378-
return updateReducer(reducer, initialState, initialAction);
1378+
return updateReducer(reducer, initialArg, init);
13791379
} finally {
13801380
ReactCurrentDispatcher.current = prevDispatcher;
13811381
}
@@ -1457,17 +1457,17 @@ if (__DEV__) {
14571457
ReactCurrentDispatcher.current = prevDispatcher;
14581458
}
14591459
},
1460-
useReducer<S, A>(
1460+
useReducer<S, I, A>(
14611461
reducer: (S, A) => S,
1462-
initialState: S,
1463-
initialAction: A | void | null,
1462+
initialArg: I,
1463+
init?: I => S,
14641464
): [S, Dispatch<A>] {
14651465
currentHookNameInDev = 'useReducer';
14661466
warnInvalidHookAccess();
14671467
const prevDispatcher = ReactCurrentDispatcher.current;
14681468
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
14691469
try {
1470-
return mountReducer(reducer, initialState, initialAction);
1470+
return mountReducer(reducer, initialArg, init);
14711471
} finally {
14721472
ReactCurrentDispatcher.current = prevDispatcher;
14731473
}
@@ -1552,17 +1552,17 @@ if (__DEV__) {
15521552
ReactCurrentDispatcher.current = prevDispatcher;
15531553
}
15541554
},
1555-
useReducer<S, A>(
1555+
useReducer<S, I, A>(
15561556
reducer: (S, A) => S,
1557-
initialState: S,
1558-
initialAction: A | void | null,
1557+
initialArg: I,
1558+
init?: I => S,
15591559
): [S, Dispatch<A>] {
15601560
currentHookNameInDev = 'useReducer';
15611561
warnInvalidHookAccess();
15621562
const prevDispatcher = ReactCurrentDispatcher.current;
15631563
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
15641564
try {
1565-
return updateReducer(reducer, initialState, initialAction);
1565+
return updateReducer(reducer, initialArg, init);
15661566
} finally {
15671567
ReactCurrentDispatcher.current = prevDispatcher;
15681568
}

packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js

+9-9
Original file line numberDiff line numberDiff line change
@@ -752,19 +752,19 @@ describe('ReactHooks', () => {
752752

753753
const ThemeContext = createContext('light');
754754
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+
}
762762
return null;
763763
}
764764

765-
expect(() => ReactTestRenderer.create(<App />)).toWarnDev(
765+
expect(() => ReactTestRenderer.create(<App />)).toWarnDev([
766766
'Context can only be read while React is rendering',
767-
);
767+
]);
768768
});
769769

770770
// Edge case.

packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js

+9-10
Original file line numberDiff line numberDiff line change
@@ -524,14 +524,12 @@ describe('ReactHooksWithNoopRenderer', () => {
524524
expect(ReactNoop.getChildren()).toEqual([span('Count: -2')]);
525525
});
526526

527-
it('accepts an initial action', () => {
527+
it('lazy init', () => {
528528
const INCREMENT = 'INCREMENT';
529529
const DECREMENT = 'DECREMENT';
530530

531531
function reducer(state, action) {
532532
switch (action) {
533-
case 'INITIALIZE':
534-
return 10;
535533
case 'INCREMENT':
536534
return state + 1;
537535
case 'DECREMENT':
@@ -541,27 +539,28 @@ describe('ReactHooksWithNoopRenderer', () => {
541539
}
542540
}
543541

544-
const initialAction = 'INITIALIZE';
545-
546542
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+
});
548547
useImperativeHandle(ref, () => ({dispatch}));
549548
return <Text text={'Count: ' + count} />;
550549
}
551550
Counter = forwardRef(Counter);
552551
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']);
555554
expect(ReactNoop.getChildren()).toEqual([span('Count: 10')]);
556555

557556
counter.current.dispatch(INCREMENT);
558-
ReactNoop.flush();
557+
expect(ReactNoop.flush()).toEqual(['Count: 11']);
559558
expect(ReactNoop.getChildren()).toEqual([span('Count: 11')]);
560559

561560
counter.current.dispatch(DECREMENT);
562561
counter.current.dispatch(DECREMENT);
563562
counter.current.dispatch(DECREMENT);
564-
ReactNoop.flush();
563+
expect(ReactNoop.flush()).toEqual(['Count: 8']);
565564
expect(ReactNoop.getChildren()).toEqual([span('Count: 8')]);
566565
});
567566

packages/react-test-renderer/src/ReactShallowRenderer.js

+11-8
Original file line numberDiff line numberDiff line change
@@ -223,10 +223,10 @@ class ReactShallowRenderer {
223223
}
224224

225225
_createDispatcher(): DispatcherType {
226-
const useReducer = <S, A>(
226+
const useReducer = <S, I, A>(
227227
reducer: (S, A) => S,
228-
initialState: S,
229-
initialAction: A | void | null,
228+
initialArg: I,
229+
init?: I => S,
230230
): [S, Dispatch<A>] => {
231231
this._validateCurrentlyRenderingComponent();
232232
this._createWorkInProgressHook();
@@ -259,13 +259,16 @@ class ReactShallowRenderer {
259259
}
260260
return [workInProgressHook.memoizedState, dispatch];
261261
} else {
262+
let initialState;
262263
if (reducer === basicStateReducer) {
263264
// 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);
269272
}
270273
workInProgressHook.memoizedState = initialState;
271274
const queue: UpdateQueue<A> = (workInProgressHook.queue = {

packages/react-test-renderer/src/__tests__/ReactShallowRendererHooks-test.js

+8-18
Original file line numberDiff line numberDiff line change
@@ -91,23 +91,19 @@ describe('ReactShallowRenderer with hooks', () => {
9191
});
9292

9393
it('should work with useReducer', () => {
94-
const initialState = {count: 0};
95-
9694
function reducer(state, action) {
9795
switch (action.type) {
98-
case 'reset':
99-
return initialState;
10096
case 'increment':
10197
return {count: state.count + 1};
10298
case 'decrement':
10399
return {count: state.count - 1};
104-
default:
105-
return state;
106100
}
107101
}
108102

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+
}));
111107

112108
return (
113109
<div>
@@ -141,25 +137,19 @@ describe('ReactShallowRenderer with hooks', () => {
141137
});
142138

143139
it('should work with a dispatched state change for a useReducer', () => {
144-
const initialState = {count: 0};
145-
146140
function reducer(state, action) {
147141
switch (action.type) {
148-
case 'reset':
149-
return initialState;
150142
case 'increment':
151143
return {count: state.count + 1};
152144
case 'decrement':
153145
return {count: state.count - 1};
154-
default:
155-
return state;
156146
}
157147
}
158148

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+
}));
163153

164154
if (state.count === 0) {
165155
dispatch({type: 'increment'});

0 commit comments

Comments
 (0)