Skip to content

Commit

Permalink
fix: createReducer stable dispatch function identity
Browse files Browse the repository at this point in the history
stable dispatch function identity so it won’t change on re-renders
  • Loading branch information
Andrew Bazhanov authored and wardoost committed Jul 16, 2019
1 parent f2b38ca commit 9780545
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 30 deletions.
22 changes: 14 additions & 8 deletions docs/createReducer.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ Factory for reducer hooks with custom middleware with an identical API as [React
An example with [`redux-thunk`](https://github.com/reduxjs/redux-thunk) and [`redux-logger`](https://github.com/LogRocket/redux-logger).

```jsx
import {createReducer} from 'react-use';
import thunk from 'redux-thunk';
import { createReducer } from 'react-use';
import logger from 'redux-logger';
import thunk from 'redux-thunk';

const useThunkReducer = createReducer(thunk, logger);

Expand All @@ -20,29 +20,35 @@ function reducer(state, action) {
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return init(action.payload);
return { count: action.payload };
default:
throw new Error();
}
}

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

setTimeout(() => {
dispatch({ type: 'reset', payload: 1 });
dispatch({ type: 'reset', payload: initialCount });
}, 1000);
};
}, []);
}, [initialCount]);

const [count, dispatch] = useThunkReducer(reducer, 1);
const [state, dispatch] = useThunkReducer(reducer, initialCount);

return (
<div>
<p>count: {count}</p>
<p>count: {state.count}</p>
<button onClick={() => dispatch(addAndReset())}>Add and reset</button>
<button
onClick={() => dispatch({ type: 'reset', payload: initialCount })}
>
Reset
</button>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</div>
Expand Down
60 changes: 38 additions & 22 deletions src/createReducer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useMemo, useRef, useState } from 'react';
import { useCallback, useRef, useState } from 'react';
import useUpdateEffect from './useUpdateEffect';

function composeMiddleware(chain) {
return (context, dispatch) => {
Expand All @@ -8,29 +9,44 @@ function composeMiddleware(chain) {
};
}

const createReducer = (...middlewares) => (reducer, initialState, initializer = value => value) => {
const ref = useRef(initializer(initialState));
const [, setState] = useState(ref.current);
let middlewareDispatch = (_ = {}) => {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.'
const createReducer = (...middlewares) => {
const composedMiddleware = composeMiddleware(middlewares);

return (reducer, initialState, initializer = value => value) => {
const ref = useRef(initializer(initialState));
const [, setState] = useState(ref.current);

const dispatch = useCallback(
action => {
ref.current = reducer(ref.current, action);
setState(ref.current);
return action;
},
[reducer]
);

const dispatchRef = useRef(
composedMiddleware(
{
getState: () => ref.current,
dispatch: (...args) => dispatchRef.current(...args),
},
dispatch
)
);

useUpdateEffect(() => {
dispatchRef.current = composedMiddleware(
{
getState: () => ref.current,
dispatch: (...args) => dispatchRef.current(...args),
},
dispatch
);
}, [dispatch]);

return [ref.current, dispatchRef.current];
};
const dispatch = action => {
ref.current = reducer(ref.current, action);
setState(ref.current);
return action;
};
const composedMiddleware = useMemo(() => {
return composeMiddleware(middlewares);
}, middlewares);
const middlewareAPI = {
getState: () => ref.current,
dispatch: (...args) => middlewareDispatch(...args),
};
middlewareDispatch = composedMiddleware(middlewareAPI, dispatch);
return [ref.current, middlewareDispatch];
};

export default createReducer;

0 comments on commit 9780545

Please sign in to comment.