首页 > 解决方案 > 孩子们使用回调依赖地狱

问题描述

据我了解,您使用 useCallback 来防止重新渲染,所以我一直在每个函数中使用它,我的蜘蛛感觉告诉我它已经听起来很糟糕。

但是故事并没有就此结束,因为我一直在到处使用它,我现在将依赖项传递给我的所有子组件,它们不需要担心,如下例所示:

编辑//沙盒: https ://codesandbox.io/s/bold-noether-0wdnp?file=/src/App.js

父组件(需要 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>
}

第一个孩子(需要 style 和 currentColor 但从其父级免费获得 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>
}

第二个孩子(只需要它自己的变量,但得到整个包)

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

我是否在做一个反模式,如果是,它是什么以及如何解决它?感谢您的帮助!

标签: javascriptreactjsreact-hooksdependenciesusecallback

解决方案


反应钩子useCallback

useCallback是一个可以在功能性 React 组件中使用的钩子。函数式组件是一个返回 React 组件并在每次渲染时运行的函数,这意味着在其主体中定义的所有内容每次都会获得新的引用标识。一个例外可以使用 React 钩子来完成,它可以在功能组件内部使用来互连不同的渲染和维护状态。这意味着如果您使用 ref 保存对功能组件中定义的常规函数​​的引用,然后将其与稍后渲染中的相同函数进行比较,它们将不相同(函数在渲染之间更改引用身份):

// 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
...

根据文档useCallback返回“仅在依赖项之一发生更改时才更改的回调的记忆版本” ,这在“将回调传递给依赖引用相等以防止不必要的渲染的优化子组件时很有用” 。

总而言之,useCallback只要依赖关系不改变,就会返回一个保持其引用身份(例如被记忆)的函数。返回的函数包含一个带有所用依赖项的闭包,因此必须在依赖项更改后进行更新。

这导致了上一个示例的更新版本

// 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.

情况1:ColorPicker

const handleColorButtons = useCallback((isToggled, id) => {
    /* code that uses colorButtons and currentColor */
}, [colorButtons, currentColor])

这个函数每次colorButtonscurrentColor变化都会得到一个新的身份。ColorPicker当设置这两个之一或它的 proponChange更改时,它本身会重新渲染。和孩子们都handleColorButtons应该更新currentColorcolorButtons改变。孩子们唯一受益于使用的时间是useCallback只有改变 onChange的时候。鉴于这ColorButton是一个轻量级组件,并且主要由于对and的ColorPicker更改而重新渲染,因此此处的使用似乎是多余的。currentColorcolorButtonsuseCallback

案例二:ColorButton

const handleClick = useCallback((isToggled) => {
    /* code that uses setStyle and currentColor */
}, [style, currentColor, colorButtons])

这是与第一种情况类似的情况。ColorButtoncurrentColor, onClick,idcolorButtons改变时重新渲染,子级在handleClick, style,colorButtonscurrentColor改变时重新渲染。useCallback就位后,propsid和可能在onClick不重新渲染孩子的情况下发生变化(至少根据上面的可见代码),所有其他重新渲染ColorButton将导致其孩子重新渲染。再一次,孩子ToggleButton很轻,id或者onClick不太可能比任何其他道具更频繁地改变,所以在useCallback这里使用似乎也是多余的。

案例3:ToggleButton

const handleClick = useCallback(() => {
        /* code that uses isToggled, data, id and setButtonStyle */
    }, [isToggled, data, id, ...Object.values(dependencies)])

这种情况很复杂,有很多依赖关系,但从我所见,无论如何,大多数组件道具将导致“新版本”handleClick的子组件是轻量级组件,使用的论点useCallback似乎很弱。

那么我应该什么时候使用useCallback呢?

正如文档所说,当您需要一个函数在渲染之间具有引用相等性时,在非常特定的情况下使用它......

  • 您有一个包含子子集的组件,这些子组件的重新渲染成本很高,并且应该比父组件的重新渲染频率低得多,但是由于每当父组件重新渲染时功能道具会改变身份而重新渲染。对我来说,这个用例也表明设计不好,我会尝试将父组件分成更小的组件,但我知道,也许这并不总是可能的。

  • 您在功能组件的主体中有一个函数,该函数在另一个钩子(列为依赖项)中使用,由于每当组件重新呈现时函数会更改身份,该函数每次都会触发。通常,您可以通过忽略 lint 规则来从依赖数组中省略这样的函数,即使这不是本书的规定。其他建议是将此类函数放置在组件主体之外或使用它的钩子内,但可能存在这些都无法按预期工作的情况。

很高兴知道与此相关的是...

  • 位于函数组件之外的函数将始终在渲染之间具有引用相等性。

  • 由返回的设置器useState将始终在渲染之间具有引用相等性。

我在评论中说,useCallback当组件中的函数进行昂贵的计算时,您可以使用该函数经常重新渲染,但我有点偏离了那里。假设您有一个函数,该函数基于一些比组件重新渲染更频繁的 prop 进行大量计算。然后您可以在其中使用useCallback并运行一个函数,该函数返回一个带有一些计算值的闭包的函数

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 */

这将有效地避免计算ab每次组件重新渲染而不c更改,但更直接的方法是改为

const a = useMemo(() => /* calculate and return a */, [c])
const b = useMemo(() => /* calculate and return b */, [c])
const fn = () => console.log(a + b)

所以这里的使用useCallback只是以一种糟糕的方式使事情复杂化。

结论

了解编程中更复杂的概念并能够使用它们是很好的,但部分优点也是知道何时使用它们。添加代码,尤其是涉及复杂概念的代码,是以降低可读性和代码更难与许多相互作用的不同机制进行调试为代价的。因此,请确保您了解这些钩子,但如果可以,请始终尽量不要使用它们。特别是useCallback, useMemoand React.memo(不是钩子,而是类似的优化),在我看来,只有在绝对需要时才应该引入。useRef有它自己的用例,但当你没有它就可以解决你的问题时也不应该介绍它。


在沙盒上做得很好!总是更容易推理代码。我冒昧地分叉了您的沙箱并对其进行了一些重构:沙箱链接。如果你愿意,你可以自己研究这些变化。这是一个摘要:

  • 你知道和使用很好,useRef但是useCallback我能够删除所有的使用,使代码更容易理解(不仅通过删除这些使用,还通过删除使用它们的上下文)。

  • 尝试使用React 来简化事情。我知道这不是一个亲身实践的建议,但是你越深入 React,你就越会意识到你可以与 React 合作做事,或者你可以按照自己的方式做事。两者都会起作用,但后者会给你和其他人带来更多的头痛。

  • 尽量隔离一个组件的范围;仅将必要的数据委托给子组件,并不断质疑您将状态保存在哪里。早些时候,您在所有三个组件中都有点击处理程序,而且流程非常复杂,我什至没有费心去完全理解它。在我的版本中,只有一个单击处理程序ColorPicker被委派。只要单击处理程序负责,按钮就不必知道单击它们时会发生什么。闭包和将函数作为参数传递的能力是 React 和 Javascript 的强大优势。

  • 键在 React 中很重要,很高兴看到您使用它们。通常,密钥应对应于唯一标识特定项目的内容。在这里使用会很好,${r}_${g}_${b}但是我们只能在按钮数组中拥有每种颜色的一个样本。这是一个自然的限制,但如果我们不想要它,分配键的唯一方法是分配一个唯一标识符,您就是这样做的。我更喜欢使用Date.now(),但有些人可能会出于某种原因反对它。如果您不想使用 ref,也可以在功能组件之外使用全局变量。

  • 尝试以功能(不可变)的方式做事,而不是“旧”的 Javascript 方式。例如,添加到数组时使用[...oldArray, newValue],分配给对象时使用{...oldObject, newKey: newValue }.

还有很多话要说,但我认为你最好学习重构版本,如果你有任何疑问,可以告诉我。


推荐阅读