💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡
我们分析了这篇文章的评论以提供一些指导: https :
💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡
这是一个新的 ESLint 规则,用于验证useEffect
等 Hook 的依赖项列表,防止过时的闭包陷阱。 在大多数情况下,它具有自动修复功能。 我们将在接下来的几周内添加更多文档。
yarn add eslint-plugin-react-hooks<strong i="18">@next</strong>
# or
npm install eslint-plugin-react-hooks<strong i="19">@next</strong>
ESLint 配置:
{
"plugins": ["react-hooks"],
// ...
"rules": {
"react-hooks/rules-of-hooks": 'error',
"react-hooks/exhaustive-deps": 'warn' // <--- THIS IS THE NEW RULE
}
}
验证规则有效的简单测试用例:
function Foo(props) {
useEffect(() => {
console.log(props.name);
}, []); // <-- should error and offer autofix to [props.name]
}
如果这个新的react-hooks/exhaustive-deps
lint 规则为您触发,但您认为您的代码是正确的,请在此问题中发帖。
请包括这三件事:
这对你来说可能很简单——但对我们来说一点也不简单。 如果您的评论不包含其中任何一个(例如没有 CodeSandbox 链接),我们将隐藏您的评论,因为否则很难跟踪讨论。 感谢您尊重每个人的时间,包括他们。
这个线程的最终目标是找到常见的场景并将它们转换成更好的文档和警告。 这只有在有足够的细节可用时才会发生。 带有不完整代码片段的匆忙评论会显着降低讨论的质量 - 以至于不值得。
这个例子有回复: https :
这是一个不受控制的 Checkbox 组件,它使用defaultIndeterminate
属性来设置初始渲染时的不确定状态(这只能在 JS 中使用 ref 完成,因为没有indeterminate
元素属性)。 这个 prop 的行为类似于defaultValue
,它的值仅用于初始渲染。
该规则抱怨依赖项数组中缺少defaultIndeterminate
,但添加它会导致组件在传递新值时错误地覆盖不受控制的状态。 依赖数组不能完全删除,因为它会导致不确定状态完全被 prop 控制。
我看不出有任何方法可以区分这种情况和规则意图捕获的案例类型,但是如果最终规则文档可以包含建议的解决方法,那就太好了。 🙂
回复: https :
@billyjanitsch这会起作用吗? https://codesandbox.io/s/jpx1pmy7ry
我为indeterminate
添加了useState
,它被初始化为defaultIndeterminate
。 然后效果接受[indeterminate]
作为参数。 您目前没有更改它 - 但如果您稍后更改,我想这也会起作用吗? 因此,代码更好地预测了未来可能的用例。
所以我得到了以下(边缘?)案例,我传递了一些 html 并将它与dangerouslySetInnerHtml
来更新我的组件(一些编辑内容)。
我没有使用html
道具,而是使用 ref ,我可以使用ref.current.querySelectorAll
来做一些魔术。
现在我需要添加html
在我的依赖useEffect
即使我没有明确地使用它。 这是我实际上应该禁用规则的用例吗?
这个想法是拦截来自编辑内容的所有链接点击并跟踪一些分析或做其他相关的事情。
这是来自真实组件的淡化示例:
https://codesandbox.io/s/8njp0pm8v2
我正在使用 react-redux,因此当从mapDispatchToProps
传递道具中的动作创建者并在挂钩中使用该动作创建者时,我收到exhaustive-deps
警告。
所以我显然可以将 redux action 添加到依赖数组中,但是因为 redux action 是一个函数并且永远不会改变,所以这是不必要的对吗?
const onSubmit = React.useCallback(
() => {
props.onSubmit(emails);
},
[emails, props]
);
我希望 lint 将 deps 修复为[emails, props.onSubmit]
,但现在它总是将 deps 修复为[emails, props]
。
- 一个 CodeSandbox 演示了一个仍然表达您的意图的最小代码示例(不是“foo bar”,而是您正在实现的实际 UI 模式)。
https://codesandbox.io/s/xpr69pllmz
- 用户执行的步骤以及您希望在屏幕上看到的内容的说明。
用户将添加电子邮件并将这些电子邮件邀请到应用程序。 我故意将 UI 的其余部分删除到button
因为它们与我的问题无关。
- 钩子/组件的预期 API的解释。
我的组件有一个道具onSubmit: (emails: string[]) => void
。 当用户提交表单时,它将以emails
状态调用。
编辑:在https://github.com/facebook/react/issues/14920#issuecomment -467494468 中回答
这是因为技术上
props.foo()
将props
本身作为this
传递给foo
调用。 所以foo
可能隐含地依赖于props
。 不过,对于这种情况,我们需要更好的信息。 最佳实践总是解构。
它没有考虑到在集成 3rd 方库时挂载和更新可以明显不同。 更新效果不能包含在挂载中(并完全删除数组),因为不应在每次渲染时销毁实例
你好,
不确定我的代码有什么问题:
const [client, setClient] = useState(0);
useEffect(() => {
getClient().then(client => setClient(client));
}, ['client']);
我得到了React Hook useEffect has a complex expression in the dependency array. Extract it to a separate variable so it can be statically checked
@joelmoss @sylvainbaronnet我感谢您的反馈,但我隐藏了您的评论,因为它们没有包含我们在问题顶部要求的信息。 这使得这个讨论对每个人来说都不必要地困难,因为缺少上下文。 如果您再次发帖并包含所有相关信息,我很乐意继续对话(请参阅顶部帖子)。 谢谢你的理解。
我相信代码不应该出错。 它现在在第 35 行,说setLastName
应该包含在数组中。 关于该怎么做的任何想法? 我在做一些意想不到的事情吗?
我完全理解,我通常会为你做所有这些,但在我的具体情况下,没有任何代码对我来说是独一无二的。 这只是关于使用在钩子外部定义的函数(即 redux 动作创建者)并在钩子内使用是否应该要求将该函数添加为钩子 dep 的问题。
如果您仍然需要更多信息,很高兴创建一个代码沙盒。 谢谢
@joelmoss我认为我的重现也涵盖了您的情况。
是的,CodeSandbox 仍然会有所帮助。 请想象一下整天在人们的代码片段之间进行上下文切换是什么感觉。 这是一个巨大的精神损失。 它们看起来都不一样。 当您需要记住人们如何在 Redux 中使用动作创建器或其他一些 React 外部概念时,就更难了。 这个问题对你来说可能听起来很明显,但对我来说你的意思并不明显。
@gaearon我明白这是有道理的,实际上它对我
如果您只想运行效果并仅将其清理一次(在装载和卸载时),您可以传递一个空数组 ([]) 作为第二个参数。
非常感谢您的澄清。 (并且抱歉跑题了)
这是我正在实施的代码版本。 它看起来并不多,但模式与我的真实代码 100% 相同。
https://codesandbox.io/s/2x4q9rzwmp?fontsize=14
在这个例子中一切都很好! 除了短绒问题。
我有几个这样的文件,我在效果中执行一个函数,但我只希望它在满足某些条件时运行 - 例如,更改系列 Id。 我不想在数组中包含该函数。
这是我在沙箱代码上运行 linter 时得到的信息:
25:5 warning React Hook useEffect has a missing dependency: 'fetchPodcastsFn'. Either include it or remove the dependency array react-hooks/exhaustive-deps
我的问题是:这是它应该如何表现(短绒规则或钩子数组规则)? 有没有更惯用的方式来描述这种效果?
@svenanders ,我很好奇您不想包含fetchPodcastsFn
? 是因为你发现它在每次渲染时都会改变吗? 如果是,您可能想要记住该函数或将其设为静态(以防它没有任何参数)
一方面 - 这是关于清晰度的。 当我查看函数时,我想很容易地理解它应该何时触发。 如果我在数组中看到 _one_ id,那就很清楚了。 如果我看到那个 id 和一堆函数,它会变得更加混乱。 我必须花费时间和精力,甚至可能调试函数,以了解发生了什么。
我的函数在运行时不会改变,所以我不知道记住它们是否重要(这个特别是一个触发史诗的调度动作,最终导致状态改变)。
https://codesandbox.io/s/4xym4yn9kx
用户访问页面上的路由,但他们不是超级用户,因此我们希望将他们从页面重定向。 props.navigate
是通过路由器库注入的,因此我们实际上不想使用window.location.assign
来防止页面重新加载。
一切正常!
我像在代码沙箱中一样正确地放入了依赖项,但是 linter 告诉我依赖项列表应该有props
而不是props.navigate
。 这是为什么?
带有截图的推文! https://twitter.com/ferdaber/status/1098966716187582464
编辑:这可能有问题的一个有效原因是,如果navigate()
是一种依赖于绑定this
,在这种情况下,技术上如果props
任何内容发生变化,那么里面的内容this
也会改变。
代码沙盒: https ://codesandbox.io/s/711r1zmq50
预期API:
这个钩子允许你去抖动任何快速变化的值。 在指定时间段内未调用 useDebounce 钩子时,去抖动值将仅反映最新值。 当与 useEffect 结合使用时,就像我们在秘籍中所做的那样,您可以轻松确保不会过于频繁地执行 API 调用等昂贵的操作。
脚步:
该示例允许您搜索 Marvel Comic API 并使用 useDebounce 来防止 API 调用在每次击键时被触发。
例如考虑useDebounce Hook 。 在现实世界的使用中(至少在我们的使用中), delay
在第一次渲染后不会改变,但我们会在每次重新渲染时检查它是否发生了变化。 所以,在这个钩子中, useEffect
的第二个参数最好是[value]
而不是[value, delay]
。
请不要认为浅层平等检查非常便宜。 当策略性地放置它们时,它们可以帮助您的应用程序,但仅仅使每个组件都纯净实际上会使您的应用程序变慢。 权衡。
我认为将所有内容添加到依赖项数组中,与在任何地方使用纯组件具有相同(甚至更糟)的性能问题。 因为,在很多情况下我们知道我们使用的某些值不会改变,所以我们不应该将它们添加到依赖项数组中,因为正如@gaearon所说,浅层相等性检查不是很便宜,这可以使我们的应用程序变慢。
我有一些关于启用此规则以按照惯例自动处理自定义钩子的反馈。
我可以在源代码中看到,有一些意图允许人们指定一个正则表达式来按名称捕获自定义钩子:
React 团队会如何看待带有依赖数组的自定义钩子的额外命名约定? 钩子已经遵循以use
为前缀的约定,以便被这个插件检测为钩子。 我提议的用于检测依赖于某些依赖项的自定义钩子的约定是某种后缀,例如WithDeps
,这意味着完整的自定义钩子名称可能类似于useCustomHookWithDeps
。 WithDeps
后缀会告诉插件最终的数组参数是依赖项之一。
该插件仍然可以额外支持正则表达式,但我认为通过允许库作者简单地导出后缀为WithDeps
的自定义钩子而不是强制库使用者为任何和所有自定义显式配置插件,我会看到很大的好处挂钩 3rd-party 或其他方式。
它警告并自动删除自定义相等性检查。
const myEqualityCheck = a => a.includes('@');
useCallback(
() => {
// ...
},
[myEqualityCheck(a)]
);
对于useEffect,我认为不应出现“不必要的依赖”警告,因为这些“依赖”会改变效果的触发方式。
假设我有两个计数器,父母和孩子:
执行:
const [parent, setParent] = useState(0);
const [child, setChild] = useState(0);
useEffect(
() => {
setChild(0);
},
[parent] // "unnecessary dependency: 'parent'"
);
详尽的deps规则给出了警告React Hook useEffect has an unnecessary dependency: 'parent'. Either exclude it or remove the dependency array.
我认为 linter 不应该提出这个建议,因为它改变了应用程序的行为方式。
parent
发生变化时, child
重置为零。parent
改变了useEffect
的行为,而不是将依赖标记为不必要的,而不应该建议我删除它。[编辑]
作为一种解决方法,我可以写一些类似的东西
const [parent, setParent] = useState(0)
const [child, setChild] = useState(0)
const previousParent = useRef(parent)
useEffect(() => {
if (parent !== previousParent.current) {
setChild(0)
}
previousParent.current = parent
}, [parent])
但是我喜欢的原始片段中有一个“诗歌”。
我认为问题归结为我们是否应该将依赖数组解释为性能优化与实际组件行为的机制。 React Hooks FAQ 使用性能作为你为什么要使用它的例子,但我没有解释这个例子意味着你应该只使用依赖数组来提高性能,相反我认为它是一种跳过效果调用的便捷方式一般来说。
这实际上是我使用过几次的模式,用于使某些内部缓存无效:
useEffect(() => {
if (props.invalidateCache) {
cache.clear()
}
}, [props.invalidateCache])
如果我不应该以这种方式使用它,请随时告诉我并忽略此评论。
@李博先生
关于什么
const [parent, setParent] = useState(0);
const [child, setChild] = useState(0);
const updateChild = React.useCallback(() => setChild(0), [parent]);
useEffect(
() => {
updateChild();
},
[updateChild]
);
我也不会理解你的那个片段。 只能根据您的代码假设依赖关系,但这可能是一个错误。 虽然我的提议不一定能解决这个问题,但它至少使它更加明确。
看起来这以前是通过getDerivedStateFromProps
? 如何实现 getDerivedStateFromProps? 帮助?
@nghiepit我隐藏了您的评论,因为您忽略了第一篇文章中所需的清单(例如 CodeSandbox)。 请按照清单重新发布。 谢谢。
@eps1lon你会在useCallback
上useEffect
或useLayoutEffect
,因为它们可以包含有效的逻辑,但规则应该保留在useCallback
, useMemo
等
我遇到了@MrLeebo在此处描述的一些相同的问题/心理模型问题。 我的直觉是useEffect
依赖规则不能那么严格。 我有一个令人难以置信的人为例子,我正在为概念想法的基本证明而努力。 我知道这段代码很烂,而且这个想法并不是特别有用,但我认为它很好地说明了手头的问题。 我认为@MrLeebo表达了我很好的问题:
我认为问题归结为我们是否应该将依赖数组解释为性能优化与实际组件行为的机制。 React Hooks FAQ 使用性能作为你为什么要使用它的例子,但我没有解释这个例子意味着你应该只使用依赖数组来提高性能,相反我认为它是一种跳过效果调用的便捷方式一般来说。
https://codesandbox.io/s/5v9w81j244
我会专门看useEffect
钩useDelayedItems
。 现在,在依赖数组中包含items
属性会导致 linting 错误,但是完全删除该属性或数组会导致您不想要的行为。
useDelayedItems
钩子的基本思想是给定一个项目数组和一个配置对象(以毫秒为单位的延迟,初始页面大小),我最初将根据我配置的页面大小获得一个项目子集。 config.delay
过去后,项目现在将是完整的项目集。 当它收到一组新的项目时,钩子应该重新运行这个“延迟”逻辑。 这个想法是大列表延迟渲染的一个非常粗糙和愚蠢的版本。
感谢您对这些规则所做的所有工作,在开发过程中非常有帮助。 即使我的示例质量有问题,我也希望它能够对如何 ab/use 这些运算符提供一些见解。
编辑:我在代码和框内添加了一些关于行为意图的澄清注释。 我希望这些信息是足够的,但如果仍有任何疑问,请告诉我。
结合其他值的单个值怎么样?
示例: fullName 派生自firstName
和lastName
。 我们只想在fullName
更改时触发效果(例如当用户点击“保存”时),但也想访问它在效果中组合的值
向依赖项添加firstName
或lastName
会破坏事情,因为我们只想在fullName
更改后运行效果。
@aweary我不确定您从useEffect
道具更改间接获得什么价值。 似乎您的onClick
应该处理该“效果”。
https://codesandbox.io/s/0m4p3klpyw
至于结合其他值的单个值, useMemo
可能是您想要的。 您的示例中计算的延迟性质意味着它不会与您的链接行为完全 1:1 映射。
我将为这些示例创建代码和框链接并编辑这篇文章。
我有一个非常简单的规则:如果当前选项卡发生更改,请滚动到顶部:
https://codesandbox.io/s/m4nzvryrxj
useEffect(() => {
window.scrollTo(0, 0);
}, [activeTab]);
我得到了React Hook useEffect has an unnecessary dependency: 'activeTab'. Either exclude it or remove the dependency array
此外,当我迁移某些组件时,我想复制 componentDidMount 行为:
useEffect(() => {
...
}, []);
这个规则是在抱怨这个。 我犹豫是否将每个依赖项添加到 useEffect 数组。
最后,如果你在 useEffect 之前声明一个新的内联函数,像这样:
https://codesandbox.io/s/nr7wz8qp7l
const foo = () => {...};
useEffect(() => {
foo();
}, []);
你得到: React Hook useEffect has a missing dependency: 'foo'. Either include it or remove the dependency array
我不确定将 foo 添加到依赖项列表的感受。 在每个渲染中 foo 是一个新函数并且 useEffect 总是会运行?
@ksaldana1将副作用放在事件处理程序中是不够的。 这会导致副作用在提交实际更新之前发生,这可能会导致副作用比您想要的更频繁地触发。
使用useReducer
时它也不起作用,因为更新的状态在事件处理程序中不可用。
至于结合其他值的单个值,
useMemo
可能是您想要的。
如果此示例使用useMemo
它会中断,因为每次firstName
或lastName
更改时, useMemo
都会派生一个新的fullName
。 这里的行为是fullName
在单击“保存”按钮之前不会更新。
@aweary会这样吗? https://codesandbox.io/s/lxjm02j50m
我很想知道推荐的实现是什么。
我也很想知道更多关于你所说的一些事情。 你能指点我更多关于这些的信息吗?
处理程序中的触发和效果:
这会导致副作用在提交实际更新之前发生,这可能会导致副作用比您想要的更频繁地触发。
在事件处理程序中使用useReducer
状态:
更新的状态将不可用。
谢谢!
@bugzpodder有点不相关,但滚动调用应该在useLayoutEffect
而不是useEffect
。 当前导航到新路线时有明显的闪烁
不确定这是否是故意的:
当您从 props 调用函数时,linter 建议将整个 props 对象添加为依赖项。
精简版:
useEffect(function() {
props.setLoading(true)
}, [props.setLoading])
// ⚠️ React Hook useEffect has a missing dependency: 'props'.
完整版:代码沙盒
再试一次……但这次更简单;)
当在 props 中传递一个函数,或者实际上从任何地方传递一个函数,并在一个钩子中使用该函数时,我会收到一个exhaustive-deps
警告。
所以我显然可以将函数添加到依赖数组中,但是因为它是一个函数并且永远不会改变,所以这是不必要的对吗?
->沙盒
希望这就是你需要的一切,但我只是把@siddharthkp的沙箱分叉了,因为这证明了我的意思。
谢谢。
所以我显然可以将函数添加到依赖数组中,但是因为它是一个函数并且永远不会改变,所以这是不必要的对吗?
这是不正确的; 当父级重新渲染时,函数可以一直改变。
@siddharthkp
当您从 props 调用函数时,linter 建议将整个 props 对象添加为依赖项。
这是因为技术上props.foo()
将props
本身作为this
传递给foo
调用。 所以foo
可能隐含地依赖于props
。 不过,对于这种情况,我们需要更好的信息。 最佳实践总是解构。
当父级重新渲染时,函数可以一直改变。
当然,但是如果函数是在别处定义的,而不是从父组件定义的,它就永远不会改变。
当然,但是如果函数是在别处定义的,而不是从父组件定义的,它就永远不会改变。
如果你直接导入它,lint 规则不会要求你将它添加到 effect deps 中。 仅当它在渲染范围内时。 如果它在渲染范围内,那么 lint 规则不知道它来自哪里。 即使它今天不是动态的,也可能是明天有人更改父组件时。 所以指定它是正确的默认值。 如果它是静态的,那么指定它并没有什么坏处。
谢谢@gaearon
这是因为技术上
props.foo()
将props
本身作为this
传递给foo
调用。 所以foo
可能隐含地依赖于props
。 不过,对于这种情况,我们需要更好的信息。 最佳实践总是解构。
这也回答了我的问题。 谢谢! 😄
https://codesandbox.io/s/98z62jkyro
因此,我正在创建一个用于处理输入验证的库,方法是使用在每个输入的上下文中公开的 api 注册所有输入以注册自己。 我创建了一个名为 useRegister 的自定义钩子。
前任:
useEffect(
() => {
register(props.name, props.rules || []);
console.log("register");
return function cleanup() {
console.log("cleanup");
unregister(props.name);
};
},
[props.name, props.rules, register, unregister]
);
当强制 props.rules 成为依赖项的一部分时,它似乎以无限渲染循环结束。 Props.rules 是一组要注册为验证器的函数。 我只在提供数组作为依赖项时检测到这个问题。 在我的代码沙盒中,您可以看到它循环打开控制台。
仅将 props.name 作为依赖项即可使其按预期工作。 正如其他人指出的那样,强制执行依赖关系会改变应用程序的行为,在这种情况下,副作用是很严重的。
@bugzpodder
回复: https :
useEffect(() => {
window.scrollTo(0, 0);
}, [activeTab]);
这似乎是一个合法的案例。 我将放宽警告,只允许无关的 deps 用于效果。 (但不适用于useMemo
或useCallback
。)
此外,当我迁移某些组件时,我想复制 componentDidMount 行为:
这个规则是在抱怨这个。 我犹豫是否将每个依赖项添加到 useEffect 数组。
抱歉,您没有为此添加任何示例,因此我们失去了讨论的机会。 这正是 OP 帖子要求指定具体 UI 示例的原因。 你做了第一点,但不是这一点。 当你为它添加一个具体的例子时,我很乐意讨论它。 具体情况真的取决于它。
最后,如果你在 useEffect 之前声明了一个新的内联函数,像这样: https ://codesandbox.io/s/nr7wz8qp7l
在这种情况下,简单的解决方法是将doSomething
到效果中。 那么你不需要声明它。 或者,您可以useCallback
左右doSomething
。 我愿意放宽规则以允许省略仅使用声明的 deps 的函数。 但是,如果您有一个函数调用另一个函数,并将 prop 或 state 添加到其中一个函数中,这可能会令人困惑。 突然间,所有使用它的效果器都必须更新。 它可能会让人感到困惑。
我不知道这是对新规则的功能请求,还是可以改进exhaustive-deps
……
import React from 'react'
import emitter from './thing'
export const Foo = () => {
const onChange = () => {
console.log('Thing changed')
}
React.useEffect(() => {
emitter.on('change', onChange)
return () => emitter.off('change', onChange)
}, [onChange])
return <div>Whatever</div>
}
因为每次渲染都会创建onChange
函数,所以useEffect
钩子[onChange]
参数是多余的,最好删除:
React.useEffect(() => {
emitter.on('change', onChange)
return () => emitter.off('change', onChange)
- }, [onChange])
+ })
linter 可以检测到这一点,并建议您删除数组参数。
在某些情况下,我一直在维护一个数组项列表,结果发现其中一个或多个正在创建,并且无论如何每次渲染都会使钩子无效。
刚刚发布了[email protected]
其中包含一些针对此规则的修复和更好的消息。 没有什么突破性的,但应该解决一些案例。 下周我会看看剩下的。
我还在此处发布了省略“安全”函数 deps 的第一个可能步骤: https :
@gaearon 好主意。 这对于在使用 hooks 时有更好的风格肯定很有用🙏
@gaearon如果动作来自道具,它仍然不起作用。 考虑这个例子:
其中setScrollTop
是一个 redux 动作。
在Slider
组件的这个示例中,我使用useEffect
等待 DOM 可用,以便我可以挂载 noUiSlider 组件。 因此,我传入[sliderElement]
以确保该 ref 在效果运行时在 DOM 中可用。 我们也服务器渲染我们的组件,因此这也确保了 DOM 在渲染之前可用。 我在useEffect
使用的其他道具(即 min、max、onUpdate 等)是常量,因此我认为不需要将它们传递给效果。
这是在 codeandbox 中看到的效果:
const { min, max, step } = props;
const sliderElement = useRef();
useEffect(() => {
if (!sliderElement.current) {
return;
}
const slider = sliderElement.current;
noUiSlider.create(slider, {
connect: true,
start: [min, max],
step,
range: {
min: [min],
max: [max],
},
});
if (props.onUpdate) {
slider.noUiSlider.on('update', props.onUpdate);
}
if (props.onChange) {
slider.noUiSlider.on('change', props.onChange);
}
}, [sliderElement]);
@WebDeg-Brian 如果没有完整的 CodeSandbox 演示,我无能为力。 对不起。 见顶帖。
我发表了一些关于“功能永不改变”的常见误解:
https://overreacted.io/how-are-function-components-different-from-classes/
不完全相同的主题,但与此规则相关。
嗨@gaearon ,这是你让我在这里发布的例子(来自推特):)
基本上我正在尝试将我的库react-trap转换为钩子。
这只是元素外部/内部事件的陷阱。
我的问题是,如果useEffect
不依赖于状态值( trapped
),它有时会过时。
我写了一些评论和日志来演示。 查看useTrap.js
文件,注释和日志在useEffect
和preventDefaultHelper
函数中。
据我所知,如果一个值不在useEffect
那么它不应该是它的依赖项的一部分(如果我错了,请纠正我)。
e.preventDefault
)。我希望我在这里很清楚并且用例是可以理解的,如果我需要提供更多信息,请告诉我。 谢谢!
嗨@gaearon ,我在这里添加了我的自定义钩子,正如你在 Twitter 上建议的那样! 我正在努力寻找不跳过依赖关系的正确形状。
举个例子说得很详细,希望能讲得通俗易懂。
这是它的当前状态: react-async-utils/src/hooks/useAsyncData.ts
功能概述
帮助用户处理异步调用、生成的数据和过程中的状态。
triggerAsyncData
根据返回Promise
的getData
函数异步更新asyncData
状态。 triggerAsyncData
既可以作为效果调用,也可以由钩子用户“手动”调用。
挑战
triggerAsyncData
的效果的依赖是内在的。 triggerAsyncData
是效果的依赖项,但它是在每次渲染中创建的。 目前的思路:useMemo
/ useCallback
和triggerAsyncData
=> useMemo
/ useCallback
应该只用于性能优化AFAIK。triggerAsyncData
作为依赖项,而是使用triggerAsyncData
依赖项作为依赖项 => 迄今为止我发现的最佳选项。 但它打破了“详尽无遗”的规则。useMemo
/ useCallback
=> 恐怕他们经常不会。 如果他们这样做,那就太冗长了。asyncData
)。 这再次打破了“exhaustive-deps”规则。比我希望的更长的解释......但我认为它反映了我以正确的方式使用钩子的努力。 请让我知道是否还有其他我可以做的事情来使这些困难更清晰。
感谢大家在这里的辛勤工作!
嘿@gaearon,感谢您的辛勤工作。
一个最小的异步数据获取示例CodeSandbox示例。
用户应该看到从json api获取的 5 个 lorem ipsum 标题字符串。
我使用预期的 API 创建了用于数据获取的自定义钩子:
const { data, isLoading, isError } = useDataApi('https://jsonplaceholder.typicode.com/posts')
自定义useDataApi
钩子的内部结构:
...
const fetchData = async () => {
let response;
setIsError(false);
setIsLoading(true);
try {
response = await fetch(url).then(response => response.json());
setData(response);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
useEffect(
() => {
fetchData();
},
[url]
);
...
问题是这段代码
useEffect(
() => {
fetchData();
},
[url]
);
其中react-hooks/exhaustive-deps
会发出一个警告,我应该将fetchData
到我的依赖项数组中,并且url
应该被删除。
如果我把这个钩子改成
useEffect(
() => {
fetchData();
},
[fetchData]
);
然后它不断地发出请求并且永不停止,这当然是一个大问题。 我不确定我的代码是否有问题,或者react-hooks/exhaustive-deps
是否触发了误报。
任何帮助表示赞赏。 非常感谢。
PS 我读了你关于useEffect 不适合数据获取的评论,但是,反应文档声称Data fetching, setting up a subscription, and manually changing the DOM in React components are all examples of side effects
这让我相信数据useEffect
非常适合数据获取。 所以现在有点迷茫😕
@jan-stehlik 你应该用 useCallback 包装 fetchData。 我在这里进行了必要的更改https://codesandbox.io/s/pjmjxprp0m
非常有帮助,非常感谢@viankakrisna !
据我所知,如果一个值不在 useEffect 内,那么它不应该是它的依赖项的一部分(如果我错了,请纠正我)。
我认为你在这里错了。 或者说,有点糊涂。 您在效果中使用了handleEvent
。 但是你没有声明它。 这就是为什么它读取的值是陈旧的。
有趣的。
您正在效果中使用 handleEvent 。 但是你没有声明它。 这就是为什么它读取的值是陈旧的。
你是什么意思:_“但你没有申报”_?
它在效果下面声明(与所有其他处理程序相同)。
或者你的意思是因为处理程序正在使用状态值,并且效果是附加处理程序然后这意味着效果取决于该状态值?
或者你的意思是因为处理程序正在使用状态值,并且效果是附加处理程序然后这意味着效果取决于该状态值?
是的,如果你使用一个函数,你必须在 deps 中声明它(在这种情况下用useCallback
包装它以避免重新创建它),或者函数使用的所有内容。
好的,这对我来说是新闻! 感谢您的输入@gaearon :)
我只是想让它对我(和其他人?)来说非常清楚......
如果一个 effect 正在调用、传递或对一个函数做任何事情,我们需要传递给它的 deps 数组:
函数本身OR此函数使用的变量。
如果函数是在函数组件/自定义钩子中声明的,那么建议将它用useCallback
包裹起来,这样每次我们的组件或自定义钩子运行时就不会重新创建它。
我必须说我没有在文档上看到它。
您认为可以将其添加到_Note_ 部分吗?
笔记
输入数组不作为参数传递给效果函数。 不过,从概念上讲,这就是它们所代表的:效果函数中引用的每个值也应该出现在输入数组中。 将来,一个足够先进的编译器可以自动创建这个数组。
编辑
还有一件事,在我的示例中,当useCallback
包装handleEvent
(或任何其他出于这个原因的处理程序)时,它的 deps 是什么。 是它自己的event
吗?
我必须说我没有在文档上看到它。
文档说“效果函数中引用的每个值也应该出现在输入数组中”。 函数也是值。 我同意我们需要更好地记录这一点——这就是这个线程的全部内容:-) 我正在为一个专门用于此的新文档页面收集用例。
还有一件事,在我的示例中,当 useCallback 包装 handleEvent(或任何其他处理程序)时,它的 deps 是什么。 它是它自己的事件吗?
不明白你的意思。 它是外部函数引用的任何值。 就像在useEffect
。
我想我没有考虑过将函数作为依赖项。 我的心智模型是错误的,我认为只有当它作为道具或参数传入我的组件/钩子时,才将函数作为依赖项传递。 感谢您为我澄清这一点。
至于useCallback
,我是这样使用的:
const memoHandleEvent = useCallback(
handleEvent
);
当然,也将memoHandleEvent
作为useEffect
的依赖项传递给addEventListener
insead 真正的handleEvent
函数。 似乎有效,希望这是正确且惯用的方法。
注意useCallback
没有第二个参数不会做任何事情。
同样,一个完整的沙箱是必要的。 我无法通过这样的不完整描述来判断您的修复是否正确。
注意没有第二个参数的 useCallback 不做任何事情。
阿格! :做鬼脸:哈哈
同样,一个完整的沙箱是必要的。 我无法通过这样的不完整描述来判断您的修复是否正确。
哦,这是上面的链接。 我刚刚更新了它:)
请不要更新 CodeSandbox 链接:PI 需要它们原来的样子——否则我无法测试它们的 lint 规则。 如果您有两种不同的解决方案,能否创建两个单独的沙箱? 所以我可以检查每一个。
哎呀,对不起! :PI 超出了我帐户中的沙箱数量。 让我删除一些,然后再创建一个(并恢复原始更改)。
@gaearon这是useCallback
解决方案的第二个链接
我有一个我认为没问题的场景,但 linter 抱怨。 我的样本:
该示例试图表示的是,当用户单击按钮时,会根据之前提供的 Id 和函数发出请求以请求新数据。 如果 Id 发生变化,则不应请求新数据; 只有重新加载的新请求才会触发新的数据请求。
这里的例子有点做作。 在我的实际应用程序中,React 存在于一个 DIV 中,它是一个更大的 Web 应用程序的一小部分。 传入的函数是通过 Redux 和 mapDispatchToProps 进行的,其中创建操作时会获取 Id 并发出 ajax 请求以获取数据并更新存储。 refreshRequest 道具通过 React.createElement 传入。 在我最初的实现中,我在类组件中有如下所示的代码:
componentDidUpdate (prevProps) {
const { getData, someId, refreshRequest} = this.props;
if (prevProps.refreshRequest!== this.props.refreshRequest) {
getData(someId);
}
}
我正在尝试使用效果钩子实现相同的行为。 但是正如示例中所写的那样,linter 抱怨:
警告 React Hook useEffect 缺少依赖项:“getData”和“someId”。 包括它们或删除依赖项数组
如果我添加了 linter 想要的所有内容,那么如果用户单击示例 useEffect 中的任一按钮,就会触发。 但我只希望在按下请求新数据按钮时触发它。
希望这是有道理的。 如果有不清楚的地方,我很乐意澄清更多。 谢谢!
我刚刚发布了[email protected]
,它具有检测裸函数依赖项的实验性支持(如果没有useCallback
,它往往不是很有用)。 这是一个gif:
如果你们能在你的项目中尝试一下,看看感觉如何,我会很高兴的! (请在 https://github.com/facebook/react/pull/15026 中评论该特定流程。)
明天我将在这个线程的例子上尝试它。
我还没有尝试过,但我想知道它是否涉及起重。 这只是我在现场想到的一个“最小示例”,所以它对任何事情都没有用,但我确实使用了很多提升声明,以便更容易看到return
语句。
function Component() {
useEffect(() => {
handleChange
}, [handleChange])
return null
function handleChange() {}
}
如果规则有一个选项可以将另一个函数配置为像useEffect
就linter 而言的行为,那就太好了。 例如,我添加了这个,所以我可以轻松地将异步函数用于执行 AJAX 调用的效果 - 但是,像这样我失去了所有exhaustive-deps
linting 好处:
const useAsyncEffect = (fn, ...args) => {
/* eslint-disable react-hooks/exhaustive-deps */
useEffect(() => {
fn();
}, ...args);
};
编辑:没关系,我只是注意到使用规则的additionalHooks
选项已经可以做到这一点。
大家好,
我有一个例子https://codesandbox.io/s/znnmwxol7l
以下是我想要的:
function App() {
const [currentTime, setCurrentTime] = React.useState(moment());
const currentMonth = React.useMemo(
() => {
console.log("RUN");
return currentTime.format("MMMM");
},
[currentTime.format("MMMM")] // <= this proplem [currentTime]
);
return (
<div className="App">
<h1>Current month: {currentMonth}</h1>
<div>
<button
onClick={() => setCurrentTime(currentTime.clone().add(1, "days"))}
>
+ 1 day
</button>
</div>
<div>{currentTime.toString()}</div>
</div>
);
}
但这就是我得到的:
const currentMonth = React.useMemo(
() => {
console.log("RUN");
return currentTime.format("MMMM");
},
[currentTime] // <= this proplem
);
每当currentTime
发生变化时, currentMonth
重新计算不必要的
或者我可以在下面做:
const currentMonth = React.useMemo(
() => {
return currentTime.format("MMMM");
},
[myEqualityCheck(currentTime)]
);
对不起,如果这已经得到回答,在上面的线程中看不到它:
仅在 mount 上运行 useEffect 钩子的方法是定义一个空的输入数组作为第二个参数。 但是,当这样做时,详尽的 deps 抱怨包括那些输入参数,这将改变效果,使其在更新时也运行。
仅在启用了详尽依赖的挂载上运行 useEffect 的方法是什么?
@einarq我想你需要确保你的安装useEffect
中的所有引用值永远不会改变。 这可以使用其他钩子来实现,比如useMemo
。 之后,无论此 ESlint 规则是否将所有引用添加到数组中(带自动修复),代码都将只执行一次。
@einarq正如线程中其他地方所述,如果没有 CodeSandbox,我们将无法帮助您。 因为答案实际上取决于您的代码。
@nghiepit你的例子对我来说没有意义。 如果您的输入是currentTime.format("MMMM")
那么useMemo
不会为您优化任何东西,因为您已经计算过了。 所以你只是不必要地计算了两次。
是否可以指定哪个参数索引是additionalHooks
选项的回调? 我看到我们现在在代码中假设这将是第一个https://github.com/facebook/react/blob/9b7e1d1389e080d19e71680bbbe979ec58fa7389/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js#L10 L1081
目前不可能。 我们的一般指导是自定义 Hook 不应该有deps
参数,因为很难考虑 deps 是如何组成的。 如果你使用一个函数,你可能想要让用户改为useCallback
。
@CarlosGines您能否按照 OP 帖子中的要求制作一个 CodeSandbox。 我问这个是有原因的——否则我很难验证更改。 尤其是当它是用 TypeScript 编写的时候。
我刚刚发布了[email protected]
,希望能提供更多有用的信息和更智能的启发式方法。 请在您的项目稳定之前尝试一下。
我希望对于这个线程中的大多数示例,它应该提供合理的建议来帮助您解决问题。
那应该是[email protected]
吗?
是的。
实际上刚刚发布了[email protected]
并做了一些小改动。
@gaearon你有没有设法让eslint
在代码和盒子内工作?
@einarq也许像这样可以工作吗?
const useDidMount = fn => {
const callbackFn = useCallback(fn)
useEffect(() => {
callbackFn()
}, [callbackFn])
}
是的,您必须弹出并将其添加到 ESLint 配置中,直到我们将其添加到那里。 您可以在分支中弹出而不合并它。 :-)
你好,
首先:非常感谢您与社区的密切合作以及您所做的出色工作!
我们在围绕Fetch
API 构建的自定义钩子方面遇到了问题。 我创建了一个 Codesandbox 来演示这个问题。
https://codesandbox.io/s/kn0km7mzv
注意:该问题(见下文)导致无限循环,我不想 DDoS jsonplaceholder.typicode.com
用于演示该问题。 因此,我使用计数器包含了一个简单的请求限制器。 这不是证明问题所必需的,但是向这个伟大的项目发出无限数量的请求感觉是错误的。
这个想法是为了更容易处理 API 请求的 3 种可能状态:加载、成功和错误。 因此,我们构建了一个自定义钩子useFetch()
,它返回 3 个属性isLoading
、 response
和error. It makes sure that either
响应or
错误is set and updates
isLoading . As the name implies, it uses the
Fetch` API。
为此,它使用了 3 个useState
钩子:
const [isLoading, setIsLoading] = useState(false);
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
和一个useEffect
钩子:
useEffect(
() => {
fetch(url, fetchConfig)
.then(/* Handle fetch response... See Codesandbox for the actual implementation */)
},
[url]
);
只要useEffect
的依赖项数组只包含[url]
它就可以正常工作。 如果我们添加fetchConfig
(= [url, fetchConfig]
),则会导致无限循环。 对于我们的特定用例,仅在url
更改时重新运行效果就足够了,但 linter 不允许仅使用[url]
(使用v1.4.0
和v1.5.0-beta.1
)。
在自定义钩子结束时,3 个状态变量作为对象返回:
return {
error,
isLoading,
response
};
我不确定这是否是就这个问题寻求指导的正确地方,因为 linting 建议是有道理的,我猜。 对不起,如果不是。
如果fetchConfig
发生变化怎么办? 为什么你期望它是静态的?
好的。 我发布了[email protected]
以及上周的修复和改进。
该线程在查找导致问题的不同模式方面非常有帮助。
非常感谢你们所有人。 (尤其是那些提供沙箱的人。:-)
我不会亲自详细回复每个人,因为情况很多。
相反,我们将在接下来的几天内编写常见的食谱和问题,并从文档中链接到它们。 我将确保涵盖此线程中的每个常见模式(或要求在单独的问题中跟进稀有模式)。
示例发布后,我将在此处发表评论,并通过将链接附加到演示正确修复的相关答案/示例来编辑包含 CodeSandbox 的每条评论。 我希望这对你和未来的读者有所帮助。
干杯!
❤️
@timkraut您应该能够在 deps 上添加fetchConfig
,并且组件应该用备忘录将其包装起来,以便引用保持不变。
一个例子: https :
我的问题是现在组件需要知道钩子的实现细节......
我不确定这个线程是否仍然对示例讨论开放,但我仍然围绕最佳实践进行思考。 我的问题是控制何时使用依赖数组触发 useEffect 钩子并使用当时的值与需要声明单独的 ref 来绕过 lint 规则。
https://codesandbox.io/s/40v54jnkyw
在此示例中,我尝试定期自动保存输入值。
每 5 秒,代码会尝试自动保存当前值(现在只需将它们打印到屏幕上)。 linter 需要所有输入值都包含在依赖数组中,这会改变效果触发的频率。
useEffect(
() => {
if (autoSaveTick % 5 === 0) {
setAutosaveValue(
`${input1Value.value}, ${input2Value.value}, ${input3Value.value}`
);
}
},
[autoSaveTick]
);
我看到的替代方法是有一个类似于示例中定义的useTick
和useInterval
钩子的实现,其中有一个分配给引用的回调。 为了与 lint 规则保持一致,这似乎会导致不必要的函数重新定义。
我正在寻找编写效果的最佳实践,该效果在一个变量更改(变量 A)时运行,该效果在前一个变量更改(变量 A)时使用其他变量值(变量 B 和变量 C)。
如果
fetchConfig
发生变化怎么办? 为什么你期望它是静态的?
我可以将它添加到依赖项数组中。 在我们的特定业务案例中,这永远不会发生,但我想无论如何添加它是个好主意。
我的问题是现在组件需要知道钩子的实现细节......
这也正是我们遇到的问题:/ 在我们的实现中,我们甚至使用了一个额外的属性来更容易地设置主体,所以每当我们使用这个钩子时,我们必须使用 2 个useMemo()
调用。 尚不确定如何克服此限制。 如果您有想法,请告诉我!
在传递空数组时,如何运行仅具有一次依赖项的 useEffect 而不会出现规则抱怨?
说,像这样:
useEffect(() => {
init({ dsn, environment})
}, [])
我的问题是现在组件需要知道钩子的实现细节......
我不会说这些是实现细节。 这是你的 API。 如果传递了一个对象,我们假设它可以随时更改。
如果在实践中它始终是静态的,您可以将其作为 Hook 工厂的参数。 就像createFetch(config)
返回useFetch()
。 您在顶层调用工厂。
我明白这有点奇怪。 我们正在处理的useSubscription
也有类似的问题。 但由于这是一个常见问题,这可能意味着useMemo
实际上是一个合法的答案,人们应该习惯于在这些情况下这样做。
在实践中——我们将在未来更多地关注这一点。 但是您不应该将潜在的动态对象视为静态对象,因为这样用户就无法更改它。
@asylejmani您没有按照顶部帖子的要求提供有关您的用例的任何详细信息。 为什么您期望有人可以回答您的问题?
该规则的重点是告诉您environment
和dsn
可以随时间变化,您的组件应该处理它。 因此,要么您的组件有问题(因为它不处理对这些值的更改),要么您有一些无关紧要的独特情况(在这种情况下,您应该添加一个 lint 规则忽略注释来解释原因)。
在这两种情况下,您都不清楚您在问什么。 规则抱怨事情会发生变化,而您无法处理这种变化。 如果没有完整的示例,您就可以回答为什么您认为没有必要处理它。
@gaearon抱歉没有说得更清楚(它总是在一个人的脑海中听起来很清楚):) 我的问题更像是您将如何使用 useEffect 实现 componentDidMount。
我的印象是空数组可以做到这一点,但规则告诉我始终在数组中包含依赖项。
@asylejmani我认为像componentDidMount
这样的类生命周期方法最大的问题是我们倾向于将它视为一个孤立的方法,但实际上它是流程的一部分。
如果您在componentDidMount
引用某些内容,您很可能也需要在componentDidUpdate
处理它,否则您的组件可能会出错。
这就是规则试图解决的问题,您需要随着时间的推移处理值。
数字 1 是您将逻辑放在componentDidMount
/ useEffect
正文中的位置
数字 2 是您将逻辑放入componentDidUpdate
/ useEffect
deps 的地方
规则是抱怨你没有做流程的第 2 部分
@gaearon抱歉没有说得更清楚(它总是在一个人的脑海中听起来很清楚):) 我的问题更像是您将如何使用 useEffect 实现 componentDidMount。
我的印象是空数组可以做到这一点,但规则告诉我始终在数组中包含依赖项。
我认为我和@asylejmani在这里处于同一页面上,但我想你在说@gaearon是我们可能错误地仅在实际有依赖关系的情况下在 mount 上运行效果。
这是一个公平的声明吗? 我想我在想提供一个空数组有点像说“我知道我在做什么”,但我明白为什么你想要仍然保留规则。
抱歉还没有提供沙箱。 前几天晚上我开始使用 Create React App 示例,无法找到如何在沙箱上运行 eslint,然后在没有先保存的情况下重新加载浏览器时丢失了沙箱(假设 CodeSandbox 临时存储,我的错)。
然后我不得不去睡觉,从那以后就没有时间了。
无论如何,我明白你的意思,在正常情况下,最好包括这些依赖项,而不是假设它只在挂载上运行就足够了。
不过,这也可能有有效的用例,但解释或提出一个很好的例子有点困难,所以我会在需要时禁用内联规则。
@asylejmani是您的用例类似于https://github.com/facebook/react/issues/14920#issuecomment -466378650? 我认为规则不可能理解这种情况下的场景,因此我们只需要为该类型的代码手动禁用它。 在所有其他情况下,规则按其应有的方式工作。
不确定这是否有意义,但对我来说很常见的一种情况是这样的:
useEffect(() => {
if(!dataIsLoaded) { // flag from redux
loadMyData(); // redux action creator
}
}, []);
这两个依赖项都来自 redux。 数据(在这种情况下)应该只加载一次,并且动作创建者总是相同的。
这是特定于 redux 的,您的 eslint 规则无法知道这一点,所以我明白为什么它应该发出警告。 仍然想知道是否提供一个空数组应该只是禁用规则? 我喜欢这条规则告诉我如果我提供了一些但不是全部,或者如果我根本没有提供任何 deps。 空数组对我来说意味着不同的东西。 但这可能只是我:)
感谢您的辛勤工作! 并让我们作为开发人员的生活变得更好:)
我的用例要简单得多,我当然可以添加所有依赖项,它仍然可以正常工作,但是我的印象是,当您有一些依赖项但缺少其他依赖项时,规则会“警告”。
@einarq的用例是我使用过几次的东西,例如在“componentDidMount”上加载数据,如果没有(来自 redux 或其他)。
我也同意在这些情况下禁用内联规则是最好的选择。 在这种情况下,您确切地知道自己在做什么。
我相信我的整个困惑是 [] vs [some],当然感谢@gaearon 所做的出色工作:)
我认为我和@asylejmani在这里处于同一页面上,但我想你在说@gaearon是我们可能错误地仅在实际有依赖关系的情况下在 mount 上运行效果。 这是一个公平的声明吗?
是的。 如果你的组件不处理 prop 的更新,它通常是有问题的。 useEffect
迫使你去面对它。 您当然可以解决它,但默认情况下是轻推您处理这些情况。 这条评论解释得很好: https :
这两个依赖项都来自 redux。 数据(在这种情况下)应该只加载一次,并且动作创建者总是相同的。
如果它是相同的,那么将它包含在依赖项中不会伤害你。 我想强调一下——如果你确定你的依赖项永远不会改变,那么列出它们并没有什么坏处。 但是,如果稍后它们发生变化(例如,如果父组件根据状态传递不同的函数),您的组件将正确处理。
仍然想知道是否提供一个空数组应该只是禁用规则?
不。提供一个空数组然后想知道为什么某些 props 或 state 是陈旧的实际上是最常见的错误。
>
很有道理,谢谢
2019 年 3 月 8 日 15:27,Dan Abramov [email protected]写道:
我认为我和@asylejmani在这里处于同一页面上,但我想你在说@gaearon是我们可能错误地仅在实际有依赖关系的情况下在 mount 上运行效果。 这是一个公平的声明吗?
是的。 如果你的组件不处理 prop 的更新,它通常是有问题的。 useEffect 的设计迫使你去面对它。 您当然可以解决它,但默认情况下是轻推您处理这些情况。 这条评论很好地解释了它:#14920(评论)。
这两个依赖项都来自 redux。 数据(在这种情况下)应该只加载一次,并且动作创建者总是相同的。
如果它是相同的,那么将它包含在依赖项中不会伤害你。 我想强调一下——如果你确定你的依赖项永远不会改变,那么列出它们并没有什么坏处。 但是,如果稍后它们发生变化(例如,如果父组件根据状态传递不同的函数),您的组件将正确处理。
仍然想知道是否提供一个空数组应该只是禁用规则?
不。提供一个空数组然后想知道为什么某些 props 或 state 是陈旧的实际上是最常见的错误。
—
你收到这个是因为你被提到了。
直接回复此邮件,在 GitHub 上查看,或将线程静音。
@骀
这会导致副作用在提交实际更新之前发生,这可能会导致副作用比您想要的更频繁地触发。
我不确定这意味着什么。 你能提供一个例子吗?
我们今天通过@threepointone对这些进行了传递。 这是一个总结:
useEffect
依赖由于存在合法场景,该规则不再阻止您向useEffect
添加“无关的” deps。
对于现在安全的情况,Linter 不会发出警告,但在所有其他情况下,它会为您提供更好的建议(例如将函数移动到效果中,或用useCallback
包装它)。
这不再产生 lint 违规,但是响应 props 重置状态的惯用方法是不同的。 此解决方案将具有额外的不一致渲染,因此我不确定它是否可取。
Hooks 尽可能地将您推向正确性。 如果您确实指定了 deps(在某些情况下您可以省略),我们强烈建议您甚至包括您认为不会更改的那些。 是的,在这个useDebounce
示例中,延迟不太可能改变。 但如果确实如此,它仍然是一个错误,但 Hook 无法处理它。 这也出现在其他场景中。 (例如 Hooks 与热重载更兼容,因为每个值都被视为动态的。)
如果您绝对坚持某个值是静态的,则可以强制执行它。
最安全的方法是在你的 API 中明确地这样做:
const useFetch = createFetch({ /* config object */});
const useDebounce = createDebounce(500);
const FormInput = createInput({ rules: [emailValidator, phoneValidator] });
那么它显然不能改变,除非你把它放在渲染中。 (这不是你的 Hook 的惯用用法。)但是说<Slider min={50} />
永远不会改变并不是真正有效的——有人可以很容易地将它改为<Slider min={state ? 50 : 100} />
。 事实上,有人可以这样做:
let slider
if (isCelsius) {
slider = <Slider min={0} max={100} />
} else {
slider = <Slider min={32} max={212} />
}
如果有人在状态中切换isCelsius
,则假定min
永远不会更改的组件将无法更新。 在这种情况下, Slider
是相同的并不明显(但这是因为它在树中具有相同的位置)。 因此,就更改代码而言,这是一个主要的利器。 React 的一个主要观点是更新呈现就像初始状态一样(您通常无法分辨哪个是哪个)。 无论是渲染 prop 值 B,还是从 prop 值 A 转到 B - 它的外观和行为都应该相同。
虽然这是不可取的,但在某些情况下,强制机制可能是一个钩子,当值发生变化时发出警告(但提供第一个)。 至少到那时它更有可能被注意到。
function useMyHook(a) {
const initialA = usePossiblyStaleValue(a);
// ...
}
function usePossiblyStaleValue(value) {
const ref = useRef(value);
if (process.env.NODE_ENV !== 'production') {
if (ref.current !== value) { // Or your custom comparison logic
console.error(
'Unlike normally in React, it is not supported ' +
'to pass dynamic values to useMyHook(). Sorry!'
);
}
}
return ref.current;
}
也可能存在您无法处理更新的合法情况。 例如,如果较低级别的 API 不支持它,例如 jQuery 插件或 DOM API。 在这种情况下,警告仍然是合适的,以便组件的使用者理解它。 或者,您可以制作一个包装器组件,在不兼容的更新中重置key
- 强制使用新的 props 进行干净的重新安装。 这对于像滑块或复选框这样的叶组件来说可能更可取。
首先,如果它是常量并提升到顶级范围,那么 linter 不会抱怨。 但这对来自道具或上下文的事物没有帮助。
如果它确实是常量,那么在 deps 中指定它不会有什么坏处。 例如,自定义 Hook 中的setState
函数返回到您的组件,然后您从效果中调用它。 lint 规则不够聪明,无法理解这样的间接性。 但另一方面,任何人都可以稍后在返回之前包装该回调,并可能在其中引用另一个 prop 或 state。 那么它就不会是恒定的! 如果你不能处理这些变化,你就会有令人讨厌的陈旧的 prop/state 错误。 所以指定它是一个更好的默认值。
然而,函数值必然是常数是一种误解。 由于方法绑定,它们在类中通常是常量,尽管这会产生其自身的一系列错误。 但一般来说,任何关闭函数组件中某个值的函数都不能被视为常量。 lint 规则现在更聪明地告诉您该做什么。 (如移动它的影响内部-最简单的修复-或与包装它useCallback
。)
与此相反,存在一个问题,即您会遇到无限循环(函数值总是在变化)。 我们现在在可能的情况下(在同一组件中)在 lint 规则中捕获它并建议修复。 但是,如果您将某些东西向下传递几个级别,那就很棘手了。
您仍然可以将其包装在useCallback
以解决该问题。 请记住,从技术上讲,函数更改是有效的,并且您不能在不冒错误风险的情况下忽略这种情况。 例如onChange={shouldHandle ? handleChange : null}
或在同一位置渲染foo ? <Page fetch={fetchComments /> : <Page fetch={fetchArticles />
。 甚至fetchComments
关闭父组件状态。 那可以改变。 对于类,它的行为会悄无声息地改变,但函数引用将保持不变。 所以你的孩子会错过那个更新——除了向孩子传递更多数据之外,你真的没有其他选择。 使用函数组件和useCallback
,函数标识本身会发生变化——但仅在必要时。 所以这是一个有用的属性,而不仅仅是要避免的障碍。
我们应该添加一个更好的解决方案来检测无限异步循环。 这应该会减轻最令人困惑的方面。 我们将来可能会为此添加一些检测。 你也可以自己写这样的东西:
useWarnAboutTooFrequentChanges([deps]);
这并不理想,我们需要考虑更优雅地处理这个问题。 我同意样病例这是非常讨厌的。 不违反规则的解决方法是将rules
设为静态,例如通过将 API 更改为createTextInput(rules)
,并将register
和unregister
包装到useCallback
。 更好的是,删除register
和unregister
,并将它们替换为单独放置dispatch
的单独上下文。 然后你可以保证你永远不会有一个与阅读它不同的函数标识。
我要补充一点,无论如何,您可能希望将useMemo
放在上下文值上,因为提供程序做了很多计算,如果没有新组件注册但它自己的父组件更新,这些计算会很遗憾地重复。 因此,这种方式使您可能没有注意到的问题更加明显。 虽然我同意我们需要在发生这种情况时让它更加突出。
完全忽略函数依赖会导致函数组件和 Hook 出现更严重的错误,因为如果你这样做,它们会一直看到陈旧的 props 和 state。 所以尽量不要,如果可以的话。
我很奇怪为什么这个例子对本质上是一个事件处理程序的东西使用效果。 在事件处理程序中执行相同的“日志”(我认为它可能是表单提交)似乎更合适。 如果我们考虑卸载组件时会发生什么,则尤其如此。 如果它在效果预定后立即卸载怎么办? 在这种情况下,诸如表单提交之类的事情不应该只是“不会发生”。 所以看起来效果在那里可能是一个错误的选择。
也就是说你仍然可以做你尝试过的事情——通过将fullName
改为setSubmittedData({firstName, lastName})
,然后[submittedData]
是你的依赖,你可以从中读取firstName
和lastName
。
当与诸如 jQuery 插件或原始 DOM API 之类的命令式东西集成时,可能会出现一些讨厌的东西。 也就是说,我仍然希望您能够在该示例中更多地巩固效果。
希望我没有忘记任何人! 如果我做了或者有什么不清楚的地方,请告诉我。 我们将尽快将这些经验教训转化为一些文档。
@gaearon ,感谢您花时间深入探讨问题并将行动项目总结为不同类别。 您在结束摘要评论中错过了我的示例(@trevorgithub 的#14920(评论)) 。 (我当然感谢很多人的大量反馈;我认为我的原始评论在问题评论中间的隐藏项目部分丢失了)。
我假设我的样本属于“与命令式/遗留代码集成”,尽管可能还有其他类别?
在“与命令式/遗留代码集成”问题的情况下,听起来可能没有很多事情可以做。 在这些情况下,人们如何忽略这一警告? 我猜:
// eslint-disable-line react-hooks/exhaustive-deps
抱歉我错过了这个。
如果您通过 props 接收到一些数据,但在某些显式更改之前不想使用这些 props,这听起来像是对它进行建模的正确方法是具有派生状态。
您将其视为“我想忽略对道具的更改,直到另一个道具”。 但您也可以将其视为“我的组件在状态中具有获取功能。 当另一个道具发生变化时,它会从一个道具更新。”
不推荐一般派生状态,但这里似乎是您想要的。 实现这一点的最简单方法是:
const [currentGetData, setCurrentGetData] = useState(getData);
const [prevRefreshRequest, setPrevRefreshRequest] = useState(refreshRequest);
if (prevRefreshRequest !== refreshRequest) {
setPrevRefreshRequest(refreshRequest);
setCurrentGetData(getData);
}
useEffect(() => {
currentGetData(someId);
}, [currentGetData, someId]);
您还需要将useCallback
放在您要传递的getData
周围。
请注意,一般来说,传递异步函数以进行获取的模式对我来说似乎很阴暗。 我猜在你的情况下你使用 Redux,所以这是有道理的。 但是如果在父组件中定义了异步函数,那将是可疑的,因为您可能会遇到竞争条件。 您的效果没有清理,所以您如何知道用户何时选择了不同的 ID? 存在请求无序到达并设置错误状态的风险。 所以这只是需要记住的事情。 如果数据获取在组件本身中,那么您可以使用效果清理功能设置“忽略”标志以防止响应中出现 setState。 (当然,将数据移动到外部缓存通常是更好的解决方案——这也是 Suspense 的工作方式。)
在事件处理程序中执行相同的“日志”(我认为它可能是表单提交)似乎更合适。
@gaearon我看到的问题是在事件处理程序中执行此操作意味着在提交更新之前会发生副作用。 没有严格的保证组件将作为该事件的结果成功重新渲染,因此在事件处理程序中执行此操作可能为时过早。
例如,如果我想记录用户已成功提交新的搜索查询并正在查看结果。 如果出现问题并且组件抛出,我不希望该日志事件发生。
然后有这种副作用可能是异步的情况,因此使用useEffect
为您提供了清理功能。
还有useReducer
,我可能想要记录的值在事件处理程序中不可用。 但我认为这已经在你们的雷达上🙂
无论哪种情况,您推荐的方法可能就足够了。 以一种形式存储组合状态,您仍然可以访问它组合的各个值。
我有一个方便的钩子,用于用额外的参数包装函数并将它们传递下去。 它看起来像这样:
function useBoundCallback(fn, ...bound) {
return useCallback((...args) => fn(...bound, ...args), [fn, ...bound]);
}
(实际上有点复杂,因为它有一些额外的功能,但上面仍然显示了相关问题)。
用例是当组件需要将属性向下传递给子组件时,并希望子组件能够修改该特定属性。 例如,考虑一个列表,其中每个项目都有一个编辑选项。 父对象将一个值传递给每个子对象,以及一个回调函数以在它希望编辑该值时调用。 子级不知道它显示的是哪个项目 ID,因此父级必须修改回调以包含此参数。 这可以嵌套到任意深度。
此流程的简化示例如下所示: https :
问题是我用这个规则得到了 2 个 linter 错误:
React Hook (X) 缺少依赖项:'bound'。 包括它或删除依赖项数组
React Hook (X) 在其依赖数组中有一个 spread 元素。 这意味着我们无法静态验证您是否传递了正确的依赖项
将其更改为使用参数列表(即不使用扩展运算符)会破坏记忆,因为即使参数相同,也会在每次调用时创建列表。
想法:
嗨@gaearon ,我刚收到这个警告,我没有在任何地方找到讨论:
Accessing 'myRef.current' during the effect cleanup will likely read a different ref value because by this time React has already updated the ref. If this ref is managed by React, store 'myRef.current' in a variable inside the effect itself and refer to that variable from the cleanup function.
我觉得这个消息有点混乱。 我想它试图警告我清理时的 ref 当前值可能与效果体的值不同。 对?
如果是这种情况,并且意识到这一点,忽略此警告是否安全/合法?
我的案例,如果有趣的话: CodeSandbox
上下文:一些自定义数据获取钩子。 我在 ref 中使用计数器来防止竞争条件和未安装组件的更新。
我想我可以通过将 ref 读取隐藏在函数中,或为清理案例创建另一个布尔 ref 来规避此警告。 但是如果我可以忽略这个警告,我会发现不必要的冗长。
@骀
例如,如果我想记录用户已成功提交新的搜索查询并正在查看结果。 如果出现问题并且组件抛出,我不希望该日志事件发生。
是的,这听起来像是一个利基用例。 我认为当大多数人想要“副作用”时,他们指的是表单提交本身——而不是您查看提交的表单的事实。 在那种情况下,我提供的解决方案似乎很好。
@davidje13
当第二个错误也适用时,是否值得显示第一个错误?
请为更改 lint 规则的建议提交一个新问题。
有什么方法可以专门针对使用扩展运算符的地方禁用此规则?
如果您认为自己知道自己在做什么,则可以随时// eslint-disable-next-line react-hooks/exhaustive-deps
。
如果函数内变量的唯一使用是与扩展运算符一起使用,则在依赖项中使用扩展运算符是安全的,并且由于规则已经检测到这一点,因此肯定应该允许它。
请提交一个新问题。
@CarlosGines
我觉得这个消息有点混乱。 我想它试图警告我清理时的 ref 当前值可能与效果体的值不同。 对?
是的。
如果是这种情况,并且意识到这一点,忽略此警告是否安全/合法?
嗯.. 如果这会导致错误,则不会。 🙂
上下文:一些自定义数据获取钩子。 我在 ref 中使用计数器来防止竞争条件和未安装组件的更新。
是的,也许这个用例是合法的。 提交一个新问题来讨论请?
最有用的评论
我们今天通过@threepointone对这些进行了传递。 这是一个总结:
在 Lint 规则中修复
无关的
useEffect
依赖由于存在合法场景,该规则不再阻止您向
useEffect
添加“无关的” deps。在同一组件中但在效果之外定义的函数
对于现在安全的情况,Linter 不会发出警告,但在所有其他情况下,它会为您提供更好的建议(例如将函数移动到效果中,或用
useCallback
包装它)。用户代码中值得修复
在道具更改时重置状态
这不再产生 lint 违规,但是响应 props 重置状态的惯用方法是不同的。 此解决方案将具有额外的不一致渲染,因此我不确定它是否可取。
“我的非函数值是常数”
Hooks 尽可能地将您推向正确性。 如果您确实指定了 deps(在某些情况下您可以省略),我们强烈建议您甚至包括您认为不会更改的那些。 是的,在这个
useDebounce
示例中,延迟不太可能改变。 但如果确实如此,它仍然是一个错误,但 Hook 无法处理它。 这也出现在其他场景中。 (例如 Hooks 与热重载更兼容,因为每个值都被视为动态的。)如果您绝对坚持某个值是静态的,则可以强制执行它。
最安全的方法是在你的 API 中明确地这样做:
那么它显然不能改变,除非你把它放在渲染中。 (这不是你的 Hook 的惯用用法。)但是说
<Slider min={50} />
永远不会改变并不是真正有效的——有人可以很容易地将它改为<Slider min={state ? 50 : 100} />
。 事实上,有人可以这样做:如果有人在状态中切换
isCelsius
,则假定min
永远不会更改的组件将无法更新。 在这种情况下,Slider
是相同的并不明显(但这是因为它在树中具有相同的位置)。 因此,就更改代码而言,这是一个主要的利器。 React 的一个主要观点是更新呈现就像初始状态一样(您通常无法分辨哪个是哪个)。 无论是渲染 prop 值 B,还是从 prop 值 A 转到 B - 它的外观和行为都应该相同。虽然这是不可取的,但在某些情况下,强制机制可能是一个钩子,当值发生变化时发出警告(但提供第一个)。 至少到那时它更有可能被注意到。
也可能存在您无法处理更新的合法情况。 例如,如果较低级别的 API 不支持它,例如 jQuery 插件或 DOM API。 在这种情况下,警告仍然是合适的,以便组件的使用者理解它。 或者,您可以制作一个包装器组件,在不兼容的更新中重置
key
- 强制使用新的 props 进行干净的重新安装。 这对于像滑块或复选框这样的叶组件来说可能更可取。“我的函数值是常数”
首先,如果它是常量并提升到顶级范围,那么 linter 不会抱怨。 但这对来自道具或上下文的事物没有帮助。
如果它确实是常量,那么在 deps 中指定它不会有什么坏处。 例如,自定义 Hook 中的
setState
函数返回到您的组件,然后您从效果中调用它。 lint 规则不够聪明,无法理解这样的间接性。 但另一方面,任何人都可以稍后在返回之前包装该回调,并可能在其中引用另一个 prop 或 state。 那么它就不会是恒定的! 如果你不能处理这些变化,你就会有令人讨厌的陈旧的 prop/state 错误。 所以指定它是一个更好的默认值。然而,函数值必然是常数是一种误解。 由于方法绑定,它们在类中通常是常量,尽管这会产生其自身的一系列错误。 但一般来说,任何关闭函数组件中某个值的函数都不能被视为常量。 lint 规则现在更聪明地告诉您该做什么。 (如移动它的影响内部-最简单的修复-或与包装它
useCallback
。)与此相反,存在一个问题,即您会遇到无限循环(函数值总是在变化)。 我们现在在可能的情况下(在同一组件中)在 lint 规则中捕获它并建议修复。 但是,如果您将某些东西向下传递几个级别,那就很棘手了。
您仍然可以将其包装在
useCallback
以解决该问题。 请记住,从技术上讲,函数更改是有效的,并且您不能在不冒错误风险的情况下忽略这种情况。 例如onChange={shouldHandle ? handleChange : null}
或在同一位置渲染foo ? <Page fetch={fetchComments /> : <Page fetch={fetchArticles />
。 甚至fetchComments
关闭父组件状态。 那可以改变。 对于类,它的行为会悄无声息地改变,但函数引用将保持不变。 所以你的孩子会错过那个更新——除了向孩子传递更多数据之外,你真的没有其他选择。 使用函数组件和useCallback
,函数标识本身会发生变化——但仅在必要时。 所以这是一个有用的属性,而不仅仅是要避免的障碍。我们应该添加一个更好的解决方案来检测无限异步循环。 这应该会减轻最令人困惑的方面。 我们将来可能会为此添加一些检测。 你也可以自己写这样的东西:
这并不理想,我们需要考虑更优雅地处理这个问题。 我同意样病例这是非常讨厌的。 不违反规则的解决方法是将
rules
设为静态,例如通过将 API 更改为createTextInput(rules)
,并将register
和unregister
包装到useCallback
。 更好的是,删除register
和unregister
,并将它们替换为单独放置dispatch
的单独上下文。 然后你可以保证你永远不会有一个与阅读它不同的函数标识。我要补充一点,无论如何,您可能希望将
useMemo
放在上下文值上,因为提供程序做了很多计算,如果没有新组件注册但它自己的父组件更新,这些计算会很遗憾地重复。 因此,这种方式使您可能没有注意到的问题更加明显。 虽然我同意我们需要在发生这种情况时让它更加突出。完全忽略函数依赖会导致函数组件和 Hook 出现更严重的错误,因为如果你这样做,它们会一直看到陈旧的 props 和 state。 所以尽量不要,如果可以的话。
对复合价值变化做出反应
我很奇怪为什么这个例子对本质上是一个事件处理程序的东西使用效果。 在事件处理程序中执行相同的“日志”(我认为它可能是表单提交)似乎更合适。 如果我们考虑卸载组件时会发生什么,则尤其如此。 如果它在效果预定后立即卸载怎么办? 在这种情况下,诸如表单提交之类的事情不应该只是“不会发生”。 所以看起来效果在那里可能是一个错误的选择。
也就是说你仍然可以做你尝试过的事情——通过将
fullName
改为setSubmittedData({firstName, lastName})
,然后[submittedData]
是你的依赖,你可以从中读取firstName
和lastName
。与命令式/遗留代码集成
当与诸如 jQuery 插件或原始 DOM API 之类的命令式东西集成时,可能会出现一些讨厌的东西。 也就是说,我仍然希望您能够在该示例中更多地巩固效果。
希望我没有忘记任何人! 如果我做了或者有什么不清楚的地方,请告诉我。 我们将尽快将这些经验教训转化为一些文档。