关于 hooks-pattern
介绍 React Hooks 的开发技能,讲解如何在函数组件中管理状态和生命周期,并抽取可复用的状态逻辑。适合希望组织 React 组件逻辑、减少对类组件依赖的开发者。
主要功能
- 01用 useState 管理组件状态
- 02用 useEffect 处理副作用
- 03创建并复用自定义 Hook
- 04遵循 Hook 调用规则
- 05避免不必要的 useEffect
适用场景
- 01为函数组件添加状态行为
- 02在多个组件间复用状态逻辑
- 03将类组件改写为函数组件
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ patternsdev/ skills --skill hooks-pattern安装后在 Claude Code 中输入/hooks-pattern确认已加载。
详细介绍
Hooks 模式
目录
React 16.8 引入了一项名为 Hooks 的新功能。Hooks 让你无需使用 ES2015 类组件,就能使用 React 状态和生命周期方法。
Hooks 虽然不一定算是一种设计模式,但在应用设计中扮演着非常重要的角色。许多传统设计模式都可以用 Hooks 取代。
何时使用
- 需要为函数组件添加状态或生命周期行为时,请使用 Hooks
- 这有助于提取状态逻辑,并在多个组件间复用
- 使用 Hooks 取代类组件,让代码更简洁、更易组合
使用说明
- 在函数组件中,使用
useState管理本地状态,使用useEffect处理副作用 - 创建自定义 Hooks(名称以
use为前缀),封装并共享可复用的逻辑 - 遵循 Hooks 规则:只在顶层调用 Hooks,并且只在 React 函数中调用
- 避免不必要的
useEffect——直接在组件主体中计算派生状态 - 尽可能让 React Compiler 处理记忆化,而不是手动使用
useMemo/useCallback
详情
类组件
在 React 引入 Hooks 之前,我们必须使用类组件,才能为组件添加状态和生命周期方法。React 中一个典型的类组件可能如下所示:
class MyComponent extends React.Component {
/* Adding state and binding custom methods */
constructor() {
super()
this.state = { ... }
this.customMethodOne = this.customMethodOne.bind(this)
this.customMethodTwo = this.customMethodTwo.bind(this)
}
/* Lifecycle Methods */
componentDidMount() { ...}
componentWillUnmount() { ... }
/* Custom methods */
customMethodOne() { ... }
customMethodTwo() { ... }
render() { return { ... }}
}
类组件可以在构造函数中包含状态,使用 componentDidMount 和 componentWillUnmount 等生命周期方法,根据组件的生命周期执行副作用,也可以通过自定义方法为类添加额外逻辑。
虽然 React Hooks 引入后仍然可以使用类组件,但类组件也有一些缺点!下面来看看使用类组件时最常见的一些问题。
理解 ES2015 类
在 React Hooks 出现之前,类组件是唯一能够处理状态和生命周期方法的组件,因此我们经常不得不将函数组件重构为类组件,以添加额外功能。
在这个示例中,我们有一个简单的 div,它的作用是充当按钮。
function Button() {
return <div className="btn">disabled</div>;
}
我们不希望它始终显示 disabled,而是希望用户点击按钮时将其改为 enabled,并在此时为按钮添加一些额外的 CSS 样式。
为此,我们需要为组件添加状态,以便判断状态是 enabled 还是 disabled。这意味着我们必须彻底重构函数组件,将其改为跟踪按钮状态的类组件。
export default class Button extends React.Component {
constructor() {
super();
this.state = { enabled: false };
}
render() {
const { enabled } = this.state;
const btnText = enabled ? "enabled" : "disabled";
return (
<div
className={`btn enabled-${enabled}`}
onClick={() => this.setState({ enabled: !enabled })}
>
{btnText}
</div>
);
}
}
在这个示例中,组件很小,重构起来并不麻烦。不过,实际项目中的组件可能包含更多代码行,因此重构组件会困难得多。
除了确保重构时不会意外改变任何行为外,你还需要理解 ES2015 类的工作方式。为什么必须对自定义方法使用 bind?constructor 有什么作用?this 关键字从何而来?如果不想意外改变数据流,正确地重构组件可能会很困难。
重构
在多个组件之间共享代码的常见方式,是使用高阶组件或 Render Props 模式。虽然这两种模式都有效,也是良好的实践,但如果要在后续阶段引入这些模式,就需要重构应用。
除了必须重构应用之外(组件越大,重构就越棘手),为了在更深层嵌套的组件之间共享代码而使用许多包装组件,还可能导致一种常被称为 包装地狱 的情况。打开开发者工具时,看到类似这样的结构并不少见:
<WrapperOne>
<WrapperTwo>
<WrapperThree>
<WrapperFour>
<WrapperFive>
<Component>
<h1>Finally in the component!</h1>
</Component>
</WrapperFive>
</WrapperFour>
</WrapperThree>
</WrapperTwo>
</WrapperOne>
包装地狱 会让人难以理解数据如何在应用中流动,因此也更难弄清楚意外行为发生的原因。
复杂性
随着我们向类组件添加更多逻辑,组件的体积会迅速增大。组件内的逻辑可能会变得纠缠不清、缺乏结构,让开发者难以理解类组件中的特定逻辑用在何处。这会增加调试和优化性能的难度。
生命周期方法也会导致代码中出现大量重复。
即使组件很小,其中的逻辑也可能已经相当纠缠不清。有些部分专用于 counter 逻辑,另一些部分则专用于 width 逻辑。随着组件不断增大,在组件内组织逻辑、查找相关逻辑会变得越来越困难。
除了逻辑纠缠不清,我们还在生命周期方法中重复了一些逻辑。在 componentDidMount 和 componentWillUnmount 中,我们都根据窗口的 resize 事件自定义了应用的行为。
Hooks
很明显,类组件并不总是 React 的优秀特性。为了解决 React 开发者使用类组件时可能遇到的常见问题,React 引入了 React Hooks。React Hooks 是可用于管理组件状态和生命周期方法的函数。React Hooks 让我们能够:
- 为函数组件添加状态
- 无需使用
componentDidMount和componentWillUnmount等生命周期方法,即可管理组件的生命周期 - 在整个应用的多个组件之间复用相同的有状态逻辑
首先,来看看如何使用 React Hooks 为函数组件添加状态。
State Hook
React 提供了一个名为 useState 的 Hook,用于管理函数组件中的状态。
接下来看看如何使用 useState Hook,将类组件重构为函数组件。我们有一个名为 Input 的类组件,它只渲染一个输入框。每当用户在输入框中输入内容时,状态中的 input 值就会更新。
class Input extends React.Component {
constructor() {
super();
this.state = { input: "" };
this.handleInput = this.handleInput.bind(this);
}
handleInput(e) {
this.setState({ input: e.target.value });
}
render() {
<input onChange={handleInput} value={this.state.input} />;
}
}
要使用 useState Hook,我们需要访问 React 提供的 useState 方法。useState 方法需要一个参数:状态的初始值,在本例中是空字符串。
我们可以从 useState 方法中解构出两个值:
- 状态的当前值。
- 用于更新状态的方法。
const [value, setValue] = React.useState(initialValue);
第一个值可以与类组件的 this.state.[value] 相对应。
第二个值可以与类组件的 this.setState 方法相对应。
由于我们处理的是输入框的值,因此将状态的当前值称为 input,将用于更新状态的方法称为 setInput。初始值应为空字符串。
const [input, setInput] = React.useState("");
现在,我们可以将 Input 类组件重构为有状态的函数组件。
function Input() {
const [input, setInput] = React.useState("");
return <input onChange={(e) => setInput(e.target.value)} value={input} />;
}
input 字段的值等于 input 状态的当前值,与类组件示例相同。当用户在输入框中输入内容时,input 状态的值会通过 setInput 方法相应更新。
Effect Hook
我们已经看到,可以使用 useState 组件在函数组件中处理状态,但类组件的另一项优势是可以为组件添加生命周期方法。
借助 useEffect Hook,我们可以“接入”组件的生命周期。useEffect Hook 实际上结合了 componentDidMount、componentDidUpdate 和 componentWillUnmount 生命周期方法。
componentDidMount() { ... }
useEffect(() => { ... }, [])
componentWillUnmount() { ... }
useEffect(() => { return () => { ... } }, [])
componentDidUpdate() { ... }
useEffect(() => { ... })
我们来使用 State Hook 部分中的输入示例。每当用户在输入框中输入内容时,我们还希望将该值记录到控制台。
我们需要使用一个 useEffect Hook 来“监听”input 值。为此,可以将 input 添加到 useEffect Hook 的依赖数组中。依赖数组是 useEffect Hook 接收的第二个参数。
useEffect(() => {
console.log(`The user typed ${input}`);
}, [input]);
现在,每当用户输入一个值,该值都会记录到控制台。
自定义 Hooks
除了 React 提供的内置 Hooks(useState、useEffect、useReducer、useRef、useContext、useMemo、useImperativeHandle、useLayoutEffect、useDebugValue、useCallback)之外,我们还可以轻松创建自己的自定义 Hooks。
你可能已经注意到,所有 Hooks 都以 use 开头。Hooks 的名称以 use 开头很重要,这样 React 才能检查是否违反了 Hooks 规则。
假设我们想跟踪用户在输入时可能按下的某些按键。自定义 Hook 应能接收我们想要监听的按键作为参数。
function useKeyPress(targetKey) {
const [keyPressed, setKeyPressed] = React.useState(false);
function handleDown({ key }) {
if (key === targetKey) {
setKeyPressed(true);
}
}
function handleUp({ key }) {
if (key === targetKey) {
setKeyPressed(false);
}
}
React.useEffect(() => {
window.addEventListener("keydown", handleDown);
window.addEventListener("keyup", handleUp);
return () => {
window.removeEventListener("keydown", handleDown);
window.removeEventListener("keyup", handleUp);
};
}, []);
return keyPressed;
}
现在,我们不必将按键逻辑局限在 Input 组件中,而是可以在多个组件中重复使用 useKeyPress Hook,无需一遍又一遍地重写相同的逻辑。
Hooks 的另一项显著优势是,社区可以构建并分享 Hooks。以下网站列出了社区构建的、可在应用程序中直接使用的 Hooks:
使用 React Hooks 重写计数器和宽度示例后,我们可以将 App 函数的逻辑拆分为几个部分:
useCounter:一个自定义 Hook,返回count的当前值、increment方法和decrement方法。useWindowWidth:一个自定义 Hook,返回窗口的当前宽度。App:一个有状态的函数组件,返回Counter和Width组件。
通过使用 React Hooks 而非类组件,我们能够将逻辑拆分为更小、可复用的部分,从而分离逻辑。
使用 React Hooks 让我们能够更清晰地将组件逻辑拆分为几个更小的部分。_复用_相同的有状态逻辑也变得容易得多;如果想让组件具有状态,我们不再需要将函数组件重写为类组件。不再需要深入掌握 ES2015 类,而且可复用的有状态逻辑还能提高组件的可测试性、灵活性和可读性。
Hooks 补充指南
与其他组件一样,如果想将 Hooks 添加到已编写的代码中,也会用到一些特殊函数。下面简要介绍一些常见的 Hook 函数:
useState
useState Hook 让开发者无需将函数组件转换为类组件,就能在其中更新和操作状态。这一 Hook 的优势之一是简单易用,且不需要像其他 React Hooks 那样复杂。
useEffect
useEffect Hook 用于在函数组件的主要生命周期事件中运行代码。函数组件的主体中不允许进行修改、订阅、设置计时器、记录日志以及执行其他副作用。如果允许这些操作,可能会导致 UI 出现令人困惑的错误和不一致。useEffect Hook 可以避免这些“副作用”,让 UI 平稳运行。它将 componentDidMount、componentDidUpdate 和 componentWillUnmount 合并在了一处。
useContext
useContext Hook 接受一个上下文对象,也就是 React.createContext 返回的值,并返回该上下文当前的值。useContext Hook 还可以与 React Context API 配合使用,在整个应用中共享数据,而无需将应用的 props 层层传递下去。
需要注意的是,传递给 useContext Hook 的参数必须是上下文对象本身;每当上下文值发生变化,调用 useContext 的组件都会重新渲染。
useReducer
useReducer Hook 是 setState 的一种替代方案。当状态逻辑较复杂、涉及多个子值,或者下一个状态取决于前一个状态时,它尤其适用。它接收一个 reducer 函数和初始状态作为输入,并通过数组解构返回当前状态和一个 dispatch 函数。useReducer 还能优化会触发深层更新的组件的性能。
优点
代码行数更少
Hooks 让你可以按关注点和功能对代码进行分组,而非按生命周期分组。这不仅让代码更整洁、简洁,也减少了代码行数。下面对比了一个使用 React 编写的可搜索产品数据表状态组件,以及使用 useState 关键字后通过 Hooks 实现的写法。
有状态组件
class TweetSearchResults extends React.Component {
constructor(props) {
super(props);
this.state = {
filterText: "",
inThisLocation: false,
};
this.handleFilterTextChange = this.handleFilterTextChange.bind(this);
this.handleInThisLocationChange =
this.handleInThisLocationChange.bind(this);
}
handleFilterTextChange(filterText) {
this.setState({
filterText: filterText,
});
}
handleInThisLocationChange(inThisLocation) {
this.setState({
inThisLocation: inThisLocation,
});
}
render() {
return (
<div>
<SearchBar
filterText={this.state.filterText}
inThisLocation={this.state.inThisLocation}
onFilterTextChange={this.handleFilterTextChange}
onInThisLocationChange={this.handleInThisLocationChange}
/>
<TweetList
tweets={this.props.tweets}
filterText={this.state.filterText}
inThisLocation={this.state.inThisLocation}
/>
</div>
);
}
}
使用 Hooks 实现的同一组件
const TweetSearchResults = ({ tweets }) => {
const [filterText, setFilterText] = useState("");
const [inThisLocation, setInThisLocation] = useState(false);
return (
<div>
<SearchBar
filterText={filterText}
inThisLocation={inThisLocation}
setFilterText={setFilterText}
setInThisLocation={setInThisLocation}
/>
<TweetList
tweets={tweets}
filterText={filterText}
inThisLocation={inThisLocation}
/>
</div>
);
};
简化复杂组件
JavaScript 类可能难以管理、难以配合热重载使用,也可能无法很好地进行压缩。React Hooks 解决了这些问题,让函数式编程更容易实现。采用 Hooks 后,我们无需再使用类组件。
复用有状态逻辑
JavaScript 中的类鼓励多层继承,这会迅速增加整体复杂度和潜在错误。不过,Hooks 让你无需编写类即可使用状态和其他 React 功能。借助 React,你可以始终复用有状态逻辑,无需反复重写代码。这能减少出错的可能性,并支持使用普通函数进行组合。
共享非视觉逻辑
在 Hooks 实现之前,React 无法提取和共享非视觉逻辑。这最终导致了更多复杂性,例如为解决常见问题而采用 HOC 模式和 Render props。不过,Hooks 的引入解决了这一问题,因为它允许将有状态逻辑提取到一个简单的 JavaScript 函数中。
当然,使用 Hooks 时也有一些潜在缺点需要牢记:
- 必须遵守其规则;如果没有 linter 插件,就很难知道违反了哪条规则。
- 需要投入相当多的时间练习才能正确使用(例如:useEffect)。
- 要注意避免错误使用(例如:useCallback、useMemo)。
React Hooks 与类组件
React 引入 Hooks 后,出现了一个新问题:我们该如何判断何时使用带 Hooks 的函数组件,何时使用类组件?借助 Hooks,即使在函数组件中,也可以使用状态和部分生命周期 Hooks。Hooks 还允许你使用本地状态和其他 React 功能,而无需编写类。
以下是 Hooks 和类组件的一些区别,可帮助你做出选择:
| React Hooks | 类组件 |
|---|---|
| 有助于避免多层级结构,让代码更清晰 | 通常,使用 HOC 或 renderProps 时,如果想在 DevTools 中查看应用,就必须将应用重构为多层级结构 |
| 让 React 组件之间更加统一。 | 由于需要理解绑定以及函数的调用上下文,类组件会让人和机器都感到困惑。 |