React hooks:为什么useEffect需要一个详尽的依赖项数组?

6

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

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

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

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 hook 中出现了以下警告

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

但是,如果我这么做,由于用户名值被hook本身更改,它将运行多次!由于它可以在没有所有依赖项的情况下工作,所以我想知道:

  • 如何清理地解决我的问题?
  • 这些依赖项是什么,为什么建议对它们进行详尽的处理?

1
如果我理解你更新后的问题正确,你不想重新运行那个更改所带来的影响,那么使用一个空数组就可以了。或者,你可以将所有依赖项都包含在 useEffect 的依赖数组中,但只在初始渲染时获取用户,使用一个条件,在 useEffect 至少执行一次后评估为 false。 - Yousaf
是的,我仍然收到这个eslint警告。但我猜那只是一个过于严格的规则,我应该禁用它。React Hook useEffect缺少依赖项:'props.userID'和'username'。要么包含它们,要么删除依赖项数组。eslint(react-hooks / exhaustive-deps) - maxime
我建议不要禁用此规则,原因在我的答案和链接的文章中已经提到。关于依赖项的虚假陈述只有在像你这样的特定情况下才可以,但通常情况下,您应该在依赖数组中包含所有效果的依赖项。如果可能的话,还应尽量改进应用程序的设计,以避免首先出现这种情况。 - Yousaf
useEffect dep's array differ from other hooks like useCallback and useMemo that could cause stale values if every dep isnt included. useEffect relies more on business logic and therefore imo a linter shouldn't decide your business logic. for the most part I usually list all the deps if possible, otherwise I ignore the rule for that scenario and leave a comment as to why. eslint-disable-next-line react-hooks/exhaustive-deps - Matt Aft
3个回答

2
这些依赖是什么? useEffect 接受一个可选的第二个参数,即一个依赖项数组。 useEffect 钩子的依赖项告诉它在其中一个依赖项更改时运行该效果。
如果不将可选的第二个参数传递给 useEffect 钩子,则每次组件重新呈现时都会执行它。空的依赖项数组指定您只想在组件的初始呈现后运行该效果一次。在这种情况下,useEffect 钩子几乎会像类组件中的 componentDidMount 一样运行。
为什么建议对它们进行详尽说明?
效果从定义它们的渲染中查看propsstate。因此,当您在useEffect钩子的回调函数中使用来自参与React数据流的函数组件范围的东西,例如props或state时,该回调函数将关闭该数据,并且除非使用新的值定义新效果的props和状态,否则您的效果将查看旧的props和state值。
以下代码片段演示了如果您在useEffect钩子的依赖项上撒谎会发生什么问题。
在以下代码片段中,有两个组件,AppUserApp 组件有三个按钮,并维护由 User 组件显示的用户的 id。用户 id 作为 prop 从 App 组件传递到 User 组件,User 组件从 jsonplaceholder API 获取传递的 id 对应的用户。
现在以下代码片段的问题是它不能正确工作。原因在于它关于 useEffect 钩子的依赖关系有误。useEffect 钩子依赖于 userID prop 来从 API 中获取用户,但由于我忘记将 userID 添加为依赖项,useEffect 钩子不会在每次更改 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>

上面的代码片段展示了如果你在依赖项上撒谎可能会出现的少数问题,这就是为什么你不能跳过或者撒谎依赖项对于useEffect钩子或任何其他有依赖项数组的钩子,例如useCallback钩子。 要修复前面的代码片段,你只需要将userID作为useEffect钩子的依赖项添加到依赖项数组中,这样每当userID属性更改并使用该属性相等的ID获取新用户时,它就会执行。

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 改变时,您的 effect 将不会获取新数据。
要了解省略 useEffect 钩子的依赖项的负面影响,请参见:

我该怎么干净地解决我的问题呢?

由于您的 effect 取决于 prop 值 userID,因此您应将其包含在依赖项数组中,以便在每次 userID 更改时始终获取新数据。
props.userID 添加为 useEffect hook 的依赖项将在每次 props.userID 更改时触发该 effect ,但问题在于你不必要地在 useEffect 中使用了 username。应该将其删除,因为这不是必需的,由于 username 值没有并且不应该决定何时获取新的用户数据。你只是想让 effect 在 props.userID 更改时运行。

您还可以通过使用 useReducer hook 来管理和更新状态,使操作与状态更新解耦。

Edit

既然你只想在 userIDuseEffect hook 使用时运行 effect,那么在你的情况下,将空数组作为第二个参数并忽略 eslint 警告是可以的。你也可以不省略 useEffect hook 的任何依赖项,并在 useEffect hook 中使用某些条件进行评估,在 effect 运行第一次并更新状态后返回 false。

个人建议尝试更改组件结构,以避免首先出现此类问题。


@MuhammadMuzammil useEffect 的回调函数在周围作用域上具有闭包;它只能看到在定义它的作用域中的值。这种行为不仅适用于 React 和 useEffect,而是所有 JavaScript 函数的行为。"我如何在不使用 useEffect 的情况下创建完全相同的陈旧闭包场景?" - 每当你在 JavaScript 中创建一个函数时,它都会在周围作用域上具有闭包;这里是一个简单的例子。 - Yousaf
"useEffect的回调函数在其周围作用域上具有闭包。" - 一个函数会关闭其周围的作用域,并继承其父函数的词法作用域,从而创建一个作用域链。那么,在渲染方法中的变量不也应该落在effect回调的词法作用域链中吗?或者我漏掉了什么? - Muhammad Muzammil
1
@MuhammadMuzammil - 在useEffect钩子的回调函数中,react组件是父函数,并继承其词法作用域。当组件重新渲染时,实际上是react再次调用您的组件,从而创建一个新的作用域;这个新的作用域与第一次调用组件时创建的作用域没有关联。请参见我在先前评论中提供的代码示例。 - Yousaf
1
@MuhammadMuzammil 直到 useEffect 钩子再次执行,设置一个新的回调函数,该函数在新组件范围内具有闭包,之前设置的 useEffect 钩子的回调将继续记录旧范围中封闭的过时状态或属性值。 - Yousaf
好的,我明白了。非常感谢。您是否可以在我的帖子上也发布这个答案呢? - Muhammad Muzammil
显示剩余2条评论

2

看起来userId确实应该是一个依赖项,因为如果它改变了,您希望重新运行查询。

我认为您可以放弃检查username,并且仅在userId更改时获取:

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

一般来说,您需要在effect中列出所有闭包变量,以避免执行effect时出现陈旧的引用。

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

另一个选项是跟踪获取的userID,例如:

const fetchedIds = useRef(new Set())

每当您获取新ID的用户名时,您可以更新引用:
fetchedIds.current.add(newId)

在你的效果中,你可以进行测试:

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

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

-1

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


这个规则是由React核心团队编写的,所以我的观点更多的是如果它发出警告——可能是我的设计有问题。那才是我提问的重点;-) - maxime

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接