Children useCallback 依赖地狱
Children useCallback dependency hell
据我了解,您使用 useCallback 来防止重新渲染,所以我一直在每个函数中使用它,我的蜘蛛感官告诉我它已经听起来很糟糕。
但故事并没有就此结束,因为我一直在到处使用它,所以我现在将依赖项传递给我的所有 child 组件,它们不需要像下面那样担心示例:
编辑 // 沙盒:https://codesandbox.io/s/bold-noether-0wdnp?file=/src/App.js
Parent 组件(需要 colorButtons 和 currentColor)
const ColorPicker = ({onChange}) => {
const [currentColor, setCurrentColor] = useState({r: 255, g:0, b: 0})
const [colorButtons, setColorButtons] = useState({0: null})
const handleColorButtons = useCallback((isToggled, id) => {
/* code that uses colorButtons and currentColor */
}, [colorButtons, currentColor])
return <div className="color-picker">
<RgbColorPicker color={currentColor} onChange={setCurrentColor} />
<div className="color-buttons">
{
Object.entries(colorButtons).map(button => <ColorButton
//...
currentColor={currentColor}
onClick={handleColorButtons}
colorButtons={colorButtons}
/>)
}
</div>
</div>
}
1st child(需要样式和 currentColor 但可以从其 parent 免费获取 colorButtons)
const ColorButton = ({currentColor, onClick, id, colorButtons}) => {
const [style, setStyle] = useState({})
const handleClick = useCallback((isToggled) => {
/* code that uses setStyle and currentColor */
}, [style, currentColor, colorButtons])
return <ToggleButton
//...
onClick={handleClick}
style={style}
dependency1={style}
dependency2={currentColor}
dependency3={colorButtons}
>
</ToggleButton>
}
第二个child(只需要自己的变量但得到整个包)
const ToggleButton = ({children, className, onClick, style, data, id, onRef, ...dependencies}) => {
const [isToggled, setIsToggled] = useState(false)
const [buttonStyle, setButtonStyle] = useState(style)
const handleClick = useCallback(() => {
/* code that uses isToggled, data, id and setButtonStyle */
}, [isToggled, data, id, ...Object.values(dependencies)])
return <button
className={className || "toggle-button"}
onClick={handleClick}
style={buttonStyle || {}}
ref={onRef}
>
{children}
</button>
}
我在做 anti-pattern 吗?如果是,那是什么以及如何解决?感谢您的帮助!
React 钩子useCallback
useCallback
是一个可以在函数式 React 组件中使用的钩子。函数式组件是 return 是一个 React 组件并且 运行 在每个渲染器上的函数,这意味着在其主体中定义的所有内容每次都会获得新的引用标识。一个例外可以通过 React hooks 来实现,它可以在功能组件内部使用来互连不同的渲染并维护状态。这意味着,如果您使用 ref 保存对功能组件中定义的常规函数的引用,然后在稍后的渲染中将其与同一函数进行比较,它们将不相同(函数更改 referential identity 效果图之间):
// Render 1
...
const fnInBody = () => {}
const fn = useRef(null)
console.log(fn.current === fnInBody) // false since fn.current is null
fn.current = fnInBody
...
// Render 2
...
const fnInBody = () => {}
const fn = useRef(null)
console.log(fn.current === fnInBody) // false due to different identity
fn.current = fnInBody
...
根据 the docs、useCallback
returns “回调的记忆版本,仅当其中一个依赖项已更改时才会更改” 这在将回调传递给优化的 child 组件时非常有用 ",这些组件依赖于引用相等性来防止不必要的渲染".
总而言之,useCallback
将 return 一个函数,只要依赖关系不改变,它就会保持其引用身份(例如被记忆)。 returned 函数包含一个带有所用依赖项的闭包,因此一旦依赖项发生变化就必须更新。
这导致了上一个示例的更新版本
// Render 1
...
const fnInBody = useCallback(() => {}, [])
const fn = useRef(null)
console.log(fn.current === fnInBody) // false since fn.current is null
fn.current = fnInBody
...
// Render 2
...
const fnInBody = useCallback(() => {}, [])
const fn = useRef(null)
console.log(fn.current === fnInBody) // true
fn.current = fnInBody
...
您的用例
记住上面的描述,让我们看看你对useCallback
的使用。
案例一:ColorPicker
const handleColorButtons = useCallback((isToggled, id) => {
/* code that uses colorButtons and currentColor */
}, [colorButtons, currentColor])
每次 colorButtons
或 currentColor
更改时,此函数将获得一个新标识。 ColorPicker
本身会在设置这两者之一或当其属性 onChange
发生变化时重新呈现。当 currentColor
或 colorButtons
更改时,handleColorButtons
和 children 都应更新。 仅 child人受益于使用useCallback
的时间是仅 onChange
变化的时候。鉴于 ColorButton
是一个轻量级组件,并且 ColorPicker
重新渲染主要是由于对 currentColor
和 colorButtons
的更改,因此在这里使用 useCallback
似乎是多余的。
案例二:ColorButton
const handleClick = useCallback((isToggled) => {
/* code that uses setStyle and currentColor */
}, [style, currentColor, colorButtons])
这种情况与第一种情况类似。 ColorButton
在 currentColor
、onClick
、id
或 colorButtons
更改时重新呈现,而 children 在 handleClick
、[=44] 时重新呈现=]、colorButtons
或 currentColor
更改。 useCallback
到位后,道具 id
和 onClick
可能会更改而无需重新渲染 children(至少根据上面的可见代码),[= 的所有其他重新渲染32=] 将导致其 children 重新渲染。同样,child ToggleButton
是轻量级的,id
或 onClick
不太可能比任何其他 prop 更频繁地更改,因此在这里使用 useCallback
似乎是多余的还有。
案例三:ToggleButton
const handleClick = useCallback(() => {
/* code that uses isToggled, data, id and setButtonStyle */
}, [isToggled, data, id, ...Object.values(dependencies)])
这种情况很复杂,有很多依赖关系,但据我所知,无论哪种方式,大多数组件道具都会导致 handleClick
的“新版本”,并且 children 是轻量级组件,使用 useCallback
的论点似乎很弱。
那么什么时候应该使用useCallback
?
正如文档所说,在非常特殊的情况下使用它,当您需要一个函数在渲染之间具有引用相等性时...
您有一个包含 children 子集的组件,重新渲染的成本很高,并且应该比 parent 组件重新渲染的频率低得多,但由于函数而重新渲染每当 parent 重新渲染时,道具都会改变身份。对我来说,这个用例也表明设计不好,我会尝试将 parent 组件分成更小的组件,但我知道什么,也许这并不总是可能的。
您在功能组件的主体中有一个函数,该函数在另一个挂钩(列为依赖项)中使用,该挂钩每次都会触发,因为每当组件重新呈现时函数都会更改身份。通常,您可以通过 ignoring the lint rule 从依赖项数组中省略这样的函数,即使这不是书上的。其他建议是将这样的函数放置在组件主体之外或使用它的钩子内,但在某些情况下,none 可能会按预期运行。
很高兴知道与此相关的是...
位于函数组件之外的函数在渲染之间始终具有引用相等性。
由 useState
编写的设置器 return 将始终在渲染之间具有引用相等性。
我在评论中说过,当组件中有函数执行昂贵的计算且经常重新渲染时,您可以使用 useCallback
,但我有点偏离。假设您有一个函数,该函数基于某个 prop 进行大量计算,该 prop 的更改频率低于组件重新渲染的频率。然后你可以使用useCallback
和运行其中的一个函数return是一个函数,它的闭包有一些计算值
const fn = useCallback(
(
() => {
const a = ... // heavy calculation based on prop c
const b = ... // heavy calculation based on prop c
return () => { console.log(a + b) }
}
)()
, [c])
...
/* fn is used for something, either as a prop OR for something else */
你好将有效地避免每次组件重新呈现而不 c
更改时计算 a
和 b
,但更直接的方法是改为
const a = useMemo(() => /* calculate and return a */, [c])
const b = useMemo(() => /* calculate and return b */, [c])
const fn = () => console.log(a + b)
所以这里使用 useCallback
只会让事情变得更糟。
结论
了解编程中更复杂的概念并能够使用它们固然很好,但部分优点还在于知道何时使用它们。添加代码,尤其是涉及复杂概念的代码,其代价是可读性降低,代码更难调试,并且有许多不同的机制相互影响。因此,请确保您了解钩子,但如果可以的话,请始终尽量不要使用它们。特别是 useCallback
、useMemo
和 React.memo
(不是钩子,而是类似的优化),在我看来,只有在绝对需要时才应该引入。 useRef
有它自己的用例,但如果没有它也可以解决您的问题,也不应该介绍。
沙盒方面做得很好!总是使对代码的推理变得更容易。我冒昧地分叉了您的沙箱并对其进行了一些重构:sandbox link。如果需要,您可以自己研究这些变化。这是一个摘要:
你知道并使用 useRef
和 useCallback
很好,但我能够删除所有用途,使代码更容易理解(不仅通过删除这些用途,但也删除了使用它们的上下文)。
尝试使用和 React 来简化事情。我知道这不是 hands-on 建议,但你越深入 React,你就会越发意识到你可以用 React 做事 co-operating,或者你可以按照自己的方式做事。两者都可以,但后者会让你和其他人更加头疼。
尽量隔离一个组件的作用域;仅委托 child 组件所必需的数据,并不断询问您将状态保存在何处。早些时候,您在所有三个组件中都有点击处理程序,流程非常复杂,我什至懒得去完全理解它。在我的版本中,ColorPicker
中只有一个点击处理程序被委派了下来。只要单击处理程序负责,按钮就不必知道单击它们时会发生什么。闭包和将函数作为参数传递的能力是 React 和 Javascript.
的强大优势
键在 React 中很重要,很高兴看到您使用它们。通常,密钥应对应于唯一标识特定项目的内容。在这里使用 ${r}_${g}_${b}
很好,但这样我们就只能在按钮数组中获得每种颜色的一个样本。这是一个自然的限制,但如果我们不想要它,分配键的唯一方法是分配一个唯一的标识符,你这样做了。我更喜欢使用 Date.now()
但有些人可能出于某种原因反对它。如果您不想使用 ref.
,您也可以在功能组件之外使用全局变量
尝试以功能性(不可变)方式做事,而不是“旧的”Javascript 方式。例如,添加到数组时,使用 [...oldArray, newValue]
,分配给 object 时,使用 {...oldObject, newKey: newValue }
.
还有很多话要说,但我觉得你还是研究一下重构后的版本比较好,有什么想知道的可以告诉我。
据我了解,您使用 useCallback 来防止重新渲染,所以我一直在每个函数中使用它,我的蜘蛛感官告诉我它已经听起来很糟糕。
但故事并没有就此结束,因为我一直在到处使用它,所以我现在将依赖项传递给我的所有 child 组件,它们不需要像下面那样担心示例:
编辑 // 沙盒:https://codesandbox.io/s/bold-noether-0wdnp?file=/src/App.js
Parent 组件(需要 colorButtons 和 currentColor)
const ColorPicker = ({onChange}) => {
const [currentColor, setCurrentColor] = useState({r: 255, g:0, b: 0})
const [colorButtons, setColorButtons] = useState({0: null})
const handleColorButtons = useCallback((isToggled, id) => {
/* code that uses colorButtons and currentColor */
}, [colorButtons, currentColor])
return <div className="color-picker">
<RgbColorPicker color={currentColor} onChange={setCurrentColor} />
<div className="color-buttons">
{
Object.entries(colorButtons).map(button => <ColorButton
//...
currentColor={currentColor}
onClick={handleColorButtons}
colorButtons={colorButtons}
/>)
}
</div>
</div>
}
1st child(需要样式和 currentColor 但可以从其 parent 免费获取 colorButtons)
const ColorButton = ({currentColor, onClick, id, colorButtons}) => {
const [style, setStyle] = useState({})
const handleClick = useCallback((isToggled) => {
/* code that uses setStyle and currentColor */
}, [style, currentColor, colorButtons])
return <ToggleButton
//...
onClick={handleClick}
style={style}
dependency1={style}
dependency2={currentColor}
dependency3={colorButtons}
>
</ToggleButton>
}
第二个child(只需要自己的变量但得到整个包)
const ToggleButton = ({children, className, onClick, style, data, id, onRef, ...dependencies}) => {
const [isToggled, setIsToggled] = useState(false)
const [buttonStyle, setButtonStyle] = useState(style)
const handleClick = useCallback(() => {
/* code that uses isToggled, data, id and setButtonStyle */
}, [isToggled, data, id, ...Object.values(dependencies)])
return <button
className={className || "toggle-button"}
onClick={handleClick}
style={buttonStyle || {}}
ref={onRef}
>
{children}
</button>
}
我在做 anti-pattern 吗?如果是,那是什么以及如何解决?感谢您的帮助!
React 钩子useCallback
useCallback
是一个可以在函数式 React 组件中使用的钩子。函数式组件是 return 是一个 React 组件并且 运行 在每个渲染器上的函数,这意味着在其主体中定义的所有内容每次都会获得新的引用标识。一个例外可以通过 React hooks 来实现,它可以在功能组件内部使用来互连不同的渲染并维护状态。这意味着,如果您使用 ref 保存对功能组件中定义的常规函数的引用,然后在稍后的渲染中将其与同一函数进行比较,它们将不相同(函数更改 referential identity 效果图之间):
// Render 1
...
const fnInBody = () => {}
const fn = useRef(null)
console.log(fn.current === fnInBody) // false since fn.current is null
fn.current = fnInBody
...
// Render 2
...
const fnInBody = () => {}
const fn = useRef(null)
console.log(fn.current === fnInBody) // false due to different identity
fn.current = fnInBody
...
根据 the docs、useCallback
returns “回调的记忆版本,仅当其中一个依赖项已更改时才会更改” 这在将回调传递给优化的 child 组件时非常有用 ",这些组件依赖于引用相等性来防止不必要的渲染".
总而言之,useCallback
将 return 一个函数,只要依赖关系不改变,它就会保持其引用身份(例如被记忆)。 returned 函数包含一个带有所用依赖项的闭包,因此一旦依赖项发生变化就必须更新。
这导致了上一个示例的更新版本
// Render 1
...
const fnInBody = useCallback(() => {}, [])
const fn = useRef(null)
console.log(fn.current === fnInBody) // false since fn.current is null
fn.current = fnInBody
...
// Render 2
...
const fnInBody = useCallback(() => {}, [])
const fn = useRef(null)
console.log(fn.current === fnInBody) // true
fn.current = fnInBody
...
您的用例
记住上面的描述,让我们看看你对useCallback
的使用。
案例一:ColorPicker
const handleColorButtons = useCallback((isToggled, id) => {
/* code that uses colorButtons and currentColor */
}, [colorButtons, currentColor])
每次 colorButtons
或 currentColor
更改时,此函数将获得一个新标识。 ColorPicker
本身会在设置这两者之一或当其属性 onChange
发生变化时重新呈现。当 currentColor
或 colorButtons
更改时,handleColorButtons
和 children 都应更新。 仅 child人受益于使用useCallback
的时间是仅 onChange
变化的时候。鉴于 ColorButton
是一个轻量级组件,并且 ColorPicker
重新渲染主要是由于对 currentColor
和 colorButtons
的更改,因此在这里使用 useCallback
似乎是多余的。
案例二:ColorButton
const handleClick = useCallback((isToggled) => {
/* code that uses setStyle and currentColor */
}, [style, currentColor, colorButtons])
这种情况与第一种情况类似。 ColorButton
在 currentColor
、onClick
、id
或 colorButtons
更改时重新呈现,而 children 在 handleClick
、[=44] 时重新呈现=]、colorButtons
或 currentColor
更改。 useCallback
到位后,道具 id
和 onClick
可能会更改而无需重新渲染 children(至少根据上面的可见代码),[= 的所有其他重新渲染32=] 将导致其 children 重新渲染。同样,child ToggleButton
是轻量级的,id
或 onClick
不太可能比任何其他 prop 更频繁地更改,因此在这里使用 useCallback
似乎是多余的还有。
案例三:ToggleButton
const handleClick = useCallback(() => {
/* code that uses isToggled, data, id and setButtonStyle */
}, [isToggled, data, id, ...Object.values(dependencies)])
这种情况很复杂,有很多依赖关系,但据我所知,无论哪种方式,大多数组件道具都会导致 handleClick
的“新版本”,并且 children 是轻量级组件,使用 useCallback
的论点似乎很弱。
那么什么时候应该使用useCallback
?
正如文档所说,在非常特殊的情况下使用它,当您需要一个函数在渲染之间具有引用相等性时...
您有一个包含 children 子集的组件,重新渲染的成本很高,并且应该比 parent 组件重新渲染的频率低得多,但由于函数而重新渲染每当 parent 重新渲染时,道具都会改变身份。对我来说,这个用例也表明设计不好,我会尝试将 parent 组件分成更小的组件,但我知道什么,也许这并不总是可能的。
您在功能组件的主体中有一个函数,该函数在另一个挂钩(列为依赖项)中使用,该挂钩每次都会触发,因为每当组件重新呈现时函数都会更改身份。通常,您可以通过 ignoring the lint rule 从依赖项数组中省略这样的函数,即使这不是书上的。其他建议是将这样的函数放置在组件主体之外或使用它的钩子内,但在某些情况下,none 可能会按预期运行。
很高兴知道与此相关的是...
位于函数组件之外的函数在渲染之间始终具有引用相等性。
由
useState
编写的设置器 return 将始终在渲染之间具有引用相等性。
我在评论中说过,当组件中有函数执行昂贵的计算且经常重新渲染时,您可以使用 useCallback
,但我有点偏离。假设您有一个函数,该函数基于某个 prop 进行大量计算,该 prop 的更改频率低于组件重新渲染的频率。然后你可以使用useCallback
和运行其中的一个函数return是一个函数,它的闭包有一些计算值
const fn = useCallback(
(
() => {
const a = ... // heavy calculation based on prop c
const b = ... // heavy calculation based on prop c
return () => { console.log(a + b) }
}
)()
, [c])
...
/* fn is used for something, either as a prop OR for something else */
你好将有效地避免每次组件重新呈现而不 c
更改时计算 a
和 b
,但更直接的方法是改为
const a = useMemo(() => /* calculate and return a */, [c])
const b = useMemo(() => /* calculate and return b */, [c])
const fn = () => console.log(a + b)
所以这里使用 useCallback
只会让事情变得更糟。
结论
了解编程中更复杂的概念并能够使用它们固然很好,但部分优点还在于知道何时使用它们。添加代码,尤其是涉及复杂概念的代码,其代价是可读性降低,代码更难调试,并且有许多不同的机制相互影响。因此,请确保您了解钩子,但如果可以的话,请始终尽量不要使用它们。特别是 useCallback
、useMemo
和 React.memo
(不是钩子,而是类似的优化),在我看来,只有在绝对需要时才应该引入。 useRef
有它自己的用例,但如果没有它也可以解决您的问题,也不应该介绍。
沙盒方面做得很好!总是使对代码的推理变得更容易。我冒昧地分叉了您的沙箱并对其进行了一些重构:sandbox link。如果需要,您可以自己研究这些变化。这是一个摘要:
你知道并使用
useRef
和useCallback
很好,但我能够删除所有用途,使代码更容易理解(不仅通过删除这些用途,但也删除了使用它们的上下文)。尝试使用和 React 来简化事情。我知道这不是 hands-on 建议,但你越深入 React,你就会越发意识到你可以用 React 做事 co-operating,或者你可以按照自己的方式做事。两者都可以,但后者会让你和其他人更加头疼。
尽量隔离一个组件的作用域;仅委托 child 组件所必需的数据,并不断询问您将状态保存在何处。早些时候,您在所有三个组件中都有点击处理程序,流程非常复杂,我什至懒得去完全理解它。在我的版本中,
的强大优势ColorPicker
中只有一个点击处理程序被委派了下来。只要单击处理程序负责,按钮就不必知道单击它们时会发生什么。闭包和将函数作为参数传递的能力是 React 和 Javascript.键在 React 中很重要,很高兴看到您使用它们。通常,密钥应对应于唯一标识特定项目的内容。在这里使用
,您也可以在功能组件之外使用全局变量${r}_${g}_${b}
很好,但这样我们就只能在按钮数组中获得每种颜色的一个样本。这是一个自然的限制,但如果我们不想要它,分配键的唯一方法是分配一个唯一的标识符,你这样做了。我更喜欢使用Date.now()
但有些人可能出于某种原因反对它。如果您不想使用 ref.尝试以功能性(不可变)方式做事,而不是“旧的”Javascript 方式。例如,添加到数组时,使用
[...oldArray, newValue]
,分配给 object 时,使用{...oldObject, newKey: newValue }
.
还有很多话要说,但我觉得你还是研究一下重构后的版本比较好,有什么想知道的可以告诉我。