我可以忽略 useContext 的 exhaustive-deps 警告吗?

Can I ignore exhaustive-deps warning for useContext?

在我的 react-typescript 应用程序中,我尝试使用上下文提供程序来封装属性和方法并将它们公开给消费者:

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

问题是以下警告:

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

对我来说,将 stockPrice(基本上是提供程序的 API)包含到 useEffect 的依赖项中没有任何意义。只有包含股票价格的实际值以防止无限调用 useEffect 的函数才有意义。

问题:我尝试使用的方法有什么问题吗?或者我可以忽略这个警告吗?


提供商:

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>;
};

我建议控制来自提供商的整个获取逻辑:

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>;
};

...作为原始方法中几个问题的解决方案:

  1. 您真的只想只获取 val 不同的数据吗?如果 2 次渲染之间的股票价格相同,则不会执行 useEffect。
  2. 是否需要在每次渲染<StockPriceProvider>时都创建一个新的fetch方法?这确实不适合 useEffect 的依赖。

    • 如果两者都正常,请随意禁用 eslint 警告
    • 如果你想在消费者挂载时以 200 毫秒的间隔继续获取:
  // StockPriceProvider
  ...
    fetch: useCallback(() => dispatch({type: 'fetch', payload: 200}), [])
  ...
  // StockPriceConsumer
  ...
    useEffect(() => {
      const i = setInterval(fetch, 200)
      return () => clearInterval(i)
    }, [fetch])
  ...

这里的重要概念是 React 通过引用相等来比较对象。这意味着每次引用(而不是内容)更改时都会触发重新渲染。根据经验,您始终需要通过 useCallbackuseMemo.

定义要传递给子组件的 objects/functions

所以在你的情况下: fetch 函数将变为:

const fetch = useCallback(() => {
    setTimeout(() => {
        dispatch({ type: 'fetch', payload: 200 });
    }, 1000);
}, []);

空数组表示该函数只有在组件挂载时才会定义。然后:

let {val, fetch} = stockPrice;

 useEffect(() => {
    fetch();
 }, [val, fetch]);

这意味着 useEffect 的回调将仅在 fetchval 更改时执行。由于 fetch 只会被定义一次,实际上这意味着只有 val 的变化会触发效果的回调。

此外,我可以想象您只想在 isNaN(val) 时触发提取,所以:

let {val, fetch} = stockPrice;

 useEffect(() => {
    if(isNaN(val)) {
        fetch();
    }
 }, [val, fetch]);

综上所述,此代码存在更大的问题!

您应该重新考虑您使用 setTimeout 的方式,因为回调可以 运行 当组件已经卸载并且可能导致不同的错误。在这些情况下,您应该 useEffect 并在卸载组件之前清除任何异步操作。所以这是我的建议:

import React, { useCallback, useContext, useEffect } from 'react';
interface StockPrice {
    val: number;
    setFetched: () => void;
}

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

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 setFetched = useCallback(() => {
        dispatch({ type: 'fetch', payload: 200 });
    }, []);
    const contextVal = {
        ...state,
        setFetched,
    };
    return <myContext.Provider value={contextVal}>{children}</myContext.Provider>;
};

const StockPriceConsumer: React.FC = () => {

    const stockPrice = useContext(myContext);
    const {val, setFetched} = stockPrice;

    useEffect(() => {
        let handle = -1;
        if(isNaN(val)) {
            let handle = setTimeout(() => { // Or whatever async operation
                setFetched();
            }, 200);
        }
        return () => clearTimeout(handle); // Clear timeout before unmounting.
    }, [val, setFetched]);
    return <h1>{stockPrice.val.toString()}</h1>;
};