Skip to content

Commit 9780545

Browse files
Andrew Bazhanovwardoost
authored andcommitted
fix: createReducer stable dispatch function identity
stable dispatch function identity so it won’t change on re-renders
1 parent f2b38ca commit 9780545

File tree

2 files changed

+52
-30
lines changed

2 files changed

+52
-30
lines changed

docs/createReducer.md

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ Factory for reducer hooks with custom middleware with an identical API as [React
77
An example with [`redux-thunk`](https://github.com/reduxjs/redux-thunk) and [`redux-logger`](https://github.com/LogRocket/redux-logger).
88

99
```jsx
10-
import {createReducer} from 'react-use';
11-
import thunk from 'redux-thunk';
10+
import { createReducer } from 'react-use';
1211
import logger from 'redux-logger';
12+
import thunk from 'redux-thunk';
1313

1414
const useThunkReducer = createReducer(thunk, logger);
1515

@@ -20,29 +20,35 @@ function reducer(state, action) {
2020
case 'decrement':
2121
return { count: state.count - 1 };
2222
case 'reset':
23-
return init(action.payload);
23+
return { count: action.payload };
2424
default:
2525
throw new Error();
2626
}
2727
}
2828

29-
const Demo = () => {
29+
const Demo = ({ initialCount = 1 }) => {
30+
// Action creator to increment count, wait a second and then reset
3031
const addAndReset = React.useCallback(() => {
3132
return dispatch => {
3233
dispatch({ type: 'increment' });
3334

3435
setTimeout(() => {
35-
dispatch({ type: 'reset', payload: 1 });
36+
dispatch({ type: 'reset', payload: initialCount });
3637
}, 1000);
3738
};
38-
}, []);
39+
}, [initialCount]);
3940

40-
const [count, dispatch] = useThunkReducer(reducer, 1);
41+
const [state, dispatch] = useThunkReducer(reducer, initialCount);
4142

4243
return (
4344
<div>
44-
<p>count: {count}</p>
45+
<p>count: {state.count}</p>
4546
<button onClick={() => dispatch(addAndReset())}>Add and reset</button>
47+
<button
48+
onClick={() => dispatch({ type: 'reset', payload: initialCount })}
49+
>
50+
Reset
51+
</button>
4652
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
4753
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
4854
</div>

src/createReducer.ts

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { useMemo, useRef, useState } from 'react';
1+
import { useCallback, useRef, useState } from 'react';
2+
import useUpdateEffect from './useUpdateEffect';
23

34
function composeMiddleware(chain) {
45
return (context, dispatch) => {
@@ -8,29 +9,44 @@ function composeMiddleware(chain) {
89
};
910
}
1011

11-
const createReducer = (...middlewares) => (reducer, initialState, initializer = value => value) => {
12-
const ref = useRef(initializer(initialState));
13-
const [, setState] = useState(ref.current);
14-
let middlewareDispatch = (_ = {}) => {
15-
throw new Error(
16-
'Dispatching while constructing your middleware is not allowed. ' +
17-
'Other middleware would not be applied to this dispatch.'
12+
const createReducer = (...middlewares) => {
13+
const composedMiddleware = composeMiddleware(middlewares);
14+
15+
return (reducer, initialState, initializer = value => value) => {
16+
const ref = useRef(initializer(initialState));
17+
const [, setState] = useState(ref.current);
18+
19+
const dispatch = useCallback(
20+
action => {
21+
ref.current = reducer(ref.current, action);
22+
setState(ref.current);
23+
return action;
24+
},
25+
[reducer]
1826
);
27+
28+
const dispatchRef = useRef(
29+
composedMiddleware(
30+
{
31+
getState: () => ref.current,
32+
dispatch: (...args) => dispatchRef.current(...args),
33+
},
34+
dispatch
35+
)
36+
);
37+
38+
useUpdateEffect(() => {
39+
dispatchRef.current = composedMiddleware(
40+
{
41+
getState: () => ref.current,
42+
dispatch: (...args) => dispatchRef.current(...args),
43+
},
44+
dispatch
45+
);
46+
}, [dispatch]);
47+
48+
return [ref.current, dispatchRef.current];
1949
};
20-
const dispatch = action => {
21-
ref.current = reducer(ref.current, action);
22-
setState(ref.current);
23-
return action;
24-
};
25-
const composedMiddleware = useMemo(() => {
26-
return composeMiddleware(middlewares);
27-
}, middlewares);
28-
const middlewareAPI = {
29-
getState: () => ref.current,
30-
dispatch: (...args) => middlewareDispatch(...args),
31-
};
32-
middlewareDispatch = composedMiddleware(middlewareAPI, dispatch);
33-
return [ref.current, middlewareDispatch];
3450
};
3551

3652
export default createReducer;

0 commit comments

Comments
 (0)