useMemo踩坑 useMemo和useCallback的区别

2,634 阅读3分钟

背景

日常开发的一个可查询、操作的列表页。

页面结构

Form表单渲染筛选项+Table展示数据 具体结构如下图:

image.png

image.png

代码实现

筛选器筛选项变更触发onChange 将筛选参数保存在state里。

  // 筛选器参数
  const [reqParams, setReqParams] = useState<any>({ pageNo: 1, pageSize: 10 })

 // 筛选器变更 存储筛选器参数
  const onChange = (params) => {
    setReqParams({ ...reqParams, ...params })
  }

 // 表格数据
  const fetchList = async () => {
    // console.log(reqParams)
    const res: any = await axios.post(URL.queryList, { ...reqParams })
    const { list, pageSize, pageNo, total } = (res && res.data) || {}
    setDataSource(list || [])
    setPagination({ ...pagination, current: pageNo, total, pageSize })
  }

表格的操作列封装了一个组件(Operate) 根据【tabKey】参数展示不同的操作如「删除、编辑」等 操作数据后那么操作结束之后应该刷新数据保证数据的准确性以及实时性。

 // COLUMNS为定义的table columns常量
  const columns = useMemo(() => {
    const newColuumns: any[] = COLUMNS.concat()
    // if (getPermission(PERMISSION.ALL_ACTION)) {
    newColuumns.push({
      title: '操作',
      dataIndex: 'actions',
      key: 'actions',
      fixed: 'right',
      width: 150,
      align: 'center',
      render: (text, record) => (
        <Operate data={record} tabKey={tabKey} handleRefresh={fetchList} reqParams={reqParams} />
      ),
    })
    // }
    return newColuumns
  }, [tabKey])

部分操作组件的代码:根据业务逻辑和权限点赋予操作button,操作之后通过外部传入的handleRefresh函数刷新table数据

const Operate: FC<OperateProps> = ({ data = {}, tabKey, handleRefresh }) => {
  const { auditStatus, orgId } = data

  const handleDel = (event) => {
    Modal.confirm({
      title: `确定${event}?`,
      onOk: () => {
         // do something 
         message.success(`${event}操作成功`)
         handleRefresh()
      },
    })
  }

  const btns = useMemo(() => {
    const newBtns: Element[] = []
    if (tabKey === TABS_MAP.CREATE && getPermission(PERMISSION.DEL)) {
      newBtns.push({
        event: '删除',
        func: handleDel,
      })
    }
    return newBtns
  }, [auditStatus, tabKey])
  
  return (
    <>
      <Space>
        {btns.map((element: Element) => (
          <Button
            key={element.event}
            type='link'
            onClick={() => {
              element.func(element.event)
            }}
          >
            {element.event}
          </Button>
        ))}
      </Space>
    </>
  )
}

问题现象

当以上述代码思路构建之后,发现当handleRefresh的时候调用外部的fetchList,fetchList函数内的reqParams参数并不是当时当刻(实时的)的state内的reqParams,而是页面初次挂载后的初始态(也就是:{pageNo:1,pageSize:10})

image.png

问题原因

问题原因是:定义columns的时候使用了useMemo,而useMemo的第二个参数我只指定了会影响btns的taKeys,想当然的以为handleRefresh调用的时候触发fetchList,fetchList函数里面的reqParms会取当前(此时此刻)state里的reqParams。而事实并非如此。当useMemo第二个参数只指定tabKey时,在下一次渲染期间依赖项(tabKey)没有改变,useMemo没有调用useMemo的第一个参数(compute)计算而是返回初次渲染的记忆值。所以解释了 为什么handleRefresh函数调用的reqParams为初始值的原因!!!

问题解决

只需在定义columns时候讲reqParams加入依赖项即可。如下:

 // COLUMNS为定义的table columns常量
  const columns = useMemo(() => {
    const newColuumns: any[] = COLUMNS.concat()
    // if (getPermission(PERMISSION.ALL_ACTION)) {
    newColuumns.push({
      title: '操作',
      dataIndex: 'actions',
      key: 'actions',
      fixed: 'right',
      width: 150,
      align: 'center',
      render: (text, record) => (
        <Operate data={record} tabKey={tabKey} handleRefresh={fetchList} reqParams={reqParams} />
      ),
    })
    // }
    return newColuumns
  }, [tabKey, reqParams])

useMemo使用总结

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

useMemo()是一个内置的 React 钩子,它接受 2 个参数——一个computeExpensiveValue计算结果和depedencies依赖数组的函数: 初始渲染时,useMemo(computeExpensiveValue, dependencies)调用computeExpensiveValue,记忆计算结果,返回给组件。

如果在下一次渲染期间依赖项没有改变,useMemo() 则不调用 computeExpensiveValue但返回记忆值

但是如果在重新渲染过程中依赖关系发生了变化,那么就会useMemo() 调用 computeExpensiveValue,记住新值,然后返回它。

这就是useMemo()钩子的本质。

如果计算回调使用 props 或 state 值,须确保将这些值指示为依赖项。

useCallback的使用以及与useMemo的区别

const memoizedCallback = useCallback( () => { doSomething(a, b); }, [a, b], );

useCallback() 相比useMemo() 是一个更专业的钩子,返回一个 memoized 回调函数,用于记忆回调函数实例

把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

import { useCallback } from 'react';
function MyComponent({ prop }) {
  const callback = () => {
    return 'Result';
  };
  const memoizedCallback = useCallback(callback, [prop]);
  
  return <ChildComponent callback={memoizedCallback} />;
}

在上面的例子中,useCallback(() => {...}, [prop])只要prop依赖相同,就返回相同的函数实例。

总结

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);钩子。给定相同的[a, b]依赖关系,一旦被记忆,钩子将返回记忆值而不再调用computeExpensiveValue(a,b)。 useEffect、useMemo、useCallback本质相同,如果依赖项没有变更不会重新之行前置函数。