【问题标题】:React hooks: why does useEffect need an exhaustive array of dependencies?React hooks:为什么 useEffect 需要一个详尽的依赖数组?
【发布时间】:2020-11-22 17:38:57
【问题描述】:

我在 React 组件中有以下用例。

这是一个使用React Autosuggest 的搜索用户输入。它的值始终是一个 ID,所以我只有用户 ID 作为道具。因此,在第一次加载以显示用户名值时,我需要在第一次加载时获取它。

编辑:我不想在以后更改时再次获取该值,因为我已经从我的建议请求中获得了该值。

type InputUserProps = {
  userID?: string;
  onChange: (userID: string) => void;
};

// Input User is a controlled input
const InputUser: React.FC<InputUserProps> = (props) => {
  const [username, setUsername] = useState<string | null>(null);

  useEffect(() => {
    if (props.userID && !username) {
      fetchUsername(props.userID).then((username) => setUsername(username));
    }
  }, []);

  async function loadSuggestions(inputValue: string): Suggestion[] {
    return fetchSuggestions(inputValue);
  }

  function selectSuggestion(suggestion: Suggestion): void {
    props.onChange(suggestion.userID); // I don't want to rerun the effect from that change...
    setUsername(suggestion.username); // ...because I set the username here
  }

  return (
    <InputSuggest
      value={props.userID}
      label={username}
      onSuggestionsRequested={loadSuggestions}
      onSuggestionSelected={selectSuggestion}
    />
  );
};

export default InputUser;

(我正在添加一个简化的方式来调用这个组件)

const App: React.FC<AppProps> = (props) => {
    // simplified, I use react-hook-form in the real code
    const [userID, setUserID] = useState<string?>(any_initial_value_or_null);
    return <InputUser userID={userID} onChange={(newValue)=>setUserID(newValue)} />
};

export default App;

它有效,但我的 useEffect 钩子上有以下警告

React Hook useEffect has missing dependencies: 'props.userID' and 'username'. Either include them or remove the dependency array.eslint(react-hooks/exhaustive-deps)

但如果我这样做,我会运行它不止一次,因为用户名值是由钩子本身更改的!由于它在没有所有依赖项的情况下工作,我想知道:

  • 如何彻底解决我的案件?
  • 这些依赖项的用途是什么?为什么建议将它们详尽无遗?

【问题讨论】:

  • 我不想从该更改中重新运行效果 - 如果我理解您更新的问题,您希望效果只运行一次,因此使用空数组是美好的。或者,您可以将所有依赖项包含到 useEffect 的依赖项数组中,但仅使用在 useEffect 至少执行一次后评估为 false 的条件在初始渲染时获取用户。
  • 是的,我仍然收到这个 eslint 警告。但我想这只是一个过于激进的规则,我应该禁用它。 React Hook useEffect has missing dependencies: 'props.userID' and 'username'. Either include them or remove the dependency array.eslint(react-hooks/exhaustive-deps)
  • 由于我的回答和链接文章中提到的原因,我建议不要禁用此规则。仅在某些特定情况下(例如您的情况)才可以对依赖项撒谎,但通常您应该在依赖项数组中包含效果的所有依赖项。如果可以的话,还请尝试改进您的应用程序的设计,以避免一开始就必须这样做。
  • useEffect dep 的数组与 useCallbackuseMemo 等其他钩子不同,如果不包含每个 dep,可能会导致值过时。 useEffect 更多地依赖于业务逻辑,因此 imo 一个 linter 不应该决定您的业务逻辑。大多数情况下,如果可能,我通常会列出所有的部门,否则我会忽略该场景的规则并就原因发表评论。 eslint-disable-next-line react-hooks/exhaustive-deps

标签: javascript reactjs typescript react-hooks


【解决方案1】:

那些依赖是什么?

useEffect 接受一个可选的第二个参数,它是一个依赖数组。 useEffect 钩子的依赖项告诉它在其依赖项发生变化时运行效果。

如果您没有将可选的第二个参数传递给 useEffect 挂钩,它将在每次组件重新渲染时执行。空依赖数组指定您只想在组件初始渲染后运行一次效果。在这种情况下,useEffect 钩子的行为几乎类似于类组件中的 componentDidMount。

为什么建议详尽无遗?

效果见 propsstate 从它们被定义的渲染中。因此,当您在 useEffect 的回调函数中使用参与反应数据流的功能组件范围内的东西时,如道具或状态钩子,该回调函数函数将关闭该数据,除非使用新的 props 和 state 值定义新效果,否则您的效果将看到陈旧的 props 和 state 值。

下面的代码 sn-p 演示了如果你对 useEffect 钩子的依赖关系撒谎会出现什么问题。

在下面的代码 sn-p 中,有两个组件,AppUserApp 组件具有三个按钮,并维护由 User 组件显示的用户 ID。用户 ID 作为 prop 从 App 传递到 User 组件,User 组件从 jsonplaceholder API 获取具有 ID 的用户,作为 prop 传递。

现在下面的代码 sn-p 的问题是它不能正常工作。原因是它与 useEffect 挂钩的依赖关系有关。 useEffect hook 依赖于 userID prop 从 API 获取用户,但是当我跳过添加 userID 作为依赖项时,useEffect hook 不会在每次 userID prop 更改时执行。

function User({userID}) {
  const [user, setUser] = React.useState(null);
  
  React.useEffect(() => {
    if (userID > 0) {
      fetch(`https://jsonplaceholder.typicode.com/users/${userID}`)
        .then(response => response.json())
        .then(user => setUser(user))
        .catch(error => console.log(error.message));
    }
  }, []); 
  
  return (
    <div>
      {user ? <h1>{ user.name }</h1> : <p>No user to show</p>}
    </div>
  );
}

function App() {
  const [userID, setUserID] = React.useState(0);
  
  const handleClick = (id) => {
    setUserID(id);
  };
  
  return(
    <div>
      <button onClick={() => handleClick(1)}>User with ID: 1</button>
      <button onClick={() => handleClick(2)}>User with ID: 2</button>
      <button onClick={() => handleClick(3)}>User with ID: 3</button>
      <p>Current User ID: {userID}</p>
      <hr/>
      <User userID={userID} />
    </div>
  );
}

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

上面的代码 sn-p 显示了如果你对依赖项撒谎可能出现的少数问题之一,这就是为什么 你不能跳过或撒谎 useEffect 钩子或任何其他具有依赖数组的钩子,例如 useCallback 钩子。'

要修复之前的代码 sn-p,你只需要将 userID 作为依赖添加到 useEffect 钩子的依赖数组中,这样只要 userID 属性发生变化就会执行 if获取 id 等于 userID 属性的新用户。

function User({userID}) {
  const [user, setUser] = React.useState(null);
  
  React.useEffect(() => {
    if (userID > 0) {
      fetch(`https://jsonplaceholder.typicode.com/users/${userID}`)
        .then(response => response.json())
        .then(user => setUser(user))
        .catch(error => console.log(error.message));
    }
  }, [userID]); 
  
  return (
    <div>
      {user ? <h1>{ user.name }</h1> : <p>No user to show</p>}
    </div>
  );
}

function App() {
  const [userID, setUserID] = React.useState(0);
  
  const handleClick = (id) => {
    setUserID(id);
  };
  
  return(
    <div>
      <button onClick={() => handleClick(1)}>User with ID: 1</button>
      <button onClick={() => handleClick(2)}>User with ID: 2</button>
      <button onClick={() => handleClick(3)}>User with ID: 3</button>
      <p>Current User ID: {userID}</p>
      <hr/>
      <User userID={userID} />
    </div>
  );
}

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

在你的情况下,如果你跳过在useEffect钩子的依赖数组中添加props.userID,当prop userID发生变化时,你的效果将不会获取新数据。

要详细了解省略 useEffect 挂钩的依赖项的负面影响,请参阅:

我怎样才能干净利落地解决我的案子?

由于您的效果取决于属性值 userID,因此您应该将其包含在依赖数组中,以便在 userID 更改时始终获取新数据。

添加props.userID 作为useEffect 钩子的依赖项将在每次props.userID 更改时触发效果,但问题是您在useEffect 内不必要地使用username。您应该删除它,因为这不是必需的,因为 username 值不会也不应该决定何时应该获取新的用户数据。您只想在 props.userID 更改时运行效果。

您还可以通过使用useReducer 挂钩来管理和更新状态,从而将操作与状态更新分离。

编辑

由于即使 userIDuseEffect 钩子使用,您也只想运行效果,在您的情况下,可以将空数组作为第二个参数并忽略 eslint 警告。您也不能省略 useEffect 挂钩的任何依赖项,并在 useEffect 挂钩中使用某些条件,在效果第一次运行并更新状态后评估为 false。

我个人建议您尝试更改组件的结构,以便您不必首先处理此类问题。

【讨论】:

  • 您的评论很有帮助,但请您也看看这个吗?我试图理解为什么 useEffect 不使用词法范围来读取更新的值?如果不使用 useEffect,我怎么能创建同样陈旧的关闭场景?谢谢..
  • useEffect 的@MuhammadMuzammil 回调函数在周围范围上有一个闭包;它只看到定义它的范围内的值。此行为并非特定于 React 和 useEffect;这就是所有 javascript 函数的行为方式。 “如果不使用 useEffect,我怎么能创建同样陈旧的闭包场景?” - 每当您在 javascript 中创建函数时,它都会在周围范围内有一个闭包; here's 一个简单的例子。
  • "useEffect 的回调函数在周围作用域上有一个闭包" - 一个函数在其周围作用域上关闭并且还继承其父函数的词法作用域所以创建一个作用域链。那么 render 方法中的变量不应该也落入效果回调的词法范围链中吗?还是我错过了什么?
  • @MuhammadMuzammil "并且还继承了其父函数的词法作用域" - 对于useEffect钩子的回调函数,react组件是父函数及其作用域是 useEffect 钩子的回调函数关闭的内容。当一个组件重新渲染时,这实际上是再次调用您的组件,从而创建一个新的范围;这个新作用域与第一次调用组件时创建的作用域没有链接。请参阅我在之前评论中链接的代码示例。
  • @MuhammadMuzammil 直到再次执行 useEffect 钩子,设置一个新的回调函数,该函数在新组件范围内有一个闭包,之前设置的 useEffect 钩子的回调将继续记录陈旧状态或它在旧范围内关闭的道具值。
【解决方案2】:

看起来userId 确实应该是一个依赖项,因为如果它发生变化,您需要再次运行查询。

我认为您可以放弃对username 的检查,并始终在且仅当userId 更改时才获取:

useEffect(() => {
    if(props.userID) {
        fetchUsername(props.userID)
            .then((username) => setUsername(username))
    }
}, [props.userID])

一般来说,您希望列出效果中的所有闭包变量,以避免执行效果时过时的引用。

-- 编辑以解决 OP 问题: 因为在您的用例中,您知道您只想在初始挂载上执行操作,传递一个空的依赖数组是一种有效的方法。

另一种选择是跟踪获取的用户 ID,例如

const fetchedIds = useRef(new Set())

每当您获取新 ID 的用户名时,您都可以更新 ref:

fetchedIds.current.add(newId)

你可以测试一下:

if (props.userID && !fetchedIds.current.has(props.userID)) {
   // do the fetch
}

【讨论】:

  • 是的,我会这样做。但是从 eslint react 规则集中得到一个警告告诉我这有问题。当我运行 eslint --fix 时,我什至会自动修复!
  • 请注意,我删除了对 username 的依赖 - 您在我的答案中使用代码时遇到了什么错误?
  • 你是对的,我没有错误。但是,当我选择一个建议时 - 它会触发 onChange,设置一个新的 props.userID 并再次获取用户名(我已经从建议请求中获得)。我刚刚用一个更详尽的例子编辑了我的帖子,很抱歉之前没有这样做;-)
  • 请添加更改userID 的代码,我不确定我是否关注你
  • 用户 ID 是一个受控输入,因此它需要一个值和一个 onChange 回调。我将编辑我的帖子
【解决方案3】:

如果您确定在挂载 InputUser 组件之前,所有依赖的 props 都已填充并且它们具有正确的值,则在 }, []) 行之前添加 eslint-disable-next-line react-hooks/exhaustive-deps,但如果依赖的 props 在第一次组件挂载时没有值, 所以你必须将它们添加到 useEffect 依赖项中。

【讨论】:

  • 这条规则是由 react 核心团队编写的,所以我的意思是,如果它大喊大叫——我的设计可能有问题。这才是我的问题的重点;-)
猜你喜欢
  • 1970-01-01
  • 2020-12-07
  • 2021-07-15
  • 2021-12-06
  • 2020-06-18
  • 2019-11-26
  • 2021-10-21
  • 2020-06-09
  • 2021-10-31
相关资源
最近更新 更多