【内容梳理】<React性能优化>降低re-render之state

1. 说明

以下针对的是useState、useReducer等

  • 直接修改 state 的值无法刷新页面
  • useState 的参数只在初始化时有效,若需根据外部的变更调整 state,请结合 useEffect 来更新 state
  • 调用 set 函数 后,当前事件中 state 还是旧值。(即使在同步事件中)
  • setState 没有渲染后的回调函数(和 class 写法不一致),只有传递函数的模式
ts 复制代码
setState((prevState) => (prevState += 1));

2. state 类型

  • 尽量使用 基本类型 属性,比较时方便
  • 若必须使用复杂对象,或多个基本属性有联动,则建议使用reducer来统一管理
ts 复制代码
export const RnRecordReduder = (state: IRnRecordState, action: IRnRecordAction): IRnRecordState => {
  switch (action.type) {
    case 'SET_FORM_VALUE': {
      ...
    }
    case 'SET_RELATION_VALUE': {
      ...
    }
    default: {
      return state;
    }
  }
};
...
const [store, dispatch] = useReducer(RnRecordReduder, {
  initialData,
  formValue: cloneDeep(initialData),
  onChangeFormValue,
});
...
dispatch({
  type: 'SET_RELATION_VALUE',
  value: (preState: Map<string, Record<string, any>>) =>
    update(preState || new Map<string, Record<string, any>>(), { $add: [[element.id, selRelation]] }),
});
  • 修改复杂大对象时,建议使用immerimmutability-helper等库进行对象操作,可以保证除了修改点和父级点之外的点没有变动;进而减少子组件的 re-render

3. 使用中的 tips

  • 调用 set 函数时,传递函数
    • 函数参数可以拿到上一次的 state 值;
    • 可以防止高频次操作,state 状态不对的情况(比如勾选和取消勾选的列表显示)
tsx 复制代码
const [curSelValue, setSelValue] = useState<IUserSelectAttr[]>([]); // 当前选中的用户
...

const handleChange = (event: CheckboxChangeEvent, selItem: IUserSelectAttr) => {
  ...
  setSelValue((prevSelValue) => {
    return prevSelValue.concat(selItem);
  });
  ...
};
  • React 接管事件中(合成事件等),setState 是异步更新的且会合并,即多次 setState,只会有一次 re-render,且 re-render 时拿到的 state 是上次的 state;
  • 非 React 接管事件中(包含生命周期),比如 PromisesetTimeoutaddEventListener 等,setState 是同步更新且不会合并,多次 setState 会产生多次 re-render( R e a c t 18 \color{red}React18 React18 后都会合并更新,并只 re-render 一次; R e a c t 18 \color{red}React18 React18 之前,可以尝试使用unstable_batchupdate
ts 复制代码
import { useUpdateEffect, useWhyDidYouUpdate } from 'ahooks';
import { Button } from 'antd';
import { useEffect, useState } from 'react';
import uniqid from 'uniqid';

const allFavorite = ['篮球', '看书', '打游戏', '跑步', '逛街', '锻炼', '做手工', '麻将', '扑克牌', '追剧'];

const SyncAndAsyncUpdate = () => {
  const [uniqKey1, setUniqKey1] = useState<string>();
  const [uniqKey2, setUniqKey2] = useState<string>();

  const [count, setCount] = useState(0); // number类型测试
  const [name, setName] = useState(''); // string类型测试
  const [isLoading, setLoading] = useState(false); // boolean类型测试
  const [favorite, setFavorite] = useState<Array<string>>([]); // 数组测试

  useWhyDidYouUpdate('SyncAndAsync', { count, name, isLoading, favorite });

  const randomSet = () => {
    const radomNum = Math.floor(Math.random() * 10);
    console.log('当前的radomNum:', radomNum);
    setCount(radomNum);
    console.log('当前的count:', count);
    setName(`Lawson-${radomNum}`);
    setLoading((prev) => !prev);
    setFavorite((prev) => prev.concat(allFavorite[radomNum]));
  };

  useEffect(() => {
    const targetDom = document.getElementById('addEventListener-test');
    const clickListener = () => {
      console.log('---addEventListener测试---');
      randomSet();
    };
    targetDom?.addEventListener('click', clickListener);
    return () => {
      targetDom?.removeEventListener('click', clickListener);
    };
  }, []);

  useUpdateEffect(() => {
    console.log('---同步effect测试---');
    randomSet();
  }, [uniqKey2]);

  useUpdateEffect(() => {
    console.log('---异步effect测试---');
    (async () => {
      await Promise.resolve();
      randomSet();
    })();
  }, [uniqKey1]);

  const handleProxy = () => {
    console.log('---react托管事件测试---');
    randomSet();
  };

  const handleSync = () => {
    setUniqKey2(uniqid());
  };

  const handleAsync = () => {
    setUniqKey1(uniqid());
  };

  const handleSetTimeout = () => {
    console.log('---setTimeout测试---');
    setTimeout(() => {
      randomSet();
    }, 200);
  };

  return (
    <div style={{ display: 'flex', flexDirection: 'column', width: '400px' }}>
      <Button onClick={handleProxy}>react托管事件测试</Button>
      <Button id="addEventListener-test">addEventListener测试</Button>
      <Button onClick={handleSync}>同步effect测试</Button>
      <Button onClick={handleAsync}>异步effect测试</Button>
      <Button onClick={handleSetTimeout}>setTimeout测试</Button>
    </div>
  );
};

export default SyncAndAsyncUpdate;
  • 高频次修改 state 时,看情况使用【防抖】或【节流】

4. 其他

  • 计算 state,使useMemo来计算出数据,以保证计算数据的不变性的方式,降低 re-render(注意依赖)
    • useMemo
    • useCreation
  • 不希望某个 state,或被解构的属性作为依赖项,但是希望在 useMemo 或 useEffect 中使用时,可以考虑使用useLatest; 以降低可能发生变动的源头的方式,来降低 re-render 的可能性
tsx 复制代码
// 当前成员的联动值
const chainUser: IUserSelectAttr = useCreation(() => {
  ...
}, [formValue, defaultValue, defaultMode]);

const chainUserLst = useLatest(chainUser);

// 联动数据发生修改,则同步更新当前成员值
useEffect(() => {
  ...
  onChange?.(chainUserLst.current);
  ...
}, [chainUser?.userCode, defaultMode]);
  • 为了解决异步数据 undefined 的问题,一般需要设置 isLoading 状态,来区分两种渲染 div; 此时需要注意 setLoading 要放在最后,因为很有可能当前处于同步更新中,不会 batchUpdate;参考上面【合成事件】那里的代码
相关推荐
Patrick_Wilson2 小时前
从 React 到 Flutter:写给前端的一张跨端知识地图
前端·flutter·react.js
张元清11 小时前
React useDropZone Hook:搭建一个文件拖放区(2026)
javascript·react.js
无人生还13 小时前
从 Vue3 到 React · 快速上手系列第 5 篇:事件处理与表单(受控组件 vs v-model)
前端·vue.js·react.js
光影少年13 小时前
RN Bridge 原理
前端·react native·react.js
moMo13 小时前
用 React 写一个 TodoList
react native·react.js
To_OC1 天前
写了三遍 Todo List,我终于搞懂了 React 父子组件到底怎么通信
前端·javascript·react.js
kyriewen1 天前
别再乱用useEffect了——你写的10个里有8个不该存在
前端·javascript·react.js
zandy10111 天前
衡石 Agentic BI的ReAct 推理框架在 Agentic BI 中的工程化实践
前端·javascript·react.js
朝阳391 天前
react19【系列实用教程】JS 项目升级为 typescript
javascript·react.js·typescript
labixiong1 天前
React Compiler 用 Rust 重写了,编译提速 10 倍——手写 useMemo 的日子到头了
react.js·rust