reactjstypescriptreact-hooksreact-context

React use hooks (useContext) inside of useReducer


I have a reducer inside useContext/provider. I need to validate an action using values from another Provider. Is there a way to retrieve those values from a Provider from within the useReducer hook? Or do I have to try to feed them in as function arguments. Ex:

Reducer

export default function reducer(state: State, action: Action): State {
    switch(action.type) {
        case "set-amount": {
            const { amount } = action.state
            const { maxAmount } = useFooContext()

            if (amount > maxAmount) // do something

            return { ...state, amount }
        }
    }
}

Provider

export function Provider(...) {
    const [state, dispatch] = useReducer(reducer, {}, initState)
    return ...
}

=== EDIT per @Drew

Wondering if I'm following what you said, does this look right?

// provider
export default function Provider( ... ) {
    const foo = useFooContext()
    const fooRef = React.useRef(foo)

    const reducerFn = useMemo( () => {
        fooRef.current = foo
        return reducer( fooRef )
    }, [foo])

    const [state, dispatch] = useReducer( reducerFn, {}, initState)

    return ( ... )
}

// reducer
export default function(ref: React.MutableRefObject<Foo>) {
    return function(state: State, action: Action): State {
        ...
        fooRef.current.value != value
        ...
    }
}

Solution

  • You can't call React hooks in nested functions, this breaks React's Rules of Hooks, only call hooks at the top-level in React functions and custom hooks.

    I suggest re-writing the reducer function to curry the Foo context value when it's instantiated.

    Example:

    const reducer = (fooContext: FooContext) => (state: State, action: Action) => {
      switch(action.type) {
        case "set-amount": {
          const { amount } = action.state;
          const { maxAmount } = fooContext;
    
          if (amount > maxAmount) // do something
    
          return { ...state, amount }
        }
    
        ...
      }
    };
    
    export default reducer;
    
    export function Provider(...) {
      const fooContext = useFooContext();
    
      const reducerFn = useMemo(() => reducer(fooContext), []);
    
      const [state, dispatch] = useReducer(reducerFn, {}, initState);
    
      return ...
    }
    

    If you need to access non-static values then you could return a "getState" method the fooContext could access, or save fooContext into a React ref that is passed to the curried reducer function so it can access whatever the current context value is each time.