随笔|写了这么多年React ,我学到这几点

1,048 阅读4分钟

从17年开始写react,满打满算也这么长时间了,你要问我它的底层原理,细节上我还真不一定能给你讲清楚,面试的东西只需要面试前去背去记就行了

当然,如果能够深刻理解、灵活运用那就更好了。

但是,如果你问我有什么经验总结,那我就有的说了。

现代的前端开发其实都是组件化开发的,用react进行开发,关键就在于如何构建咱们的组件,这就有些规则要去遵循。

React 组件代码如何处理?

🛑 不要这样写

下面的代码展示了一个 React 组件,其中内置和自定义钩子的顺序不太明确。

export const MyReactComponent = memo(function MyReactComponent(props: Props) {
  const [state1, setState1] = useState(initialState1);
  const memoizedValue = useMemo(() => computeExpensiveValue(state1, props.prop1), [state1, props.prop1]);
  useEffect(() => {
    console.log(memoizedValue);
  }, [memoizedValue]);
  const ref1 = useRef(null);
  const handleClick = () => {
    // 点击事件
  };
  const { customValue, customFunction } = useMyCustomHook();
  const [state2, setState2] = useState(initialState2);
  const memoizedCallback = useCallback(() => doSomething(state1, props.prop2), [state1, props.prop2]);
  useEffect(() => {
    // 设置title
    document.title = `${state1} ${state2}`;
  }, [state1, state2]);
  return (
    <div>
      <button onClick={handleClick} ref={ref1}>
        My button ({state1}, {state2})
      </button>
      <SomeOtherComponent 
        value={memoizedValue} 
        callback={memoizedCallback}
        customValue={customValue}
      />
    </div>
  );
});

通常我们可能会认为相关的代码发在一起会比较好,容易理解,比如上面的代码中:

const memoizedValue = useMemo(() => computeExpensiveValue(state1, props.prop1), [state1, props.prop1]);
  useEffect(() => {
    console.log(memoizedValue);
  }, [memoizedValue]);

memoizedValue 和它下面的useEffect就挨在一起。

但是,实际上,React 组件中有一个明确的顺序会更有效

如果我们的代码或者组件一直增加,这种写法就会让人感觉代码越来越乱,越来越难以理解,我们需要有一个清晰的结构来整理我们的代码。

✅ 建议这样写

那么怎么才能有一个清晰的代码结构呢?

我的建议是按这样的结构顺序来写:

  1. useState()
  2. useRef()
  3. useMemo() 缓存值
  4. useCallback()缓存函数
  5. 自定义hook
  6. useEffect()
  7. JSX

示例如下:

export const MyReactComponent = memo(function MyReactComponent(props: Props) {
  // 1. State declarations
  const [state1, setState1] = useState(initialState1);

  // 2. Refs
  const ref1 = useRef(null);

  // 3. Memoized values
  const memoizedValue = useMemo(() => computeExpensiveValue(state1, prop1), [state1, prop1]);

  // 4. Memoized callbacks
  const memoizedCallback = useCallback(() => doSomething(state1, prop2), [state1, prop2]);

  // 5. Custom hooks
  const { customValue, customFunction } = useMyCustomHook();
  
  // 6. Effects
  useEffect(() => {
    // This effect uses the memoized value
    console.log(memoizedValue);
  }, [memoizedValue]);

  // 7. Event handlers and other functions
  const handleClick = useCallback(() => {
    // Handle click
  }), []);

  // 8. JSX
  return (
    <button onClick={handleClick}>
      My button
    </button>
  );
});

当然,这只是一个非常普通的规则,咱们可以根据具体的情况自由发挥。

自定义钩子如何处理?

🛑 不要这样写

遇到复杂的业务逻辑,我们有可能会封装一个非常复杂的组件,所有的逻辑都在一个文件里,可能会包含几百行、甚至上千行代码。

如果别人来接手,或者我们接手别人的这种代码,很难、甚至根本不知道这写代码要做什么功能。

比如:

import React, { useState, useMemo, useEffect } from 'react';

const WeatherDisplay = React.memo(function WeatherDisplay({ cityId }) {
  const [temperatureUnit, setTemperatureUnit] = useState('celsius');
  const weatherData = useMemo(() => fetchWeatherData(cityId), [cityId]);
  
  const convertedTemperature = useMemo(() => {
    if (temperatureUnit === 'fahrenheit') {
      return (weatherData.temperature * 9/5) + 32;
    }
    return weatherData.temperature;
  }, [weatherData.temperature, temperatureUnit]);

  useEffect(() => {
    localStorage.setItem('preferredTempUnit', temperatureUnit);
  }, [temperatureUnit]);

  const toggleTemperatureUnit = () => {
    setTemperatureUnit(prev => prev === 'celsius' ? 'fahrenheit' : 'celsius');
  };

  return (
    <div>
      <h2>Weather in {weatherData.cityName}</h2>
      <p>Temperature: {convertedTemperature}°{temperatureUnit === 'celsius' ? 'C' : 'F'}</p>
      <button onClick={toggleTemperatureUnit}>
        Switch to {temperatureUnit === 'celsius' ? 'Fahrenheit' : 'Celsius'}
      </button>
    </div>
  );
});

上面的代码,数据获取、单位转换的逻辑都在一个组件里,这只是个简单的示例,非常不方便阅读,如果是个复杂的业务,可能就更加难以理解了。

所以我们要想办法对它进行改造。

✅ 建议这样改造

React 官方文档曾经说过:

…每当你编写一个 Effect 时,请考虑是否将其包装在自定义 Hook 中会更清晰。

我们将单位转换的逻辑封装成自定义hook:

import React, { useState, useMemo, useEffect, useCallback } from 'react';

// 自定义hook useTemperatureConversion
function useTemperatureConversion(initialTemperature) {
  const [unit, setUnit] = useState(() => {
    return localStorage.getItem('preferredTempUnit') || 'celsius';
  });

  const convertedTemperature = useMemo(() => {
    if (unit === 'fahrenheit') {
      return (initialTemperature * 9/5) + 32;
    }
    return initialTemperature;
  }, [initialTemperature, unit]);

  useEffect(() => {
    localStorage.setItem('preferredTempUnit', unit);
  }, [unit]);

  const toggleUnit = useCallback(() => {
    setUnit(prev => prev === 'celsius' ? 'fahrenheit' : 'celsius');
  }, []);

  return { convertedTemperature, unit, toggleUnit };
}

// 展示组件
const WeatherDisplay = React.memo(function WeatherDisplay({ cityId }) {
  const weatherData = useMemo(() => fetchWeatherData(cityId), [cityId]);
  const { convertedTemperature, unit, toggleUnit } = useTemperatureConversion(weatherData.temperature);

  return (
    <div>
      <h2>Weather in {weatherData.cityName}</h2>
      <p>Temperature: {convertedTemperature}°{unit === 'celsius' ? 'C' : 'F'}</p>
      <button onClick={toggleUnit}>
        Switch to {unit === 'celsius' ? 'Fahrenheit' : 'Celsius'}
      </button>
    </div>
  );
});

这样一来,组件的逻辑就非常清晰了。

什么情况下需要自定义hook?

这个需要具体问题具体分析,以下几点可以作为参考:

  1. 有几个相互依赖的值或回调函数,这些可以提取到钩子中。

  2. 组件中有一个useEffect,通过将useEffect及其依赖项放入钩子中,可以为其指定一个更具声明性的名称,从而使代码更易于理解。

  3. 重复逻辑

所以,总结就是:

将相关代码放入自定义钩子中,并给这个钩子起一个通俗易懂的名字就好