I am leveraging the useReducer
hook with Context to create a Redux-ish state store supporting middleware.
const Provider = (props: any) => {
const [state, dispatch] = React.useReducer(reducer, {
title: 'Default title',
count: 0,
});
const actionDispatcher = makeActionDispatcher(
dispatch,
applyMiddleware(state, thunkMiddleware, callApiMiddleware, logger),
);
return (
<Context.Provider value={{ ...state, ...actionDispatcher }}>
{props.children}
</Context.Provider>
);
};
Note that I am passing state
to applyMiddleware
:
const applyMiddleware = (state: {}, ...middlewares: Function[]) =>
function dispatcher(dispatch: Function) {
const middlewareAPI = {
state,
dispatch: (...args) => dispatch(...args),
};
const chain = middlewares.map((middleware) => {
return middleware(middlewareAPI);
});
return compose(...chain)(dispatch);
};
This works, but eventually I want to be able to work with async actions, so ideally I'd have something like redux-thunk
:
function thunkMiddleware(store: Store) {
return (next: Function) => (action: any) => {
typeof action === 'function' ? action(next, store.getState) : next(action);
};
}
Given the thunk middleware will be acting upon async actions, ideally we would be able to pass a function to retrieve the current state when needed - getState
- rather than be forced to use state as it existed when the middleware was applied, which could be out of date.
Normally I would pass something like this down:
const getState = () => React.useReducer(reducer, {
title: 'Default title',
count: 0,
})[0];
But if I pass that down to middleware to be invoked, I get an error indicating I can only call hooks from React functions.
Am I architecting things wrong? Am I not properly wrapping my head around hooks?
UPDATE: adding requested makeActionDispatcher
implementation
export const makeActionDispatcher = (
dispatch: React.Dispatch<any> | undefined,
enhancer?: Function,
): ActionDispatcher => {
const actionDispatcher: { [key: string]: (...args: any) => void } = {};
Object.keys(actionCreators).forEach((key) => {
const creator = actionCreators[key];
actionDispatcher[key] = (...args: any) => {
if (!dispatch) {
throw new Error('ActionDispatcher has not been initialized!');
}
const action = creator(...args);
if (enhancer) {
const enhancedDispatch = enhancer(dispatch);
enhancedDispatch(action);
} else {
dispatch(action);
}
};
});
return actionDispatcher as ActionDispatcher;
};
Use the useEnhancedReducer
hook introduced here.
Then you will have something like.
const [state, dispatch, getState] = useEnahancedReducer(reducer, initState)
Because dispatch
, getState
will never change, you can pass it to some hook without adding them to the dependence list or store them somewhere else to call them from outside.
There is also version of useEnhancedReducer
which supports adding middleware, in the same post.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With