React 提供了众多出色的特性以及丰富的设计模式,用于简化开发流程。开发者能够借助 React 组件设计模式,降低开发时间以及编码的工作量。此外,这些模式让 React 开发者能够构建出成果更显著、性能更优越的各类应用程序。
本文将会为您介绍五个基础的 React 组件设计模式,并提供实例来协助您优化您的 React 应用。
文章目录
- 1. 高阶组件(HOC)模式
- 2. Provider 模式
- 3. 容器/表现模式
- 4. 复合模式
- 5. Hooks 模式
1. 高阶组件(HOC)模式
随着您的应用规模逐步扩大,可能会出现需要在多个组件之间共享相同组件逻辑的情况。而这正是 HOC 模式所能为您实现的。
HOC 是一个纯粹的 JavaScript 函数,它以一个组件作为参数,并在注入或者添加额外的数据和功能之后,返回另一个组件。从本质上讲,它充当了一个 JavaScript 装饰器函数。HOCs 的基本理念与 React 的特性相符,也就是倾向于组合而非继承。
例如,设想一个我们需要对多个应用组件进行统一风格化的场景。我们能够通过实现一个 HOC ,来避免在本地反复构建样式对象,该 HOC 会将样式应用于传入的组件。
import React from 'react';
// HOC
function decoratedComponent(WrappedComponent) {
return props => {
const style = { padding: '5px', margin: '2px' };
return <WrappedComponent style={style} {...props} />;
};
}
const Button = ({ style }) => <button style={{ ...style, color: 'yellow' }}>这是一个按钮。</button>;
const Text = ({ style }) => <p style={style}>这是文本。</p>;
const DecoratedButton = decoratedComponent(Button);
const DecoratedText = decoratedComponent(Text);
function App() {
return (
<>
<DecoratedButton />
<DecoratedText />
</>
);
}
export default App;
在上述代码示例中,我们已经对Button 和Text 组件进行了修改,分别生成了DecoratedButton 和DecoratedText。现在这两个组件都继承了由高阶组件(HOC)decoratedComponent 添加的样式。由于Button 组件已经有一个名为style 的prop,HOC 将覆盖它并附加新的prop。
优点
- 集中维护:有助于在单一位置维护可重用功能。
- 代码清晰:通过将所有逻辑整合到一个部分来保持代码的清晰,并实现关注点分离。
- 减少错误:通过避免代码重复,减少整个应用中意外错误的可能性。
缺点
- Props 名称冲突:有时会导致props 名称冲突,使得调试和扩展应用更具挑战性,特别是当组合许多共享相同prop 名称的 HOC 时。
2. Provider 模式
在复杂的 React 应用中,经常会出现如何使数据对多个组件可访问的挑战。虽然可以使用props 来传递数据,但在所有组件中访问props 值可能会变得繁琐,导致props 钻取(prop drilling)。
Provider 模式
利用 React Context API,以及在某些情况下使用 Redux,为这一挑战提供了解决方案。这种模式允许开发者将数据存储在中心区域,称为 React 上下文对象或 Redux 存储,消除了props 钻取的需要。
使用 React-Redux 实现 Provider 模式
React-Redux 在应用的顶层使用 Provider 模式,为所有组件提供对 Redux 存储的访问权限。以下代码示例展示了如何设置它。
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import App from './App';
const rootElement = document.getElementById('root');
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
rootElement
);
使用 React Context 的 Provider 模式
在 Redux 可能过于复杂的情况下,可以使用 React 的Context API。例如,如果一个 App 组件有一个数据集需要被深层组件树中的 List、PageHeader 和 Text 组件访问,Context API 可以绕过 props 钻取。
以下代码示例说明了如何创建和提供上下文。
import React, { createContext } from 'react';
import SideMenu from './SideMenu';
import Page from './Page';
const DataContext = createContext();
function App() {
const data = {
list: ['项目1', '项目2', '项目3'],
text: '你好世界',
header: 'WriterGate'
}; // 在这里定义你的数据结构。
return (
<DataContext.Provider value={data}>
<SideMenu/>
<Page/>
</DataContext.Provider>
);
}
使用 Context 数据
组件可以使用useContext 钩子访问数据,它允许读取和写入上下文对象中的数据。
以下代码示例供参考。
import React, { useContext } from 'react';
const SideMenu = () => <List/>
const Page = () => <div><PageHeader/><Content/></div>
function List() {
const data = useContext(DataContext);
return <span>{data.list}</span>;
}
function Text() {
const data = useContext(DataContext);
return <h1>{data.text}</h1>;
}
function PageHeader() {
const data = useContext(DataContext);
return <div>{data.header}</div>;
}
const Content = () => {
const data = useContext(DataContext);
return <div><Text/></div>;
}
优点
- 允许将数据发送到多个组件,无需通过组件层次结构。
- 减少了重构代码时出现不可预见错误的可能性。
- 消除了 props-drilling,这是一种反模式。
- 简化了保持某种形式的全局状态,因为组件可以访问它。
缺点
- 过度使用 Provider 模式可能会导致性能问题,特别是当向多个组件传递不断变化的变量时。然而,在较小的应用中,这不会是一个大问题。
3. 容器/表现模式
React 中的容器/表现模式提供了一种实现关注点分离的方法,有效地将视图与应用逻辑分开。理想情况下,我们需要通过将这个过程分成两部分来实现关注点分离。
表现组件
这些组件专注于如何向用户展示数据。它们通过props 接收数据,并负责以视觉上令人愉悦的方式呈现它,通常带有样式,而不修改数据。
考虑以下示例,它显示了从 API 获取的食物图片。为了实现这一点,我们实现了一个函数组件,该组件通过props 接收数据并相应地呈现它。
import React from "react";
export default function FoodImages({ foods }) {
return foods.map((food, i) => <img src={food} key={i} alt="食物" />);
}
在这个代码示例中,FoodImages 组件充当表现组件。表现组件保持无状态,除非它们需要 React 状态来渲染 UI。接收到的数据不会被修改。相反,它是从相应的容器组件中检索的。
容器组件
这些组件专注于决定向用户展示什么数据。它们的主要角色是将数据传递给表现组件。容器组件通常不渲染除了与它们的数据相关联的表现组件之外的其他组件。容器组件通常没有任何样式,因为它们的责任在于管理状态和生命周期方法,而不是渲染。
以下代码示例是一个容器组件,它从外部 API 获取图片并将它们传递给表现组件(FoodImages)。
import React from "react";
import FoodImages from "./FoodImages";
export default class FoodImagesContainer extends React.Component {
constructor() {
super();
this.state = {
foods: []
};
}
componentDidMount() {
fetch("http://localhost:4200/api/food/images/random/6")
.then(res => res.json())
.then(({ message }) => this.setState({ foods: message }));
}
render() {
return <FoodImages foods={this.state.foods} />;
}
}
优点
- 实现了关注点分离。
- 表现组件高度可重用。
- 由于表现组件不改变应用逻辑,它们的外表可以在不了解源代码的情况下进行修改。
- 测试表现组件是直接的,因为这些组件根据提供的数据进行渲染。
缺点
- 容器/表现模式下,无状态函数组件需要被重写为类组件。
4. 复合模式
复合组件是 React 组件模式中的高级模式,它允许构建功能以协作完成任务。它允许许多相互依赖的组件共享状态和处理逻辑,同时协同工作。
这种模式为父组件与其子组件之间的通信提供了一个富有表现力和多功能的 API。此外,它允许父组件隐式地与其子组件共享状态。复合组件模式可以使用 Context API 或 React.cloneElement API 来实现。
以下代码示例展示了如何使用 Context API 实现复合组件模式。
import React, { useState, useContext } from "react";
const SelectContext = React.createContext();
const Select = ({ children }) => {
const [activeOption, setActiveOption] = useState(null);
return (
<SelectContext.Provider value={{ activeOption, setActiveOption }}>
{children}
</SelectContext.Provider>
);
};
const Option = ({ value, children }) => {
const context = useContext(SelectContext);
if (!context) {
throw new Error("Option必须在Select组件内使用。");
}
const { activeOption, setActiveOption } = context;
return (
<div
style={activeOption === value ? { backgroundColor: "black" } : { backgroundColor: "white" }}
onClick={() => setActiveOption(value)}>
<p>{children}</p>
</div>
);
};
// 将"Option"作为"Select"的静态属性附加。
Select.Option = Option;
export default function App() {
return (
<Select>
<Select.Option value="john">John</Select.Option>
<Select.Option value="bella">Bella</Select.Option>
</Select>
);
}
在上面的示例中,select 组件是一个复合组件。它由多个共享状态和行为的组件组成。我们使用「Select.Option」 =「Option」将「Option」和「Select」组件链接起来。现在,导入「Select」组件会自动包含「Option」组件。
优点
- 复合组件维护其内部状态,这些状态在子组件之间共享。因此,在使用复合组件时无需显式管理状态。
- 无需手动导入子组件。
缺点
- 使用「React.Children.map」传递值时,组件堆叠受到限制。只有父组件的直接子组件才能访问 props。
- 如果现有的 prop 与提供给「React.cloneElement」方法的 props 同名,可能会出现命名冲突。
5. Hooks 模式
React Hooks API 在 React 16.8 中引入,从根本上改变了我们处理 React 组件设计的方式。Hooks 是为了解决 React 开发者遇到的常见问题而开发的。它们通过允许函数组件访问状态、生命周期方法、上下文和 refs 等特性,彻底改变了我们编写 React 组件的方式,这些特性以前是类组件独有的。
useState
useState 钩子使得函数组件能够添加状态。它返回一个包含两个元素的数组:当前状态值和允许你更新它的函数。
import React, { useState } from "react";
function ToggleButton() {
const [isToggled, setIsToggled] = useState(false);
const toggle = () => {
setIsToggled(!isToggled);
};
return (
<div>
<p>切换状态:{isToggled ? "ON" : "OFF"}</p>
<button onClick={toggle}>
{isToggled ? "关闭" : "开启"}
</button>
</div>
);
}
export default ToggleButton;
useEffect
useEffect 钩子便于在函数组件中执行副作用。它类似于类组件中的「componentDidMount」、「componentDidUpdate」和「componentWillUnmount」的组合。
import React, { useState, useEffect } from "react";
function Example() {
const [data, setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/posts");
const jsonData = await response.json();
setData(jsonData);
} catch (error) {
console.error("获取数据错误:", error);
}
};
fetchData();
}, []);
return (
<div>
{data ? (
<div>
<h2>数据获取成功!</h2>
<ul>
{data.map((item, index) => (
<li key={index}>{JSON.stringify(item)}</li>
))}
</ul>
</div>
) : (
<p>正在加载数据...</p>
)}
</div>
);
}
export default Example;
useRef
useRef 返回一个可变的 ref 对象,其 current 属性被初始化为传递的参数。这个对象在整个组件的生命周期内持续存在。
import React, { useRef } from "react";
function InputWithFocusButton() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current.focus();
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>聚焦输入</button>
</div>
);
}
export default InputWithFocusButton;
优点
- 组织代码,使其整洁清晰,不像生命周期方法那样。
- 克服了维护挑战,利用热重载和压缩问题。
- 允许在不编写类的情况下利用状态和其他 React 功能。
- 促进了跨组件重用有状态逻辑,减少代码重复。
- 减少了错误的可能性,并使用简单函数实现组合。
缺点
- 需要遵守特定规则,尽管没有 linter 插件很难识别规则违规。
- 需要实践才能有效使用某些钩子(例如,useEffect)。
- 需要小心避免不当使用(例如,useCallback,useMemo)。