Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Retrieve current state from useReducer outside React component

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;
};
like image 460
Mister Epic Avatar asked Nov 06 '22 18:11

Mister Epic


1 Answers

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.

like image 153
transang Avatar answered Nov 15 '22 06:11

transang