• 简体中文
  • useShallow react

    useShallow 是一个 React Hook,可用于优化重新渲染。

    const memoizedSelector = useShallow(selector)

    类型

    签名

    useShallow<T, U = T>(selectorFn: (state: T) => U): (state: T) => U

    参考

    useShallow(selectorFn)

    参数

    • selectorFn:一个函数,用于返回基于当前状态的数据。

    返回值

    useShallow 返回一个选择器函数的记忆化版本,它使用浅比较来实现记忆化。

    用法

    编写记忆化的选择器

    首先,我们需要创建一个 store 来保存小熊一家的状态。在这个 store 中,我们定义了三个属性:papaBear、mamaBear 和 babyBear,它们分别代表小熊一家的不同成员,以及各自那份燕麦粥锅的大小。

    import { create } from 'zustand'
    
    type BearFamilyMealsStore = {
      [key: string]: string
    }
    
    const useBearFamilyMealsStore = create<BearFamilyMealsStore>()(() => ({
      papaBear: 'large porridge-pot',
      mamaBear: 'middle-size porridge pot',
      babyBear: 'A little, small, wee pot',
    }))

    接下来,我们会创建一个 BearNames 组件,它获取状态中的键(也就是小熊一家的成员),并把它们显示出来。

    function BearNames() {
      const names = useBearFamilyMealsStore((state) => Object.keys(state))
    
      return <div>{names.join(', ')}</div>
    }

    然后,我们会创建一个 UpdateBabyBearMeal 组件,它会定期更新小熊宝宝的餐食选择。

    const meals = [
      'A tiny, little, wee bowl',
      'A small, petite, tiny pot',
      'A wee, itty-bitty, small bowl',
      'A little, petite, tiny dish',
      'A tiny, small, wee vessel',
      'A small, little, wee cauldron',
      'A little, tiny, small cup',
      'A wee, small, little jar',
      'A tiny, wee, small pan',
      'A small, wee, little crock',
    ]
    
    function UpdateBabyBearMeal() {
      useEffect(() => {
        const timer = setInterval(() => {
          useBearFamilyMealsStore.setState({
            babyBear: meals[Math.floor(Math.random() * (meals.length - 1))],
          })
        }, 1000)
    
        return () => {
          clearInterval(timer)
        }
      }, [])
    
      return null
    }

    最后,我们把这两个组件组合到 App 组件中,看看它们如何运作。

    export default function App() {
      return (
        <>
          <UpdateBabyBearMeal />
          <BearNames />
        </>
      )
    }

    代码应该像这样:

    import { useEffect } from 'react'
    import { create } from 'zustand'
    
    type BearFamilyMealsStore = {
      [key: string]: string
    }
    
    const useBearFamilyMealsStore = create<BearFamilyMealsStore>()(() => ({
      papaBear: 'large porridge-pot',
      mamaBear: 'middle-size porridge pot',
      babyBear: 'A little, small, wee pot',
    }))
    
    const meals = [
      'A tiny, little, wee bowl',
      'A small, petite, tiny pot',
      'A wee, itty-bitty, small bowl',
      'A little, petite, tiny dish',
      'A tiny, small, wee vessel',
      'A small, little, wee cauldron',
      'A little, tiny, small cup',
      'A wee, small, little jar',
      'A tiny, wee, small pan',
      'A small, wee, little crock',
    ]
    
    function UpdateBabyBearMeal() {
      useEffect(() => {
        const timer = setInterval(() => {
          useBearFamilyMealsStore.setState({
            babyBear: meals[Math.floor(Math.random() * (meals.length - 1))],
          })
        }, 1000)
    
        return () => {
          clearInterval(timer)
        }
      }, [])
    
      return null
    }
    
    function BearNames() {
      const names = useBearFamilyMealsStore((state) => Object.keys(state))
    
      return <div>{names.join(', ')}</div>
    }
    
    export default function App() {
      return (
        <>
          <UpdateBabyBearMeal />
          <BearNames />
        </>
      )
    }

    一切看起来可能都没问题,但有个小问题:即使名字没有变化,BearNames 组件也会不断重新渲染。这是因为只要状态的任何部分发生变化,组件就会重新渲染,即使我们关心的那部分(名字列表)并没有变化。

    为了解决这个问题,我们用 useShallow 来确保只有当状态中实际的键发生变化时,组件才会重新渲染:

    function BearNames() {
      const names = useBearFamilyMealsStore(useShallow((state) => Object.keys(state)))
    
      return <div>{names.join(', ')}</div>
    }

    代码应该像这样:

    import { useEffect } from 'react'
    import { create } from 'zustand'
    import { useShallow } from 'zustand/react/shallow'
    
    type BearFamilyMealsStore = {
      [key: string]: string
    }
    
    const useBearFamilyMealsStore = create<BearFamilyMealsStore>()(() => ({
      papaBear: 'large porridge-pot',
      mamaBear: 'middle-size porridge pot',
      babyBear: 'A little, small, wee pot',
    }))
    
    const meals = [
      'A tiny, little, wee bowl',
      'A small, petite, tiny pot',
      'A wee, itty-bitty, small bowl',
      'A little, petite, tiny dish',
      'A tiny, small, wee vessel',
      'A small, little, wee cauldron',
      'A little, tiny, small cup',
      'A wee, small, little jar',
      'A tiny, wee, small pan',
      'A small, wee, little crock',
    ]
    
    function UpdateBabyBearMeal() {
      useEffect(() => {
        const timer = setInterval(() => {
          useBearFamilyMealsStore.setState({
            babyBear: meals[Math.floor(Math.random() * (meals.length - 1))],
          })
        }, 1000)
    
        return () => {
          clearInterval(timer)
        }
      }, [])
    
      return null
    }
    
    function BearNames() {
      const names = useBearFamilyMealsStore(useShallow((state) => Object.keys(state)))
    
      return <div>{names.join(', ')}</div>
    }
    
    export default function App() {
      return (
        <>
          <UpdateBabyBearMeal />
          <BearNames />
        </>
      )
    }

    通过使用 useShallow,我们优化了渲染过程,确保组件只在必要时重新渲染,从而提升了整体性能。

    疑难解答

    从 store 中读取多个值时出现 “Maximum update depth exceeded”

    从 v5 开始,每次渲染都返回新引用的选择器可能触发无限更新循环,因为默认的相等性检查是 Object.is。最常见的情况是在选择器内部把多个值打包成一个对象。下面的写法会触发该错误:

    const { searchValue, setSearchValue } = useStore((state) => ({
      searchValue: state.searchValue,
      setSearchValue: state.setSearchValue,
    }))

    这个包装对象在每次渲染时都会被重新创建,因此每次比较都会失败,组件便会陷入循环重新订阅。

    用 useShallow 包裹选择器,这样就会按引用比较包装对象的各个属性,而不是比较包装对象本身:

    const { searchValue, setSearchValue } = useStore(
      useShallow((state) => ({
        searchValue: state.searchValue,
        setSearchValue: state.setSearchValue,
      })),
    )

    另一种做法是为每个值分别使用自己的选择器来订阅 —— 这样就不存在需要重新创建的包装对象:

    const searchValue = useStore((state) => state.searchValue)
    const setSearchValue = useStore((state) => state.setSearchValue)

    关于底层的这一行为变更,请参阅 v5 迁移指南中的 要求稳定的选择器输出。