简体   繁体   English

React Hook useEffect 缺少依赖项:'dispatch'

[英]React Hook useEffect has a missing dependency: 'dispatch'

This is my first time working with react js, im trying to remove the alert when leaving this view cause i don't want to show it on the other view but in case that there is no error i want to keep the success alert to show it when i'm gonna redirect to the other view这是我第一次使用 React js,我试图在离开此视图时删除警报,因为我不想在另一个视图上显示它,但如果没有错误,我想保留成功警报以显示当我要重定向到另一个视图时

but im getting this wearning on google chrome Line 97:6: React Hook useEffect has a missing dependency: 'dispatch'. Either include it or remove the dependency array react-hooks/exhaustive-deps但我在 google chrome Line 97:6: React Hook useEffect has a missing dependency: 'dispatch'. Either include it or remove the dependency array react-hooks/exhaustive-deps Line 97:6: React Hook useEffect has a missing dependency: 'dispatch'. Either include it or remove the dependency array react-hooks/exhaustive-deps

if i did include dispatch i get infinite loop如果我确实包括调度我得到无限循环

const [state, dispatch] = useUserStore();
useEffect(() => {
    let token = params.params.token;
    checktoken(token, dispatch);
  }, [params.params.token]);

  useEffect(() => {
    return () => {
      if (state.alert.msg === "Error") {
        dispatch({
          type: REMOVE_ALERT
        });
      }
    };
  }, [state.alert.msg]);

//response from the api
if (!token_valide || token_valide_message === "done") {
      return <Redirect to="/login" />;
    }

this is useUserStore这是 useUserStore

  const globalReducers = useCombinedReducers({
    alert: useReducer(alertReducer, alertInitState),
    auth: useReducer(authReducer, authInitState),
    register: useReducer(registerReducer, registerInitState),
    token: useReducer(passeditReducer, tokenvalidationInitState)
  });
  return (
    <appStore.Provider value={globalReducers}>{children}</appStore.Provider>
  );
};

export const useUserStore = () => useContext(appStore);

dispatch comes from a custom hook so it doesn't have an stable signature therefore will change on each render (reference equality). dispatch来自自定义hook ,因此它没有稳定的签名,因此会在每次渲染时发生变化(引用相等)。 Add an aditional layer of dependencies by wrapping the handler inside an useCallback hook通过将处理程序包装在useCallback挂钩中来添加额外的依赖层

   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. useCallbackuseMemo是辅助钩子,主要目的是添加额外的依赖检查层以确保同步性。 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.通常你想使用useCallback来确保一个你知道如何改变而 React 不会改变的prop的稳定签名。

A function (reference type) passed via props for example例如,通过props传递的function (参考类型)

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假设您有一个子组件,在安装时必须在父组件中设置一些 state(不常见),上面的代码将在useEffect中生成未声明依赖项的警告,所以让我们将setParentState声明为依赖项以由 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.发生这种情况是因为setParentState是一个function ,每次调用 function Component时都会重新创建它。 You know that setParentState won't change it's signature overtime so it's safe to tell React that.你知道setParentState不会改变它的签名超时,所以告诉 React 是安全的。 By wrapping the original helper inside an useCallback you're doing exactly that (adding another dependency check layer).通过将原始帮助程序包装在useCallback ,您就可以做到这一点(添加另一个依赖检查层)。

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

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

There you go.那里有 go。 Now React knows that stableSetter won't change it's signature inside the lifecycle therefore the effect do not need too run unecessarily.现在React知道stableSetter不会在生命周期内改变它的签名,因此效果不需要太不必要地运行。

On a side note useCallback it's also used like useMemo , to optmize expensive function calls (memoization).在旁注useCallback它也像useMemo一样使用,以优化昂贵的 function 调用(记忆)。

The two mai/n purposes of useCallback are useCallback的两个主要目的是

  • Optimize child components that rely on reference equality to prevent unnecessary renders.优化依赖引用相等的子组件以防止不必要的渲染。 Font字体

  • Memoize expensive calculations记住昂贵的计算

UPDATE 09/11/2020 2020 年 9 月 11 日更新

This solution is no longer needed on es-lint-plugin-react-hooks@4.1.0 and above. es-lint-plugin-react-hooks@4.1.0及更高版本不再需要此解决方案。

Now useMemo and useCallback can safely receive referential types as dependencies.现在useMemouseCallback可以安全地接收引用类型作为依赖项。 #19590 #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现在,具有稳定签名的引用类型(例如来自useStateuseDispatch的那些来源)可以安全地在效果内使用,即使来自props也不会触发exhaustive-deps

编辑 silly-andras-9v1yp

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.我认为您可以从根本上解决问题,但这意味着更改 useCombinedReducers,我分叉了 repo 并创建了一个拉取请求,因为我认为 useCombinedReducers 不应该在您每次调用它时返回一个新的调度参考。

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>

Minimal example of the problem, 2021问题的最小示例,2021 年

Problem: You have this code and it creates an infinite loop.问题:你有这段代码,它创建了一个无限循环。 This is because array is in the dependency array, and you setArray on every run of the useEffect , which means it will continue to be set.这是因为array在依赖数组中,并且您在每次运行setArray时都设置了useEffect ,这意味着它将继续设置。

const MyComponent = ({ removeValue }) => {
  const [array, setArray] = useState([1, 2, 3, 4, 5]);

  // useEffect to remove `removeValue` from array
  useEffect(() => {
    const newArray = array.filter((value) => value !== removeValue);
    setArray(newArray);
  }, [array, removeValue]);

  return <div>{array.join(" ")}</div>;
};

There are multiple solutions:有多种解决方案:

Solution #1: using the previous state解决方案#1:使用之前的 state

The setState can also take a callback, which gives you the current state as an argument. setState也可以接受一个回调,它给你当前的 state 作为参数。 This can sometimes be used to solve the problem.这有时可以用来解决问题。 Now you will not need to include array in the dependency array:现在您不需要在依赖数组中包含array

const MyComponent = ({ removeValue }) => {
  const [array, setArray] = useState([1, 2, 3, 4, 5]);

  // useEffect to remove `removeValue` from array
  useEffect(() => {
    setArray((previousArray) => {
      const newArray = previousArray.filter((value) => value !== removeValue);
      return newArray;
    });
  }, [removeValue]);

  return <div>{array.join(" ")}</div>;
};

Solution #2: running the useEffect conditionally解决方案#2:有条件地运行 useEffect

Sometimes you can find a condition to run the useEffect .有时您可以找到运行useEffect的条件。 For example, we only need to set our array when the array includes the removeValue .例如,我们只需要在数组包含removeValue时设置我们的array Therefore we can return early:因此我们可以提前返回:

const MyComponent = ({ removeValue }) => {
  const [array, setArray] = useState([1, 2, 3, 4, 5]);

  // useEffect to remove `removeValue` from array
  useEffect(() => {
    const containsValue = array.includes(removeValue);
    if (!containsValue) return;

    const newArray = array.filter((value) => value !== removeValue);
    setArray(newArray);
  }, [array, removeValue]);

  return <div>{array.join(" ")}</div>;
};

Solution #3: using useCompare解决方案#3:使用useCompare

Sometimes the solution above is not possible due to restrictions, so here is a more sophisticated way.有时上面的解决方案由于限制是不可能的,所以这里有一个更复杂的方法。 This is for more complex problems, but I have yet to see a useEffect problem it cannot solve.这是针对更复杂的问题,但我还没有看到它无法解决的useEffect问题。 It requires two additional functions I will provide below.它需要我将在下面提供的两个附加功能。

I have commented on the code below to explain the function:我对下面的代码进行了评论,以解释 function:

const MyComponent = ({ removeValue }) => {
  const [array, setArray] = useState([1, 2, 3, 4, 5]);

  // useCompare will return either `true` or `false` depending
  // on if the value has changed. In this example, `useEffect` will
  // rerun every time the array length changes.
  // This can be applied to any other logic such as if removeValue
  // changes, depending on when you want to run the `useEffect`
  const arrayLengthChanged = useCompare(array.length);
  useEffect(() => {
    if (!arrayLengthChanged) return;
    const newArray = array.filter((value) => value !== removeValue);
    setArray(newArray);
  }, [array, arrayLengthChanged, removeValue]);

  return <div>{array.join(" ")}</div>;
};

Solution #4: disabling the error (not recommended)`解决方案#4:禁用错误(不推荐)`

The last "solution" is avoiding the problem.最后一个“解决方案”是避免问题。 In some cases, this is enough and will work perfectly, but might cause debugging problems if the useEffect changes later, or if not used correctly.在某些情况下,这已经足够并且可以完美地工作,但如果useEffect稍后更改或未正确使用,则可能会导致调试问题。

In this example, say you know that you will never need to run the useEffect when array changes, you can just remove it from the dependency array and ignore the error:在此示例中,假设您知道当数组更改时您将永远不需要运行useEffect ,您可以将其从依赖数组中删除并忽略错误:

const MyComponent = ({ removeValue }) => {
  const [array, setArray] = useState([1, 2, 3, 4, 5]);

  // useEffect to remove `removeValue` from array
  useEffect(() => {
    const newArray = array.filter((value) => value !== removeValue);
    setArray(newArray);

    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [removeValue]);

  return <div>{array.join(" ")}</div>;
};

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM