Skip to content

React 最佳实践

如何在实际项目开发中写出更好的 React 代码?React 技术栈中的最佳实践有哪些?

进行 React 开发的前提是要对 React 的工作原理有一定的理解,我们不需要从源码的层面一点一点地去剖析,毕竟项目都是业务功能优先,我们之需要确保功能地实现即可,但是理解 React 的工作原理有助于我们写出更健壮更可靠的 React 代码。

理解 React

响应式的实现原理主要基于虚拟 DOMFiber 架构状态更新机制调度系统的协同工作。

  1. 状态变化。
  2. 触发重新渲染。
  3. 生成新的虚拟 DOM 并通过 Diff 算法计算差异。
  4. Fiber 架构和调度系统确保高效可中断的更新。
  5. 最终将最小变更应用到真实 DOM。

React 是自顶向下的数据流,父组件通过props向子组件传递数据,子组件不能直接修改 props,需要通过回调函数修改父组件的状态来触发变更。这与 Vue 不同,Vue 中通过v-model实现的双向绑定,可以直接修改 props 让触发变更。

这种数据流的设计方式优点是高可维护性数据流透明,但需要使用Context或者第三方状态管理库来解决深层次的状态传递问题。

合理的状态管理

1.不可变数据的正确使用。避免直接修改state,而是通过setState方法更新状态,对于引用类型需要确保引用发生变化。

jsx
const [count, setCount] = useState(0);
const [books, setBooks] = useState([]);
count++; 
setCount(count + 1); 

books.push(newBook); 
setBooks(books); 
setBooks([...books, newBook]); 

2.尽量只用 updater 函数更新状态,而不是直接修改状态。

jsx
const [count, setCount] = useState(0);

// 点击一次,最终结果count为1
// React内部合并了多次相同的setState的调用,只执行一次setCount
const handleClick1 = () => {
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
};
// 点击一次,最终结果count为3
// React内部将updater函数放入updateQueue,依次执行,执行结果作为下一个updater的参数传入
// updateQueue执行完毕后得到最终的结果
const handleClick2 = () => {
  setCount((prev) => prev + 1);
  setCount((prev) => prev + 1);
  setCount((prev) => prev + 1);
};

3.避免滥用useState,如果数据可以从已有状态派生,直接计算使用而不是用useState保存。

jsx
const formatDate = new Date().toLocaleDateString();
function App() {
  // some code...
}

4.使用useMemo缓存复杂的计算,如果依赖项频繁变化时则不考虑,应该考虑是否将计算放入Web Worker中处理。

js
const expensiveValue = useMemo(() => expensiveComputation(data), [data]);

5.合理使用useRef,它不仅能保存对 DOM 元素的引用,还可以作为组件生命周期内持久化的可变值存储容器。修改 ref 值不会导致组件重新渲染,这在需要跨渲染周期保留数据或避免副作用依赖时很有用。

jsx
function Child({ value }) {
  const valueRef = useRef(value);
  useEffect(() => {
    valueRef.current = value; //更新value前保存旧值
  });
  return (
    <div>
      当前值: {value}, 上一次值: {valueRef.current}
    </div>
  );
}
jsx
import * as echarts from 'echarts';
function EChartComponent() {
  const chartContainerRef = useRef(null);
  const instanceRef = useRef(null);

  useEffect(() => {
    if (chartContainerRef.current) {
      if (instanceRef.current) {
        // 调用实例方法
      } else {
        // 实例化实例并保存到ref
        instanceRef.current = echarts.init(chartContainerRef.current);
      }
    }
  }, []);

  return <div ref={chartContainerRef}></div>;
}
jsx
function Counter() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);

  useEffect(() => {
    countRef.current = count; // 同步最新值到 ref
  }, [count]);

  useEffect(() => {
    const timer = setInterval(() => {
      console.log("Latest count:", countRef.current); // 通过 ref 获取最新值
    }, 1000);
    return () => clearInterval(timer);
  }, []); // 无需依赖 count

  return <button onClick={() => setCount((c) => c + 1)}>Increment</button>;
}
jsx
function MyComponent() {
  const isFirstRender = useRef(true);

  useEffect(() => {
    if (isFirstRender.current) {
      isFirstRender.current = false;
      console.log("首次渲染");
    } else {
      console.log("非首次渲染");
    }
  });
}

组件设计

1.解构props以减少重复代码,提高可读性。

2.区分组件类型,确保符合功能职责单一性原则。例如负责管理数据逻辑的容器组件,负责渲染数据的展示组件,负责处理用户交互的交互组件。

3.使用 Fragments 减少冗余 DOM。

副作用和性能优化

1.不要过早地进行性能优化,这是没有意义的。

2.避免滥用useEffect,这可能会对导致组件的不必要重新渲染,导致性能问题,还会降低组件的可读性和可维护性。派生状态优先使用useMemo或直接计算。

js
// ✅ 推荐
const formattedDate = useMemo(() => formatData(rawData), [rawData]);

// ❌ 避免
const [formattedDate, setFormattedDate] = useState(0);
useEffect(() => {
  setFormattedDate(formatData(rawData));
}, [rawData]);

3.明确useEffect的依赖项,避免出现闭包问题和无限循环。

4.拆分副作用,将无关的逻辑放到不同的useEffect中。

js
useEeffect(() => {
  /**初始化逻辑 */
}, []);
useEffect(() => {
  /**数据订阅 */
}, [data]);

5.即时清理副作用,避免内存泄露。

js
useEffect(() => {
    const timer = setTimeout(() => {
        console.log('hello');
    }, 1000)
    })
    return () => clearTimeout(timer);
})

6.如果当前组件出现过多的useEffect,表明组件的逻辑耦合和职责过重,考虑按功能拆分自定义 Hook、依赖项相同的逻辑进行合并、抽取逻辑分离到子组件、复杂状态逻辑迁移到useReducer或状态管理库等方式。

7.尽量使用预定义函数,避免直接在子组件上写内联函数,这可能会导致子组件的不必要渲染。(原生 DOM 可以忽略)

jsx
// 子组件因 onClick 引用变化而重渲染
const Child = React.memo(({ onClick }) => (
  <button onClick={onClick}>Click</button>
));

function Parent() {
  return <Child onClick={() => console.log("Clicked")} />; // ❌ 每次渲染新建函数
}

8.不是所有的函数都需要useCallback缓存,只有函数被子组件使用(子组件使用React.memo优化)、函数作为依赖项、函数通过Context传递给深层组件时才需要缓存。默认不使用useCallback,仅在性能优化时使用

9.合理使用useLayoutEffect,在它的副作用中执行耗时高开销的操作会阻塞渲染导致页面卡顿,仅在布局计算和同步 DOM 操作时使用。

代码组织和可维护性

1.按功能组织文件,一个组件一个文件夹,内含index文件、测试文件、类型定义文件、样式文件等。

2.组件采用 PascalCase命名方式,文件名采用 kebab-case 命名方式。

3.请默认使用 TypeScript,即使你不是很喜欢它。

4.使用枚举或策略模式代替复杂条件。

jsx
const statusMap = {
  loading: <Spinner />,
  error: <ErrorPage />,
  success: <Content />,
};
return statusMap[status];

5.设置好错误边界以捕获错误,避免出现白屏。

6.使用eslintprettier来统一代码风格和格式,并设置好eslint规则以提高代码质量。

7.不推荐使用css-in-js的方式来管理样式,请使用 CSS 预处理器、CSS Module 或者原子化 CSS。

8.合理使用第三方依赖,采用成熟的库来解决问题,同时避免对于的简单问题过度依赖第三方库而增加项目代码体积。

9.不要吝啬使用注释,每一个函数与变量都应该注明它的含义和用途。

10.命名要规范,不要使用拼音,尽量使用有意义的英文单词,让人容易理解。

Last updated: