Reputation: 2641
I am refactoring the complex state management of one of my contexts, which previously used the hook "useStateWithCallback".
I have decided to refactor the code using "useReducer", which will make it more readable.
Currently, I am doing this:
export function ContentsProvider({ children }) {
const [contents, setContents] = useStateWithCallback(new Map([]));
const addContents = (newContents, callback = undefined) => {
setContents(
(prevContents) => new Map([...prevContents, ...newContents]),
callback
);
};
...
Now, on any of my app components which consumes this context I can do:
contents.addContents([ ... ], () => { ... });
And the callback will be executed only when the state has changed.
Is there any way to achieve this behavior with useReducer? I mean, if I am passing the callback as argument to the method of my provider, then I will be not able to run a
useEffect(() => { callback?.() } , [contents]);
Any ideas?
I have tried this:
import { useReducer, useEffect } from 'react'
export const useReducerWithCallback = (
initialState,
reducer,
callback = undefined
) => {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
callback?.(state);
}, [state, callback]);
return [state, dispatch];
};
But it will not work, as in my use case, the callback is not always the same, so I mustn't declare the reducer with the callback.
Upvotes: 1
Views: 1534
Reputation: 11
Drew's answer is great! If you're a TypeScript user, you could potentially type this function using the types from the @types/react declaration file:
/**
* Defines the custom dispatch function that wraps the dispatch returned from useReducer.
*/
type CustomReactDispatchWithCallback<A, S> = (action: A, callback?: DispatchCallback<S>) => void;
/**
* Defines the callback contract. It should be a function that receives the updated state.
*/
type DispatchCallback<S> = (state: S) => void;
/**
* Wraps `React.useReducer` and provides a custom dispatch function that accepts
* a callback that will be cached and then invoked when the reducer state changes.
*/
export function useReducerWithCallback<R extends React.Reducer<any, any>, I>(
reducer: R,
initialState: I & React.ReducerState<R>,
initializer: (arg: I & React.ReducerState<R>) => React.ReducerState<R>
) {
const callbackRef = React.useRef<DispatchCallback<React.ReducerState<R>>>();
const [state, dispatch] = React.useReducer(reducer, initialState, initializer);
React.useEffect(() => {
callbackRef.current?.(state);
}, [state]);
const customDispatch: CustomReactDispatchWithCallback<
React.ReducerAction<R>,
React.ReducerState<R>
> = (action, callback) => {
callbackRef.current = callback;
dispatch(action);
};
return [state, customDispatch] as const;
}
This doesn't account for all of React.useReducer
's overloads, but worked for my case.
Upvotes: 1
Reputation: 202667
If I understand what you are looking for then I think a custom hook using a React ref and effect might achieve the behavior you seek.
useEffect
to call the callback when the state has updateddispatch
function to set the callback and dispatch an action to the reducer function.Code
const useReducerWithCallback = (reducer, initialState, initializer) => {
const callbackRef = useRef();
const [state, dispatch] = useReducer(reducer, initialState, initializer);
useEffect(() => {
callbackRef.current?.(state);
}, [state]);
const customDispatch = (action, callback) => {
callbackRef.current = callback;
dispatch(action);
};
return [state, customDispatch];
};
const useReducerWithCallback = (reducer, initialState, initializer) => {
const callbackRef = React.useRef();
const [state, dispatch] = React.useReducer(reducer, initialState, initializer);
React.useEffect(() => {
callbackRef.current && callbackRef.current(state);
}, [state]);
const customDispatch = (action, callback) => {
callbackRef.current = callback;
dispatch(action);
};
return [state, customDispatch];
};
const reducer = (state, action) => {
switch (action.type) {
case "ADD":
return state + action.payload;
case "SUBTRACT":
return state - action.payload;
default:
return state;
}
};
function App() {
const [state, dispatch] = useReducerWithCallback(reducer, 0);
return (
<div className="App">
State: {state}
<button
type="button"
onClick={() =>
dispatch({ type: "ADD", payload: 1 }, (state) =>
console.log("Added 1, state", state)
)
}
>
+
</button>
<button
type="button"
onClick={() =>
dispatch({ type: "SUBTRACT", payload: 1 }, (state) =>
console.log("Subtracted 1, state", state)
)
}
>
-
</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
rootElement
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Upvotes: 2