一、组件的复用:避免重复代码的最佳实践
在开发中,我们经常会遇到多个组件拥有相同的逻辑(如请求数据、处理表单、监听事件),此时需要将这些逻辑抽离出来,实现复用。以下是 React 中常用的组件复用方式。
1. 自定义 Hooks(推荐)
自定义 Hooks 是 React 16.8 + 推出的复用逻辑的最佳方式,它是一个以use开头的函数,可以调用其他 Hooks,将组件的逻辑抽离为独立的函数。
示例 1:自定义 Hook 实现数据请求
javascript
import { useState, useEffect } from 'react';
// 自定义Hook:处理数据请求
const useFetch = (url) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// 取消请求的控制器
const controller = new AbortController();
const fetchData = async () => {
try {
const response = await fetch(url, { signal: controller.signal });
if (!response.ok) {
throw new Error('请求失败');
}
const result = await response.json();
setData(result);
setError(null);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
setData(null);
}
} finally {
setLoading(false);
}
};
fetchData();
// 组件卸载时取消请求
return () => {
controller.abort();
};
}, [url]);
return { data, loading, error };
};
// 使用自定义Hook的组件
const DataList = () => {
const { data, loading, error } = useFetch('https://api.example.com/data');
if (loading) return <p>加载中...</p>;
if (error) return <p>错误:{error}</p>;
return (
<ul>
{data.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
};
示例 2:自定义 Hook 实现本地存储
javascript
import { useState, useEffect } from 'react';
// 自定义Hook:处理localStorage
const useLocalStorage = (key, initialValue) => {
// 从本地存储中获取初始值
const [value, setValue] = useState(() => {
try {
const storedValue = localStorage.getItem(key);
return storedValue ? JSON.parse(storedValue) : initialValue;
} catch (err) {
console.error('读取本地存储失败:', err);
return initialValue;
}
});
// 当value变化时,更新本地存储
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (err) {
console.error('写入本地存储失败:', err);
}
}, [key, value]);
return [value, setValue];
};
// 使用自定义Hook的组件
const LocalStorageDemo = () => {
const [name, setName] = useLocalStorage('name', '游客');
return (
<div>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="请输入姓名"
/>
<p>你好,{name}!</p>
</div>
);
};
2. 高阶组件(HOC)
高阶组件是一个接收组件并返回新组件的函数,用于复用组件逻辑。它是 Hooks 出现之前的主要复用方式,现在逐渐被自定义 Hooks 替代。
示例:高阶组件实现加载状态
javascript
// 高阶组件:withLoading
const withLoading = (WrappedComponent) => {
return ({ loading, ...props }) => {
if (loading) {
return <p>加载中...</p>;
}
return <WrappedComponent {...props} />;
};
};
// 原始组件
const DataList = ({ data }) => {
return (
<ul>
{data.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
};
// 增强后的组件
const DataListWithLoading = withLoading(DataList);
// 使用增强后的组件
const App = () => {
const [loading, setLoading] = useState(true);
const [data, setData] = useState([]);
useEffect(() => {
setTimeout(() => {
setData([{ id: 1, name: 'React' }, { id: 2, name: 'Vue' }]);
setLoading(false);
}, 1000);
}, []);
return <DataListWithLoading loading={loading} data={data} />;
};
3. Render Props
Render Props 是指组件通过一个名为render的 Props 接收一个函数,该函数返回 JSX,从而实现逻辑复用。它也是 Hooks 出现之前的复用方式,现在使用较少。
示例:Render Props 实现鼠标位置跟踪
javascript
// Render Props组件:跟踪鼠标位置
const MouseTracker = ({ render }) => {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
};
}, []);
// 调用render函数,传递鼠标位置
return render(position);
};
// 使用Render Props组件
const App = () => {
return (
<MouseTracker
render={(position) => (
<p>
鼠标位置:X={position.x},Y={position.y}
</p>
)}
/>
);
};
二、组件的性能优化:让组件更高效
React 组件默认会在 Props 或 State 变化时重新渲染,但有时会出现不必要的重渲染,导致性能下降。以下是常用的组件性能优化方法。
1. 使用 React.memo 缓存函数组件
React.memo是一个高阶组件,用于缓存函数组件的渲染结果,只有当组件的 Props 发生变化时,才会重新渲染组件(浅比较 Props)。
javascript
import { memo } from 'react';
// 普通组件:每次父组件渲染,该组件都会重新渲染
const Child = ({ name }) => {
console.log('Child组件渲染了');
return <p>{name}</p>;
};
// 缓存组件:只有name变化时才会重新渲染
const MemoizedChild = memo(Child);
// 父组件
const Parent = () => {
const [count, setCount] = useState(0);
return (
<div>
<p>计数:{count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
{/* 使用缓存后的组件 */}
<MemoizedChild name="React" />
</div>
);
};
2. 使用 useMemo 缓存计算结果
useMemo用于缓存复杂计算的结果,避免每次渲染都重新计算。
javascript
import { useState, useMemo } from 'react';
// 复杂计算函数
const calculateTotal = (list) => {
console.log('执行复杂计算');
return list.reduce((total, item) => total + item, 0);
};
const App = () => {
const [list, setList] = useState([1, 2, 3, 4, 5]);
const [count, setCount] = useState(0);
// 缓存计算结果:只有list变化时才会重新计算
const total = useMemo(() => calculateTotal(list), [list]);
return (
<div>
<p>总和:{total}</p>
<p>计数:{count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => setList([...list, 6])}>添加数字</button>
</div>
);
};
3. 使用 useCallback 缓存函数
useCallback用于缓存函数的引用,避免每次渲染都创建新的函数,从而防止子组件因 Props 变化而不必要的重渲染。
javascript
import { useState, useCallback, memo } from 'react';
const Child = memo(({ onButtonClick }) => {
console.log('Child组件渲染了');
return <button onClick={onButtonClick}>点击我</button>;
});
const Parent = () => {
const [count, setCount] = useState(0);
// 缓存函数:每次渲染返回相同的函数引用
const handleClick = useCallback(() => {
console.log('按钮被点击了');
}, []); // 空依赖数组:仅创建一次
return (
<div>
<p>计数:{count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
<Child onButtonClick={handleClick} />
</div>
);
};
4. 列表渲染优化:使用唯一的 key
列表渲染时,为每个列表项添加唯一的key属性,帮助 React 识别列表项的变化,避免不必要的 DOM 操作。
javascript
const TodoList = ({ todos }) => {
return (
<ul>
{todos.map((todo) => (
// 使用唯一的id作为key,不要使用索引
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
};
5. 懒加载组件:React.lazy 与 Suspense
使用React.lazy和Suspense实现组件的按需加载,减少初始加载的代码体积,提升页面加载速度。
javascript
import { lazy, Suspense, useState } from 'react';
// 懒加载组件:只有当组件被渲染时才会加载
const LazyComponent = lazy(() => import('./LazyComponent'));
const App = () => {
const [showLazy, setShowLazy] = useState(false);
return (
<div>
<button onClick={() => setShowLazy(true)}>显示懒加载组件</button>
{showLazy && (
// Suspense:指定加载中的占位符
<Suspense fallback={<p>加载中...</p>}>
<LazyComponent />
</Suspense>
)}
</div>
);
};
三、组件的最佳实践与避坑指南
掌握以下最佳实践,可以让你的组件更易维护、更健壮。
1. 组件的单一职责
一个组件只负责一个功能,避免创建过于庞大的组件。如果组件功能过多,应将其拆分为多个子组件。
2. 组件命名规范
- 组件名使用大驼峰命名法 (PascalCase),如
TodoList、Button; - 组件文件名为组件名,如
TodoList.jsx、Button.tsx; - 避免使用通用名称,如
Component、Box,应使用语义化名称。
3. 合理拆分组件
按功能或 UI 结构拆分组件,例如:
- 页面级组件:
HomePage、UserPage; - 布局组件:
Header、Footer、Sidebar; - 业务组件:
TodoList、UserCard; - 通用组件:
Button、Input、Modal。
4. 避免在渲染时创建函数或对象
在渲染时创建函数或对象会导致每次渲染的引用不同,触发子组件的不必要重渲染,应将其提取到组件外部或使用useCallback/useMemo缓存。
ini
// 错误:每次渲染都会创建新的函数和对象
const Child = memo(({ onClick, style }) => { /* ... */ });
const Parent = () => {
return (
<Child
onClick={() => console.log('点击')}
style={{ color: 'red' }}
/>
);
};
// 正确:缓存函数和对象
const Parent = () => {
const handleClick = useCallback(() => console.log('点击'), []);
const style = useMemo(() => ({ color: 'red' }), []);
return <Child onClick={handleClick} style={style} />;
};
5. 不要直接修改 Props 或 State
Props 是只读的,State 是不可变的,直接修改会导致 React 无法检测到变化,引发渲染异常。应使用 setState 或不可变方法(如concat、map、扩展运算符)创建新的状态。
ini
// 错误:直接修改State
const [list, setList] = useState([1, 2, 3]);
const handleAdd = () => {
list.push(4); // 直接修改数组
setList(list);
};
// 正确:创建新的数组
const handleAdd = () => {
setList([...list, 4]); // 扩展运算符
// 或
// setList(prev => [...prev, 4]);
};
6. 正确处理异步操作
在组件中执行异步操作(如请求数据)时,应在组件卸载前取消操作,避免内存泄漏。
7. 使用 TypeScript 增强类型安全
使用 TypeScript 编写组件,可以为 Props、State 添加类型注解,提前发现类型错误,提升代码的可维护性。
四、总结
组件是 React 的核心,掌握组件的创建、核心特性、通信、复用和性能优化,是编写高质量 React 应用的关键。从函数组件 + Hooks 的基础使用,到 Context API 和状态管理库的跨组件通信,再到自定义 Hooks 的逻辑复用和性能优化,每一步都体现了 React 的设计思想:组件化、单向数据流、不可变性。
随着 React 的发展,函数组件 + Hooks 已经成为主流开发模式,自定义 Hooks 取代了高阶组件和 Render Props,成为逻辑复用的最佳方式。在实际开发中,应遵循组件的单一职责原则,合理拆分和复用组件,同时注重性能优化和代码规范,让你的 React 应用更高效、更易维护。