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 docsuseCallback 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])

每次 colorButtonscurrentColor 更改时,此函数将获得一个新标识。 ColorPicker 本身会在设置这两者之一或当其属性 onChange 发生变化时重新呈现。当 currentColorcolorButtons 更改时,handleColorButtons 和 children 都应更新。 child人受益于使用useCallback的时间是 onChange变化的时候。鉴于 ColorButton 是一个轻量级组件,并且 ColorPicker 重新渲染主要是由于对 currentColorcolorButtons 的更改,因此在这里使用 useCallback 似乎是多余的。

案例二:ColorButton

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

这种情况与第一种情况类似。 ColorButtoncurrentColoronClickidcolorButtons 更改时重新呈现,而 children 在 handleClick、[=44] 时重新呈现=]、colorButtonscurrentColor 更改。 useCallback 到位后,道具 idonClick 可能会更改而无需重新渲染 children(至少根据上面的可见代码),[= 的所有其他重新渲染32=] 将导致其 children 重新渲染。同样,child ToggleButton 是轻量级的,idonClick 不太可能比任何其他 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 更改时计算 ab,但更直接的方法是改为

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

所以这里使用 useCallback 只会让事情变得更糟。

结论

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


沙盒方面做​​得很好!总是使对代码的推理变得更容易。我冒昧地分叉了您的沙箱并对其进行了一些重构:sandbox link。如果需要,您可以自己研究这些变化。这是一个摘要:

  • 你知道并使用 useRefuseCallback 很好,但我能够删除所有用途,使代码更容易理解(不仅通过删除这些用途,但也删除了使用它们的上下文)。

  • 尝试使用 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 }.

还有很多话要说,但我觉得你还是研究一下重构后的版本比较好,有什么想知道的可以告诉我。