React 设计模式
这是在 React 中使用的最重要设计模式的集合。由 Cosden Solutions 制作。
1. 单一职责原则
您的组件应该只有一个责任。它们应该只做“一件事”,并将其他所有事情委托给其他组件。以下是一个责任过多的组件示例:
// ❌ 责任过多!
function BigComponent() {
// 负责多个无关的状态
const [data, setData] = useState();
const [isModalOpen, setIsModalOpen] = useState(false);
// 负责获取数据
useEffect(() => {
fetch('/api/data')
.then(response => response.json())
.then(data => setData(data));
}, []);
// 负责发送分析事件
useEffect(() => {
sendAnalyticsEvent('page_view', { page: 'big_component' });
}, []);
// 负责切换模态框
function toggleModal() {
setIsModalOpen(prev => !prev);
}
// ... 其他代码
}
相反,创建多个具有单一职责的组件/钩子。
首先,创建 useFetchData.ts。这个钩子将持有 data 状态并管理获取和更新它。
// ✅ 单一职责:管理数据
export function useFetchData() {
const [data, setData] = useState();
useEffect(() => {
fetch('/api/data')
.then(response => response.json())
.then(data => setData(data));
}, []);
return data;
}
或者更好的是,使用 react-query。
// ✅ 单一职责:通过 react-query 管理数据
export function useFetchData() {
return useQuery({
queryKey: ['data'],
queryFn: () => fetch('/api/data'),
});
}
然后创建 usePageAnalytics.ts。这个钩子将通过 props 接收 event 并发送它。
type Event = {
page: string;
};
// ✅ 单一职责:管理分析
export function usePageAnalytics(event: Event) {
useEffect(() => {
sendAnalyticsEvent('page_view', event);
}, []);
}
最后创建 Modal.tsx。这个组件将接收 children 作为 props,并管理自己的 isModalOpen 状态。
type ModalProps = {
children: React.ReactNode;
};
// ✅ 单一职责:管理模态框
export function Modal({ children }: ModalProps) {
const [isModalOpen, setIsModalOpen] = useState(false);
function toggleModal() {
setIsModalOpen(prev => !prev);
}
return (
<>
<button onClick={toggleModal}>打开</button>
{isModalOpen && children}
</>
);
}
这样,BigComponent 只需要导入并组合所有内容。现在它小巧、易于管理且高度可扩展。
import { useFetchData } from './useFetchData';
import { useAnalytics } from './useAnalytics';
import { Modal } from './Modal';
// ✅ 单一职责:将所有内容组合在一起
function BigComponent() {
const data = useFetchData();
useAnalytics();
return <Modal>{/* ... 其他代码 */}</Modal>;
}
2. 容器和展示组件
为了保持代码的组织性,您可以将组件分为容器组件和展示组件。容器组件持有所有逻辑,而展示组件渲染 UI。
// 容器组件负责逻辑
function ContainerComponent() {
const [items, setItems] = useState([]);
const [filters, setFilters] = useState({});
useEffect(() => {
const filteredItems = filterItems(items, filters);
}, [filters]);
function handleFilters(newFilters) {
setFilters(newFilters);
}
// ... 其他业务逻辑代码
return <PresentationComponent items={items} />;
}
// 展示组件负责 UI
function PresentationComponent({ items }) {
return (
<>
{/* ... 其他 UI 代码 */}
{items.map(item => (
<ItemCard key={item.id} item={item} />
))}
{/* ... 其他 UI 代码 */}
</>
);
}
3. 复合组件模式
将一起使用的组件组合成一个复合组件,使用 React 上下文 API。
import { createContext, useState } from 'react';
const ToggleContext = createContext();
// 主组件导出以供项目使用
export default function Toggle({ children }) {
const [on, setOn] = useState(false);
function toggle() {
setOn(!on);
}
return (
<ToggleContext.Provider value={{ on, toggle }}>
{children}
</ToggleContext.Provider>
);
}
// 附加到主组件的复合组件
Toggle.On = function ToggleOn({ children }) {
const { on } = useContext(ToggleContext);
return on ? children : null;
};
// 附加到主组件的复合组件
Toggle.Off = function ToggleOff({ children }) {
const { on } = useContext(ToggleContext);
return on ? null : children;
};
// 附加到主组件的复合组件
Toggle.Button = function ToggleButton(props) {
const { on, toggle } = useContext(ToggleContext);
return <button onClick={toggle} {...props} />;
};
现在这个组件可以在任何地方灵活使用。子组件可以任意顺序放置,或者只使用它们的一个子集:
import Toggle from '@/components/Toggle';
// 示例用法,包含所有组件
function App() {
return (
<Toggle>
<Toggle.On>按钮开启</Toggle.On>
<Toggle.Off>按钮关闭</Toggle.Off>
<Toggle.Button>切换</Toggle.Button>
</Toggle>
);
}
// 示例用法,顺序不同
function App() {
return (
<Toggle>
<Toggle.Button>切换</Toggle.Button>
<Toggle.Off>按钮关闭</Toggle.Off>
<Toggle.On>按钮开启</Toggle.On>
</Toggle>
);
}
// 示例用法,仅使用部分组件
function App() {
return (
<Toggle>
<Toggle.Button>切换</Toggle.Button>
</Toggle>
);
}
4. 嵌套属性转发
当一个灵活组件使用另一个组件时,允许将属性转发到嵌套组件。
// 接收属性作为 `...rest`
function Text({ children, ...rest }) {
return (
<span className="text-primary" {...rest}>
{children}
</span>
);
}
// 按钮组件使用 `Text` 组件作为其文本
function Button({ children, textProps, ...rest }) {
return (
<button {...rest}>
{/* ✅ `textProps` 被转发 */}
<Text {...textProps}>{children}</Text>
</button>
);
}
示例用法:
function App() {
return (
<Button textProps={{ className: 'text-red-500' }}>
红色文本按钮
</Button>
);
}
5. 子组件模式
为了提高性能并防止不必要的重新渲染,提升组件并将其作为子元素传递。
function Component() {
const [count, setCount] = useState(0);
return (
<div>
{count}
{/* ❌ 昂贵的组件会在每次 count 更改时不必要地重新渲染 */}
<ExpensiveComponent />
</div>
);
}
将 ExpensiveComponent 移动到上面并作为子元素传递将防止其重新渲染。
// 组件
function Component({ children }) {
const [count, setCount] = useState(0);
// ✅ 子组件在状态变化时不会重新渲染
return <Component>{children}</Component>;
}
// 应用
function App() {
return (
<Component>
{/* ✅ 昂贵的组件在组件更新时不会重新渲染 */}
<ExpensiveComponent />
</Component>
);
}
6. 自定义钩子
为了保持代码整洁和可重用,将相关功能提取到可以共享的自定义钩子中。
// ❌ 与 `items` 相关的所有代码直接在组件中。
function Component() {
const [items, setItems] = useState([]);
const [filters, setFilters] = useState({});
useEffect(() => {
const filteredItems = filterItems(items, filters);
}, [filters]);
function handleFilters(newFilters) {
setFilters(newFilters);
}
// ... 其他代码
}
您可以创建 useFilteredItems.ts 并将所有功能放在那里。
// ✅ 与 `items` 相关的所有代码在自定义可重用钩子中
export function useFilteredItems() {
const [items, setItems] = useState([]);
const [filters, setFilters] = useState({});
useEffect(() => {
const filteredItems = filterItems(items, filters);
}, [filters]);
function handleFilters(newFilters) {
setFilters(newFilters);
}
return {
items,
filters,
handleFilters,
};
}
然后在 Component 中可以使用该钩子。
// ✅ 组件更简洁,并可以共享过滤项的功能
function Component() {
const { items, filters, handleFilters } = useFilteredItems();
// ... 其他代码
}
7. 高阶组件 (HOC)
有时,创建一个高阶组件 (HOC) 来共享可重用功能是更好的选择。
function Button(props) {
// ❌ 样式对象重复
const style = { padding: 8, margin: 12 };
return <button style={style} {...props} />;
}
function TextInput(props) {
// ❌ 样式对象重复
const style = { padding: 8, margin: 12 };
return <input type="text" style={style} {...props} />;
}
通过 HOC,您可以创建一个包装组件,该组件接受一个组件及其 props,并增强它。
// ✅ 高阶组件实现样式
function withStyles(Component) {
return props => {
const style = { padding: 8, margin: 12 };
// 将组件 props 与自定义样式对象合并
return <Component style={style} {...props} />;
};
}
// 内部组件通过 props 接收样式
function Button({ style, ...props }) {
return <button style={style} {...props} />;
}
function TextInput({ style, ...props }) {
return <input type="text" style={style} {...props} />;
}
// ✅ 使用 HOC 包装导出
export default withStyles(Button);
export default withStyles(TextInput);
8. 变体属性
如果您有在整个应用程序中共享的组件,请创建变体属性,以便使用预设值轻松自定义它们。
type ButtonProps = ComponentProps<'button'> & {
variant?: 'primary' | 'secondary';
size?: 'sm' | 'md' | 'lg';
};
function Button({ variant = 'primary', size = 'md', ...rest }: ButtonProps) {
// ✅ 基于变体和大小派生样式
const style = {
...styles.variant[variant],
...styles.size[size],
};
return <button style={style} {...rest} />;
}
// ✅ 明确定义每个变体/大小的样式的自定义对象
const styles = {
variant: {
primary: {
backgroundColor: 'blue',
},
secondary: {
backgroundColor: 'gray',
},
},
size: {
sm: {
minHeight: 10,
},
md: {
minHeight: 12,
},
lg: {
minHeight: 16,
},
},
};
示例用法:
function App() {
return (
<div>
<Button>主要按钮</Button>
<Button variant="secondary" size="sm">
次要按钮
</Button>
</div>
);
}
9. 通过 ref 暴露功能
有时,通过 ref 从一个子组件导出功能到父组件是有用的。这可以通过 useImperativeHandle 钩子来实现。
type Props = {
componentRef: React.RefObject<{ reset: () => void }>;
};
function Component({ componentRef }: Props) {
const [count, setCount] = useState(0);
// ✅ 通过 ref 向父组件暴露自定义重置函数以更改状态
useImperativeHandle(componentRef, () => ({
reset: () => {
setCount(0);
},
}));
return (
<div>
{count}
<button onClick={() => setCount(count + 1)}>增加</button>
</div>
);
}
要使用它,只需在渲染它的同一组件中创建一个 ref。
function App() {
const componentRef = useRef(null);
return (
<>
<Component componentRef={componentRef} />
{/* ✅ 使用 ref 我们可以重置 Component 的内部状态 */}
<button onClick={() => componentRef.current?.reset()}>重置</button>
</>
);
}
10. 使用提供者共享常用数据
如果您有在多个组件之间共享的数据,考虑使用上下文 API 将其放入提供者中。
function Component1() {
// ❌ 用户在多个组件中被获取
const { data: user } = useFetchUser();
// ❌ 不必要的重复检查未定义的用户
if (!user) {
return <div>加载中...</div>;
}
// ... 返回 JSX
}
function Component2() {
// ❌ 用户在多个组件中被获取
const { data: user } = useFetchUser();
// ❌ 不必要的重复检查未定义的用户
if (!user) {
return <div>加载中...</div>;
}
// ... 返回 JSX
}
使用提供者,我们可以将所有这些功能放在一个组件内。
const UserContext = createContext(undefined);
function UserProvider({ children }) {
// ✅ 用户获取在提供者中完成
const { data: user } = useFetchUser();
// ✅ 用户检查在提供者中完成
if (!user) {
return <div>加载中...</div>;
}
return (
<UserContext.Provider value={{ user }}>{children}</UserContext.Provider>
);
}
// 自定义钩子以便轻松访问上下文
export function useUser() {
const context = useContext(UserContext);
if (!context) {
throw new Error('useUser 必须在 UserProvider 内部使用。');
}
return context;
}
在用它包装整个应用程序后,您可以在任何需要的地方使用共享功能...