【问题标题】:React Hook useEffect has a missing dependency: 'dispatch'React Hook useEffect 缺少依赖项:'dispatch'
【发布时间】:2020-02-25 16:23:48
【问题描述】:

这是我第一次使用 react js,我试图在离开这个视图时删除警报,因为我不想在另一个视图上显示它,但如果没有错误,我想保持成功当我要重定向到另一个视图时提醒显示它

但我在谷歌浏览器上穿了这个 Line 97:6: React Hook useEffect has a missing dependency: 'dispatch'. Either include it or remove the dependency array react-hooks/exhaustive-deps

如果我确实包含调度,我会得到无限循环

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

这是 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);

【问题讨论】:

  • 什么是useUserStore?
  • @Dupocas Op 已经尝试过了,但随后陷入无限循环。
  • 更新了答案。将其包装在 useCallback 中并将此包装后的版本传递给您的效果
  • 现在我们要问什么是useCombineRecurs。
  • useCombineRecurs 是一个类似于 redux 中的 CombineReducers 的库

标签: reactjs react-hooks


【解决方案1】:

2020 年 9 月 11 日更新

es-lint-plugin-react-hooks@4.1.0 及更高版本不再需要此解决方案。

现在useMemo 和useCallback 可以安全地接收引用类型作为依赖项。#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} />
}

这是另一个如何安全稳定(规范化)回调的示例

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

现在,具有稳定签名的引用类型(例如来自useState 或useDispatch 的那些来源)可以安全地在效果内使用而无需触发exhaustive-deps,即使来自props 也是如此

---

旧答案

dispatch 来自自定义hook,因此它没有稳定的签名,因此在每次渲染时都会发生变化(引用相等)。通过将处理程序包装在 useCallback 挂钩中来添加额外的依赖层

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

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

useCallback 和useMemo 是辅助钩子,主要目的是添加额外的依赖检查层以确保同步。通常你希望使用useCallback 来确保prop 的稳定签名,你知道它会如何改变而React 不会。

例如通过props 传递的function(引用类型)

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

假设你有一个子组件,它在挂载时必须在父组件中设置一些状态(不常见),上面的代码将在useEffect 中生成未声明依赖项的警告,所以让我们将setParentState 声明为依赖项被 React 检查

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

现在这种效果在每次渲染时都会运行,不仅在安装时,而且在每次更新时。这是因为setParentState 是一个function,每次调用函数Component 时都会重新创建它。你知道setParentState 不会改变它的签名超时,所以告诉 React 是安全的。通过将原始帮助程序包装在 useCallback 中,您就可以做到这一点(添加另一个依赖检查层)。

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

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

给你。现在React 知道stableSetter 不会在生命周期内更改它的签名,因此效果不需要太不必要地运行。

在旁注useCallback 它也像useMemo 一样用于优化昂贵的函数调用(记忆)。

useCallback 的两个主要目的是

  • 优化依赖引用相等的子组件,防止不必要的 呈现。 Font

  • 记住昂贵的计算

【讨论】:

  • 会起作用,但我认为 OP 应该从源头解决它,而不是 OP 将在许多地方使用调度的地方。
  • 即使他将dispatch 与useCallback 包装在customHook 内,问题仍然存在,并且useUserStore 并不被视为他可以实际更改源代码的钩子。
  • 我不知道 useCombinedReducers 库,但它可能由useCombinedReducers(not passing new ref every time)修复
  • 我也不知道。如果他能确定这是一个比每次都将其包裹在useCallback 中更好的解决方案。
  • @Dupocas 你的解决方案对我来说很好,谢谢你的帮助,我实际上没有使用 redux 我是这个框架的新手
【解决方案2】:

我认为您可以从根本上解决问题,但这意味着更改 useCombinedReducers,我分叉了 repo 并创建了 a pull request,因为我认为 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;

这是一个工作示例:

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>

【讨论】:

  • 非常感谢您的帮助,我会尝试一下
【解决方案3】:

问题的最小示例,2021 年

问题:您有这段代码,它会创建一个无限循环。 这是因为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>;
};

有多种解决方案:

解决方案 #1:使用之前的状态

setState 也可以接受回调,它将当前状态作为参数提供给您。这有时可以用来解决问题。 现在你不需要在依赖数组中包含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>;
};

解决方案 #2:有条件地运行 useEffect

有时您可以找到运行useEffect 的条件。 例如,当数组包含removeValue 时,我们只需要设置我们的array。所以我们可以早点回来:

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

解决方案#3:使用useCompare

有时由于限制,上述解决方案是不可能的,所以这里有一个更复杂的方法。这是针对更复杂的问题,但我还没有看到它无法解决的useEffect 问题。它需要我将在下面提供的两个附加功能。

我已经注释了下面的代码来解释功能:

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

解决方案 #4:禁用错误(不推荐)`

最后一个“解决方案”是避免问题。在某些情况下,这已经足够并且可以完美运行,但如果 useEffect 稍后更改或未正确使用,可能会导致调试问题。

在这个例子中,假设你知道当数组改变时你永远不需要运行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>;
};

【讨论】:

    猜你喜欢
    • 2021-02-23
    • 2019-10-24
    • 2020-10-26
    • 2020-05-27
    • 2020-03-07
    • 2020-06-11
    • 2020-03-30
    • 2020-12-29
    • 2019-09-20
    相关资源
    最近更新 更多