React 中常用的 Hook 介绍(一)
2023年06月11日星期日
React Hooks 的诞生是为了简化组件开发、提高逻辑复用性,并解决类组件在状态管理和生命周期方面的局限性。它使得开发者可以更直观地管理状态和副作用,同时提升了代码的可读性和复用性,极大地改善了 React 的开发体验。
什么是Hook?
Hook
有"钩子"的意思。React Hooks
的意思是,组件尽量写成纯函数,如果需要外部功能和副作用,就用钩子把外部代码"钩"进来。React Hooks
就是那些钩子 1
1.useState
useState
是 React 中最常用的 Hook,用于在函数组件中声明和管理状态。它可以让函数组件具备类似类组件中的 this.state
和 this.setState
功能。
基本语法
const [state, setState] = useState(initialValue)
state
:当前的状态值。setState
:更新状态的函数。initialValue
:状态的初始值,可以是任意类型(数字、字符串、对象、数组等)。
管理简单的状态
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>当前计数:{count}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
</div>
);
}
export default Counter;
- 初始状态
count
为 0。 - 点击按钮时调用
setCount
,将count
的值增加 1。 - 组件会重新渲染,并显示更新后的
count
。
使用函数更新状态
当更新状态依赖于前一个状态值时,使用函数形式的 setState
更加安全。
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>当前计数:{count}</p>
<button onClick={() => setCount(prevCount => prevCount + 1)}>增加</button>
</div>
);
}
setCount(prevCount => prevCount + 1)
中的prevCount
表示之前的状态值。- 这种方式可以确保在异步操作或多次快速点击时,状态更新是准确的。
管理对象状态
useState
也可以用来管理对象状态。在更新对象时,需要使用解构赋值或 spread
操作符来保持其他字段不变。
function UserInfo() {
const [user, setUser] = useState({ name: 'Alice', age: 25 });
const updateName = () => {
setUser(prevUser => ({ ...prevUser, name: 'Bob' }));
};
return (
<div>
<p>姓名:{user.name}</p>
<p>年龄:{user.age}</p>
<button onClick={updateName}>修改姓名</button>
</div>
);
}
- 初始状态
user
是一个对象。 - 在
updateName
函数中使用setUser
更新name
字段,同时通过{ ...prevUser }
保留了age
字段。 - React 会重新渲染组件并显示新的用户信息。
管理数组状态
function TodoList() {
const [todos, setTodos] = useState(['学习 React', '写代码']);
const addTodo = () => {
setTodos(prevTodos => [...prevTodos, '复习 useState']);
};
return (
<div>
<ul>
{todos.map((todo, index) => (
<li key={index}>{todo}</li>
))}
</ul>
<button onClick={addTodo}>添加任务</button>
</div>
);
}
- 初始状态
todos
是一个数组。 - 在
addTodo
函数中,通过[...prevTodos, '复习 useState']
添加新任务,保持了之前的任务列表。 - 使用
map
方法渲染每个任务。
惰性初始化
如果初始状态计算开销较大,可以使用惰性初始化,即将一个函数传给 useState,仅在组件首次渲染时调用该函数来计算初始值。
function ExpensiveComponent() {
const [value, setValue] = useState(() => {
console.log('计算初始值...');
return expensiveCalculation();
});
return <div>值:{value}</div>;
}
function expensiveCalculation() {
// 模拟耗时操作
return 42;
}
useState(() => expensiveCalculation())
只会在首次渲染时调用expensiveCalculation
计算初始值。- 避免了每次渲染都重新计算初始状态,提高了性能。
useState 的常见陷阱
状态更新是异步的
useState
的状态更新并不会立即生效,而是会在下一次渲染时生效。因此,如果需要依赖当前状态的值来进行更新,建议使用函数形式的 setState
。
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
};
return (
<div>
<p>当前计数:{count}</p>
<button onClick={handleClick}>增加</button>
</div>
);
}
-
问题: 点击按钮时,
count
只会增加 1,而不是预期的 3。 -
原因:
setCount
是异步的,每次调用setCount
时,count
的值都还是之前的 0。因此,最终只会增加一次。
2.useEffect
useEffect
是 React 中用于管理副作用的 Hook。它允许在函数组件中执行副作用操作,如数据获取、订阅事件、计时器、手动修改 DOM 等。
基本语法
useEffect(() => {
// 副作用逻辑
return () => {
// 清理副作用(可选)
};
}, [依赖数组]);
- 第一个参数:一个函数,包含要执行的副作用逻辑。
- 返回值(可选):返回一个函数,用于清理副作用(如取消订阅、清除计时器等),在组件卸载之前或者在下一次效果触发之前执行。
- 依赖数组:控制
useEffect
的执行时机。如果数组为空 [],则只在组件挂载时(首次渲染后)运行。
在组件挂载时运行副作用
import React, { useEffect } from 'react';
function Fun() {
useEffect(() => {
console.log('组件已挂载');
}, []); // 空依赖数组,表示只在组件挂载后运行
return <div>Hello, useEffect!</div>;
}
export default Example;
useEffect
中的回调函数只会在组件首次挂载时执行一次。
依赖数组控制副作用
import React, { useState, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(`计数器更新为:${count}`);
}, [count]); // 依赖于 count,当 count 发生变化时执行
return (
<div>
<p>当前计数:{count}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
</div>
);
}
- 依赖数组
[count]
表示只有count
发生变化时,useEffect
才会执行。 - 每次点击按钮时,
useEffect
会输出当前的计数值。
清理副作用
import React, { useState, useEffect } from 'react';
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(prevCount => prevCount + 1);
}, 1000);
// 返回清理函数,组件卸载时和下一次执行回调时清除定时器
return () => clearInterval(interval);
}, []); // 空依赖数组,表示只在挂载时设置定时器
return <p>计时器:{count}</p>;
}
export default Timer;
- 在
useEffect
的返回值中,clearInterval
用于清理定时器,防止内存泄漏。 - 这种模式常用于订阅事件、设置计时器等需要清理的副作用。
模拟数据获取
import React, { useState, useEffect } from 'react';
function DataFetcher() {
const [data, setData] = useState(null);
useEffect(() => {
async function fetchData() {
const response = await fetch('https://xxxxx.com/data');
const result = await response.json();
setData(result);
}
fetchData();
}, []); // 空依赖数组,表示只在组件挂载时运行
return <div>{data ? JSON.stringify(data) : '加载中...'}</div>;
}
export default DataFetcher;
useEffect
中的异步函数fetchData
负责获取数据并更新状态。- 由于依赖数组为空,
fetchData
只会在组件挂载时调用一次。
依赖数组为空和省略依赖数组的区别
useEffect(() => {
console.log('每次渲染都会执行');
});
- 空依赖数组
[]
:useEffect
只会在组件初次渲染时运行。 - 省略依赖数组:
useEffect
会在每次组件渲染时运行,类似于类组件中的componentDidMount
和componentDidUpdate
。
3.useRef
useRef 是 React 提供的一个 Hook
使用场景
- 访问 DOM 元素
- 在组件生命周期内存储可变值(不触发重新渲染)
基本语法
const refContainer = useRef(initialValue);
initialValue
是ref
的初始值,可以是null
或任意值。- 返回值是一个包含
.current
属性的对象,refContainer.current
可以存储值或 DOM 节点。
访问 DOM 元素
useRef
常用于获取和操作 DOM 元素,类似于类组件中的 React.createRef
import React, { useRef } from 'react';
function InputFocus() {
const inputRef = useRef(null);
const handleFocus = () => {
inputRef.current.focus(); // 通过 ref 获取 DOM 元素并调用 focus 方法
};
return (
<div>
<input ref={inputRef} type="text" placeholder="点击按钮聚焦" />
<button onClick={handleFocus}>聚焦输入框</button>
</div>
);
}
export default InputFocus;
inputRef
使用useRef
创建,并通过ref
属性绑定到<input>
元素。- 点击按钮时,通过
inputRef.current
获取<input>
元素并调用其focus
方法。
存储可变值
useRef
可以存储在组件生命周期内需要保持的值,但更新这个值不会触发组件重新渲染。
import React, { useState, useEffect, useRef } from 'react';
function PreviousValue() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count; // 保存当前 count 值到 ref
});
return (
<div>
<p>当前值:{count}</p>
<p>前一次值:{prevCountRef.current}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
</div>
);
}
export default PreviousValue;
- 每次
count
更新时,通过useEffect
将当前count
存入prevCountRef.current
。 - 在渲染时显示前一次的
count
值。
避免闭包陷阱
import React, { useState, useRef, useEffect } from 'react';
function Timer() {
const [count, setCount] = useState(0);
const timerRef = useRef(null);
const startTimer = () => {
timerRef.current = setInterval(() => {
setCount(prevCount => prevCount + 1);
}, 1000);
};
const stopTimer = () => {
clearInterval(timerRef.current);
};
useEffect(() => {
return () => clearInterval(timerRef.current); // 组件卸载时清理定时器
}, []);
return (
<div>
<p>计时器:{count}</p>
<button onClick={startTimer}>开始</button>
<button onClick={stopTimer}>停止</button>
</div>
);
}
export default Timer;
timerRef
存储了定时器的 ID,方便在停止计时或组件卸载时清除定时器。- 通过
useRef
避免了闭包陷阱,因为timerRef.current
始终指向最新的定时器 ID。
4.useContext
useContext
是 React 的一个 Hook,用于在函数组件中订阅 React 上下文(Context)。它可以帮助组件直接访问共享的状态或数据,而不需要通过多层级的 props
传递。
使用场景
- 在组件树中共享状态,例如:主题、用户信息、语言设置等。
- 通过上下文避免繁琐的
props drilling
(逐层传递 props)。
使用示例(主题切换)
import React, { createContext, useState, useContext } from 'react';
// 创建一个 ThemeContext,初始值为 'light'
const ThemeContext = createContext('light');
function App() {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(theme === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={theme}>
<Page />
<button onClick={toggleTheme}>切换主题</button>
</ThemeContext.Provider>
);
}
function Page() {
const theme = useContext(ThemeContext); // 订阅 ThemeContext
return (
<div style={{ backgroundColor: theme === 'light' ? '#fff' : '#333', color: theme === 'light' ? '#000' : '#fff' }}>
当前主题:{theme}
</div>
);
}
参考资料
Footnotes
-
阮一峰. React Hooks入门教程 ↩