Search code examples
reactjsreact-contextuse-effectuse-reducer

Can I ignore exhaustive-deps warning for useContext?


In my react-typescript application, I am trying to use a context provider that encapsulates properties and methods and exposes them for a consumer:

const StockPriceConsumer: React.FC = () => {
  const stockPrice = useContext(myContext);
  let val = stockPrice.val;
  useEffect(() => {
    stockPrice.fetch();
  }, [val]);
  return <h1>{val}</h1>;
};

The problem is the following warning:

React Hook useEffect has a missing dependency: 'stockPrice'. Either include it or remove the dependency array. eslint(react-hooks/exhaustive-deps)

To me it does not make any sense to include the stockPrice (which is basically the provider's API) to the dependencies of useEffect. It only makes sense to include actual value of stock price to prevent infinite calls of useEffect's functions.

Question: Is there anything wrong with the approach I am trying to use or can I just ignore this warning?


The provider:

interface StockPrice {
  val: number;
  fetch: () => void;
}

const initialStockPrice = {val: NaN, fetch: () => {}};

type Action = {
  type: string;
  payload: any;
};

const stockPriceReducer = (state: StockPrice, action: Action): StockPrice => {
  if (action.type === 'fetch') {
    return {...state, val: action.payload};
  }
  return {...state};
};

const myContext = React.createContext<StockPrice>(initialStockPrice);

const StockPriceProvider: React.FC = ({children}) => {
  const [state, dispatch] = React.useReducer(stockPriceReducer, initialStockPrice);
  const contextVal  = {
    ...state,
    fetch: (): void => {
      setTimeout(() => {
        dispatch({type: 'fetch', payload: 200});
      }, 200);
    },
  };
  return <myContext.Provider value={contextVal}>{children}</myContext.Provider>;
};

Solution

  • I would recommend to control the whole fetching logic from the provider:

    const StockPriceProvider = ({children}) => {
      const [price, setPrice] = React.useState(NaN);
    
      useEffect(() => {
        const fetchPrice = () => {
          window.fetch('http...')
           .then(response => response.json())
           .then(data => setPrice(data.price))
        }
        const intervalId = setInterval(fetchPrice, 200)
        return () => clearInterval(intervalId)
      }, [])
    
      return <myContext.Provider value={price}>{children}</myContext.Provider>;
    };
    
    const StockPriceConsumer = () => {
      const stockPrice = useContext(myContext);
      return <h1>{stockPrice}</h1>;
    };
    

    ...as a solution to a couple of problems from the original appproach:

    1. do you really want to fetch only so long as val is different? if the stock price will be the same between 2 renders, the useEffect won't execute.
    2. do you need to create a new fetch method every time <StockPriceProvider> is rendered? That is not suitable for dependencies of useEffect indeed.

      • if both are OK, feel free to disable the eslint warning
      • if you want to keep fetching in 200ms intervals so long as the consumer is mounted:
      // StockPriceProvider
      ...
        fetch: useCallback(() => dispatch({type: 'fetch', payload: 200}), [])
      ...
      // StockPriceConsumer
      ...
        useEffect(() => {
          const i = setInterval(fetch, 200)
          return () => clearInterval(i)
        }, [fetch])
      ...