React Hook useEffect has a missing dependency: 'dispatch'

I think you can solve the problem at the root but that means changing useCombinedReducers, I forked the repo and created a pull request because I don't think useCombinedReducers should return a new reference for dispatch every time you call it.

function memoize(fn) {
  let lastResult,
    //initial last arguments is not going to be the same
    //  as anything you will pass to the function the first time
    lastArguments = [{}];
  return (...currentArgs) => {
    //returning memoized function
    //check if currently passed arguments are the same as
    //  arguments passed last time
    const sameArgs =
      currentArgs.length === lastArguments.length &&
      lastArguments.reduce(
        (result, lastArg, index) =>
          result && Object.is(lastArg, currentArgs[index]),
        true,
      );
    if (sameArgs) {
      //current arguments are same as last so just
      //  return the last result and don't execute function
      return lastResult;
    }
    //current arguments are not the same as last time
    //  or function called for the first time, execute the
    //  function and set last result
    lastResult = fn.apply(null, currentArgs);
    //set last args to current args
    lastArguments = currentArgs;
    //return result
    return lastResult;
  };
}

const createDispatch = memoize((...dispatchers) => action =>
  dispatchers.forEach(fn => fn(action)),
);
const createState = memoize(combinedReducers =>
  Object.keys(combinedReducers).reduce(
    (acc, key) => ({ ...acc, [key]: combinedReducers[key][0] }),
    {},
  ),
);
const useCombinedReducers = combinedReducers => {
  // Global State
  const state = createState(combinedReducers);

  const dispatchers = Object.values(combinedReducers).map(
    ([, dispatch]) => dispatch,
  );

  // Global Dispatch Function
  const dispatch = createDispatch(...dispatchers);

  return [state, dispatch];
};

export default useCombinedReducers;

Here is a working example:

const reduceA = (state, { type }) =>
  type === 'a' ? { count: state.count + 1 } : state;
const reduceC = (state, { type }) =>
  type === 'c' ? { count: state.count + 1 } : state;
const state = { count: 1 };
function App() {
  const [a, b] = React.useReducer(reduceA, state);
  const [c, d] = React.useReducer(reduceC, state);
  //memoize what is passed to useCombineReducers
  const obj = React.useMemo(
    () => ({ a: [a, b], c: [c, d] }),
    [a, b, c, d]
  );
  //does not do anything with reduced state
  const [, reRender] = React.useState();
  const [s, dispatch] = useCombinedReducers(obj);
  const rendered = React.useRef(0);
  const [sc, setSc] = React.useState(0);
  const [dc, setDc] = React.useState(0);
  rendered.current++;//display how many times this is rendered
  React.useEffect(() => {//how many times state changed
    setSc(x => x + 1);
  }, [s]);
  React.useEffect(() => {//how many times dispatch changed
    setDc(x => x + 1);
  }, [dispatch]);
  return (
    <div>
      <div>rendered {rendered.current} times</div>
      <div>state changed {sc} times</div>
      <div>dispatch changed {dc} times</div>
      <button type="button" onClick={() => reRender({})}>
        re render
      </button>
      <button
        type="button"
        onClick={() => dispatch({ type: 'a' })}
      >
        change a
      </button>
      <button
        type="button"
        onClick={() => dispatch({ type: 'c' })}
      >
        change c
      </button>
      <pre>{JSON.stringify(s, undefined, 2)}</pre>
    </div>
  );
}

function memoize(fn) {
  let lastResult,
    //initial last arguments is not going to be the same
    //  as anything you will pass to the function the first time
    lastArguments = [{}];
  return (...currentArgs) => {
    //returning memoized function
    //check if currently passed arguments are the same as
    //  arguments passed last time
    const sameArgs =
      currentArgs.length === lastArguments.length &&
      lastArguments.reduce(
        (result, lastArg, index) =>
          result && Object.is(lastArg, currentArgs[index]),
        true
      );
    if (sameArgs) {
      //current arguments are same as last so just
      //  return the last result and don't execute function
      return lastResult;
    }
    //current arguments are not the same as last time
    //  or function called for the first time, execute the
    //  function and set last result
    lastResult = fn.apply(null, currentArgs);
    //set last args to current args
    lastArguments = currentArgs;
    //return result
    return lastResult;
  };
}

const createDispatch = memoize((...dispatchers) => action =>
  dispatchers.forEach(fn => fn(action))
);
const createState = memoize(combinedReducers =>
  Object.keys(combinedReducers).reduce(
    (acc, key) => ({
      ...acc,
      [key]: combinedReducers[key][0],
    }),
    {}
  )
);
const useCombinedReducers = combinedReducers => {
  // Global State
  const state = createState(combinedReducers);

  const dispatchers = Object.values(combinedReducers).map(
    ([, dispatch]) => dispatch
  );

  // Global Dispatch Function
  const dispatch = createDispatch(...dispatchers);

  return [state, dispatch];
};

ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

UPDATE 09/11/2020

This solution is no longer needed on [email protected] and above.

Now useMemo and useCallback can safely receive referential types as dependencies.#19590

function MyComponent() {
  const foo = ['a', 'b', 'c']; // <== This array is reconstructed each render
  const normalizedFoo = useMemo(() => foo.map(expensiveMapper), [foo]);
  return <OtherComponent foo={normalizedFoo} />
}

Here is another example of how to safely stabilize(normalize) a callback

const Parent = () => {
    const [message, setMessage] = useState('Greetings!')

    return (
        <h3>
            { message }
        </h3>
        <Child setter={setMessage} />
    )
}

const Child = ({
    setter
}) => {
    const stableSetter = useCallback(args => {
        console.log('Only firing on mount!')
        return setter(args)
    }, [setter])

    useEffect(() => {
        stableSetter('Greetings from child\'s mount cycle')
    }, [stableSetter]) //now shut up eslint

    const [count, setCount] = useState(0)

    const add = () => setCount(c => c + 1)

    return (
        <button onClick={add}>
            Rerender {count}
        </button>
    )
}

Now referential types with stable signature such as those provenients from useState or useDispatch can safely be used inside an effect without triggering exhaustive-deps even when coming from props

Edit silly-andras-9v1yp

---

Old answer

dispatch comes from a custom hook so it doesn't have an stable signature therefore will change on each render (reference equality). Add an aditional layer of dependencies by wrapping the handler inside an useCallback hook

   const [foo, dispatch] = myCustomHook()
  
   const stableDispatch = useCallback(dispatch, []) //assuming that it doesn't need to change

   useEffect(() =>{
        stableDispatch(foo)
   },[stableDispatch])

useCallback and useMemo are helper hooks with the main purpose off adding an extra layer of dependency check to ensure synchronicity. Usually you want to work with useCallback to ensure a stable signature to a prop that you know how will change and React doesn't.

A function(reference type) passed via props for example

const Component = ({ setParentState }) =>{
    useEffect(() => setParentState('mounted'), [])
}

Lets assume you have a child component which uppon mounting must set some state in the parent (not usual), the above code will generate a warning of undeclared dependency in useEffect, so let's declare setParentState as a dependency to be checked by React

const Component = ({ setParentState }) =>{
    useEffect(() => setParentState('mounted'), [setParentState])
}

Now this effect runs on each render, not only on mounting, but on each update. This happens because setParentState is a function which is recreated every time the function Component gets called. You know that setParentState won't change it's signature overtime so it's safe to tell React that. By wrapping the original helper inside an useCallback you're doing exactly that (adding another dependency check layer).

const Component = ({ setParentState }) =>{
   const stableSetter = useCallback(() => setParentState(), [])

   useEffect(() => setParentState('mounted'), [stableSetter])
}

There you go. Now React knows that stableSetter won't change it's signature inside the lifecycle therefore the effect do not need too run unecessarily.

On a side note useCallback it's also used like useMemo, to optmize expensive function calls (memoization).

The two mai/n purposes of useCallback are

  • Optimize child components that rely on reference equality to prevent unnecessary renders. Font

  • Memoize expensive calculations