记一个React.memo引起的bug

目录
  • 一般memo用法:
  • 问题描述
  • 解决方法
    • 方案一.使用useRef+forceUpdate方案
    • 方案2.使用useCallback
  • 总结

与PureComponent不同的是PureComponent只是进行浅对比props来决定是否跳过更新数据这个步骤,memo可以自己决定是否更新,但它是一个函数组件而非一个类,但请不要依赖它来“阻止”渲染,因为这会产生 bug。

一般memo用法:

import React from "react";

function MyComponent({props}){
    console.log('111);
    return (
        <div> {props} </div>
    )
};

function areEqual(prevProps, nextProps) {
    if(prevProps.seconds===nextProps.seconds){
        return true
    }else {
        return false
    }

}
export default React.memo(MyComponent,areEqual)

问题描述

我们在处理业务需求时,会用到memo来优化组件的渲染,例如某个组件依赖自身的状态即可完成更新,或仅在props中的某些数据变更时才需要重新渲染,那么我们就可以使用memo包裹住目标组件,这样在props没有变更时,组件不会重新渲染,以此来规避不必要的重复渲染。
下面是我创建的一个公共组件:

type Props = {
 inputDisable?: boolean
 // 是否一直展示输入框
 inputVisible?: boolean
 value: any
 min: number
 max: number
 onChange: (v: number) => void
}

const InputNumber: FC<Props> = memo(
 (props: Props) => {
   const { inputDisable, max, min, value, inputVisible } = props

   const handleUpdate = (e: any, num) => {
     e.stopPropagation()
     props.onChange(num)
   }
   return (
     <View className={styles.inputNumer}>
       {(value !== 0 || inputVisible) && (
         <>
           <Image
             className={styles.btn}
             src={require(value <= min
               ? '../../assets/images/reduce-no.png'
               : '../../assets/images/reduce.png')}
             onClick={e => handleUpdate(e, value - 1)}
             mode='aspectFill'
           />
           <Input
             value={value}
             disabled={inputDisable}
             alwaysEmbed
             type='number'
             cursor={-1}
             onInput={e => handleUpdate(e, parseInt(e.detail.value ? e.detail.value : '0'), 'input')}
           />
         </>
       )}
       <Image
         className={styles.btn}
         src={require(max !== -1 && (value >= max || min > max)
           ? '../../assets/images/plus-no.png'
           : '../../assets/images/plus.png')}
         onClick={e => handleUpdate(e, value + 1)}
       />
     </View>
   )
 },
 (prevProps, nextProps) => {
   return prevProps.value === nextProps.value && prevProps.min === nextProps.min && prevProps.max === nextProps.max
 }
)

export default InputNumber

这个组件是一个自定义的数字选择器,在memo的第二个参数中设置我们需要的参数,当这些参数有变更时,组件才会重新渲染。
在下面是我们用到这个组件的场景。

type Props = {
info: any
onUpdate: (items) => void
}

const CartBrand: FC<Props> = (props: Props) => {
const { info } = props
const [items, setItems] = useState<any>(
  info.items.map(item => {
  // selected默认为false
    return { num:1, selected: false }
  })
)

useEffect(() => {
  getCartStatus()
}, [])

// 获取info.items中没有提供,但是展示需要的数据
const getCartStatus = () => {
  setTimeout(() => {
    setItems(
      info.items.map(item => {
      //更新selected为true
        return {num: 1, selected: true }
      })
    )
  }, 1000)
}

return (
  <View className={styles.brandBox}>
    {items.map((item: GoodSku, index: number) => {
      return (
        <InputNumber
          key={item.skuId}
          inputDisable
          min={0}
          max={50}
          value={item.num}
          onChange={v => {
            console.log(v, item.selected)
          }}
        />
      )
    })}
  </View>
)
}

export default CartBrand

这个组件的目的是展示props传过来的列表,但是列表中有些数据服务端没有给到,需要你再次通过另一个接口去获取,我用settimeout替代了获取接口数据的过程。为了让用户在获取接口的过程中不需要等待,我们先根据props的数据给items设置了默认值。然后在接口数据拿到后再更新items。
但几秒钟后我们在子组件InputNumber中更新数据,会看到:

selected依然是false!
这是为什么呢?前面不是把items中所有的selected都改为true了吗?
我们再打印一下items看看:

似乎在InputNumber中的items依然是初始值。
对于这一现象,我个人理解为memo使用的memoization算法存储了上一次渲染的items数值,由于InputNumber没有重新渲染,所以在它的本地状态中,items一直是初始值。

解决方法

方案一. 使用useRef + forceUpdate方案

我们可以使用useRef来保证items一直是最新的,讲useState换为useRef

  type Props = {
  info: any
  onUpdate: (items) => void
}

const CartBrand: FC<Props> = (props: Props) => {
  const { info } = props
  const items = useRef<any>(
    info.items.map(item => {
    // selected默认为false
      return { num:1, selected: false }
    })
  )

  useEffect(() => {
    getCartStatus()
  }, [])
  
  // 获取info.items中没有提供,但是展示需要的数据
  const getCartStatus = () => {
    setTimeout(() => {
      items.current = info.items.map(() => {
        return { num: 1, selected: true }
      })
    }, 1000)
  }

  return (
    <View className={styles.brandBox}>
      {items.current.map((item: GoodSku, index: number) => {
        return (
          <InputNumber
            key={item.skuId}
            inputDisable
            min={0}
            max={50}
            value={item.num}
            onChange={v => {
              console.log(v, items)
            }}
          />
        )
      })}
    </View>
  )
}

export default CartBrand

这样再打印的时候我们会看到

items中的selected已经变成true了
但是此时如果我们需要根据items中的selected去渲染不同的文字,会发现并没有变化。

  return (
    <View className={styles.brandBox}>
      {items.current.map((item: GoodSku, index: number) => {
        return (
          <View key={item.skuId}>
            <View>{item.selected ? '选中' : '未选中'}</View>
            <InputNumber
              inputDisable
              // 最小购买数量
              min={0}
              max={50}
              value={item.num}
              onChange={() => {
                console.log('selected', items)
              }}
            />
          </View>
        )
      })}
    </View>
  )

显示还是未选中

这是因为useRef的值会更新,但不会更新他们的 UI,除非组件重新渲染。因此我们可以手动更新一个值去强制让组件在我们需要的时候重新渲染。

const CartBrand: FC<Props> = (props: Props) => {
  const { info } = props
  // 定义一个state,它在每次调用的时候都会让组件重新渲染
  const [, setForceUpdate] = useState(Date.now())
  const items = useRef<any>(
    info.items.map(item => {
      return { num: 1, selected: false }
    })
  )
  useEffect(() => {
    getCartStatus()
  }, [])

const getCartStatus = () => {
    setTimeout(() => {
      items.current = info.items.map(() => {
        return { num: 1, selected: true }
      })
      setForceUpdate()
    }, 5000)
  }

  return (
    <View className={styles.brandBox}>
      {items.current.map((item: GoodSku, index: number) => {
        return (
          <View key={item.skuId}>
            <View>{item.selected ? '选中' : '未选中'}</View>
            <InputNumber
              inputDisable
              // 最小购买数量
              min={0}
              max={50}
              value={item.num}
              onChange={() => {
                console.log('selected', items)
              }}
            />
          </View>
        )
      })}
    </View>
  )
}

export default CartBrand

这样我们就可以使用最新的items,并保证items相关的渲染不会出错

方案2. 使用useCallback

在InputNumber这个组件中,memo的第二个参数,我没有判断onClick回调是否相同,因为无论如何它都是不同的。
参考这个文章:use react memo wisely
函数对象只等于它自己。让我们通过比较一些函数来看看:

function sumFactory() {

return (a, b) => a + b;

}

const sum1 = sumFactory();

const sum2 = sumFactory();

console.log(sum1 === sum2); // => false

console.log(sum1 === sum1); // => true

console.log(sum2 === sum2); // => true

sumFactory()是一个工厂函数。它返回对 2 个数字求和的函数。
函数sum1和sum2由工厂创建。这两个函数对数字求和。但是,sum1和sum2是不同的函数对象(sum1 === sum2is false)。
每次父组件为其子组件定义回调时,它都会创建新的函数实例。在自定义比较函数中过滤掉onClick固然可以规避掉这种问题,但是这也会导致我们上述的问题,在前面提到的文章中,为我们提供了另一种解决思路,我们可以使用useCallback来缓存回调函数:

type Props = {
  info: any
  onUpdate: (items) => void
}

const CartBrand: FC<Props> = (props: Props) => {
  const { info } = props
  const [items, setItems] = useState(
    info.items.map(item => {
      return { num: 1, selected: false }
    })
  )
  useEffect(() => {
    getCartStatus()
  }, [])
  // 获取当前购物车中所有的商品的库存状态
  const getCartStatus = () => {
    setTimeout(() => {
      setItems(
        info.items.map(() => {
          return { num: 1, selected: true }
        })
      )
    }, 5000)
  }

  // 使用useCallback缓存回调函数
  const logChange = useCallback(
    v => {
      console.log('selected', items)
    },
    [items]
  )

  return (
    <View className={styles.brandBox}>
      {items.map((item: GoodSku, index: number) => {
        return (
          <View key={item.skuId}>
            <InputNumber
              inputDisable
              // 最小购买数量
              min={0}
              max={50}
              value={item.num}
              onChange={logChange}
            />
          </View>
        )
      })}
    </View>
  )
}

相应的,我们可以把InputNumber的自定义比较函数去掉。

type Props = {
 inputDisable?: boolean
 // 是否一直展示输入框
 inputVisible?: boolean
 value: any
 min: number
 max: number
 onChange: (v: number) => void
}

const InputNumber: FC<Props> = memo(
 (props: Props) => {
   const { inputDisable, max, min, value, inputVisible } = props

   const handleUpdate = (e: any, num) => {
     e.stopPropagation()
     props.onChange(num)
   }
   return (
     <View className={styles.inputNumer}>
       {(value !== 0 || inputVisible) && (
         <>
           <Image
             className={styles.btn}
             src={require(value <= min
               ? '../../assets/images/reduce-no.png'
               : '../../assets/images/reduce.png')}
             onClick={e => handleUpdate(e, value - 1)}
             mode='aspectFill'
           />
           <Input
             value={value}
             disabled={inputDisable}
             alwaysEmbed
             type='number'
             cursor={-1}
             onInput={e => handleUpdate(e, parseInt(e.detail.value ? e.detail.value : '0'), 'input')}
           />
         </>
       )}
       <Image
         className={styles.btn}
         src={require(max !== -1 && (value >= max || min > max)
           ? '../../assets/images/plus-no.png'
           : '../../assets/images/plus.png')}
         onClick={e => handleUpdate(e, value + 1)}
       />
     </View>
   )
 }
)

export default InputNumber

这样在items更新的时候,inputNumber也会刷新,不过在复杂的逻辑中,比如items的结构非常复杂,items中很多字段都会有高频率的改变,那这种方式会减弱InputNumber中memo的效果,因为它会随着items的改变而刷新。

总结

在最后,我还是选择了方案一解决这个问题。同时提醒自己,memo的使用要谨慎

到此这篇关于记一个React.memo引起的bug的文章就介绍到这了,更多相关React memo bug内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • React useMemo和useCallback的使用场景

    useMemo 我们知道当父组件发生重新渲染时,其所有(状态.局部变量等)都是新的.一旦子组件依赖于父组件的某一个对象变量,那么无论对象是否发生变化,子组件拿到的都是新的对象,从而使子组件对应的 diff 失效,依旧会重新执行该部分逻辑.在下面的例子中,我们的副作用依赖项中包含了父组件传入的对象参数,每次父组件发生更新时,都会触发数据请求. function Info({ style, }) { console.log('Info 发生渲染'); useEffect(() => { consol

  • 详解使用React.memo()来优化函数组件的性能

    React核心开发团队一直都努力地让React变得更快.在React中可以用来优化组件性能的方法大概有以下几种: 组件懒加载(React.lazy(...)和<Suspense />) Pure Component shouldComponentUpdate(...){...}生命周期函数 本文还会介绍React16.6加入的另外一个专门用来优化函数组件(Functional Component)性能的方法: React.memo. 无用的渲染 组件是构成React视图的一个基本单元.有些组件

  • 记一个React.memo引起的bug

    目录 一般memo用法: 问题描述 解决方法 方案一.使用useRef+forceUpdate方案 方案2.使用useCallback 总结 与PureComponent不同的是PureComponent只是进行浅对比props来决定是否跳过更新数据这个步骤,memo可以自己决定是否更新,但它是一个函数组件而非一个类,但请不要依赖它来“阻止”渲染,因为这会产生 bug. 一般memo用法: import React from "react"; function MyComponent(

  • Android开发中记一个SwipeMenuListView侧滑删除错乱的Bug

    做侧滑删除网上有很多方案,比如重写Listview实现滑动的监听,今天说下一个SwipeListView,这个是之前一个朋友在网上开源的一个封装组件,能够适用于多种情况,项目地址:https://github.com/baoyongzhang/SwipeMenuListView,我也采用了拿来主义直接拿来用了. 但是在调试运行的滑动删除数据的时候,却出现了一个问题,删除位置错乱,删除的第一个数据,却删除了最后一个,于是找问题呗,我首先用listview试了下,数据是没有问题的,那么说明是删除的时

  • React.memo函数中的参数示例详解

    目录 React.memo?这是个啥? React.memo的第一个参数 父组件 子组件 React.memo优化 React.memo的第二个参数 父组件 子组件 React.memo优化 父组件 子组件 小结 React.memo?这是个啥? 按照官方文档的解释: 如果你的函数组件在给定相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现.这意味着在这种情况下,React 将跳过渲染组件的操作并直

  • React memo减少重复渲染详解

    目录 1. 概述 2. 使用 1. 概述 此方法是一个 React 顶层 Api 方法,给函数组件来减少重复渲染,类似于 PureComponent 和 shouldComponentUpdate 方法的集合体. React.memo顶层Api方法,它可以用来减少子组件的重复渲染次数,从而提升组件渲染性能. React.memo它是一个只能在函数组件中使用的顶层Api方法. 当父组件发生改变时,默认情况下它的子孙组件也会重新渲染,当某些子组件不需要更新时,也会被强制更新,为了避免这种情况,我们可

  • React.memo 和 useMemo 的使用问题小结

    目录 问题背景 useMemo 进行优化 React.memo 进行优化 props的值是基本类型 props的值是引用类型 写在最后 问题背景 大家在使用 React 框架进行开发时一定遇到过以下问题: 当函数式组件中的某一状态改变,整个组件刷新,重新渲染 在类组件中 setState() 时,整个组件也会重新渲染 以上问题若不进行优化,导致的结果是: 随着代码的增加,每次的状态改变,页面进行一次 reRender ,这将产生很多不必要的 reRender 不仅浪费性能,从而导致页面卡顿: u

  • 从零开始搭建一个react项目开发

    本文介绍了从零开始搭建一个react项目开发,分享给大家,具体如下: 1.npm init 生成 package.json 文件. 2.安装各种需要的依赖: npm install  --save react - 安装React. npm install  --save react-dom 安装React Dom,这个包是用来处理virtual DOM.这里提一下用React Native的话,这里就是安装react-native. npm install  --save-dev webpack

  • 使用webpack5从0到1搭建一个react项目的实现步骤

    前言 在这之前,每开始一个新项目我都是使用现有的脚手架,这非常便于快速地启动一个新项目,而且通用的脚手架通常考虑地更加全面,也有利于项目的稳定开发:不过对于一个小项目,根据需求自己搭建可能会更好,一方面小项目不需要脚手架那么丰富的功能,另一方面可以提高对项目的掌控度以方便后期的扩展. 这篇文章是在实践中总结的,具有实操性,读者可跟着一步步进行搭建,中间我会穿插一些原理,当然因为笔者的能力有限,不会特别深入. 预备知识 熟悉Javascript && HTML && CSS

  • 教你如何从 html 实现一个 react

    什么是 React React是一个简单的javascript UI库,用于构建高效.快速的用户界面.它是一个轻量级库,因此很受欢迎.它遵循组件设计模式.声明式编程范式和函数式编程概念,以使前端应用程序更高效.它使用虚拟DOM来有效地操作DOM.它遵循从高阶组件到低阶组件的单向数据流. 前言

  • 使用 Rails API 构建一个 React 应用程序的详细步骤

    目录 后端:Rails API部分 前端:React部分 React组件 使用 axios 获取 API 数据 [51CTO.com快译]使用React创建项目时,动态数据无法保存的问题要怎么办呢?为此,我开始寻找一个充当备份的API来解决这一问题. 在本文中,我将介绍如何设置和构建一个以React作为前端的Rails API的一些要点,以帮助那些和我遇到一样问题的人. 本文计划使用Rails API作为后端,React作为前端,所以需要学习本文的人遵循同样的路径和步骤. 后端:Rails AP

随机推荐