从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 组件中有一个明确的顺序会更有效。
如果我们的代码或者组件一直增加,这种写法就会让人感觉代码越来越乱,越来越难以理解,我们需要有一个清晰的结构来整理我们的代码。
✅ 建议这样写
那么怎么才能有一个清晰的代码结构呢?
我的建议是按这样的结构顺序来写:
- useState()
- useRef()
- useMemo() 缓存值
- useCallback()缓存函数
- 自定义hook
- useEffect()
- 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?
这个需要具体问题具体分析,以下几点可以作为参考:
-
有几个相互依赖的值或回调函数,这些可以提取到钩子中。
-
组件中有一个
useEffect,通过将useEffect及其依赖项放入钩子中,可以为其指定一个更具声明性的名称,从而使代码更易于理解。 -
重复逻辑
所以,总结就是:
将相关代码放入自定义钩子中,并给这个钩子起一个通俗易懂的名字就好。