常用的 React Hooks
常用的内置 React Hooks 一共有 8 个,分别是:
useState
(前面已学习)useContext
(前面已学习)useReducer
useRef
forwardRef
&useImperativeHandle
useEffect
useMemo
useCallback
一、useReducer
useReducer
接收一个 reducer 函数和初始状态作为参数,并返回当前状态以及一个 dispatch 函数。这个 Hook 特别适用于管理复杂或嵌套的状态对象,以及当状态之间的逻辑比较复杂或需要多个状态共同作用时。
工作原理
- Reducer 函数:Reducer 函数是一个纯函数,它接收当前的状态和一个代表“动作”的对象(action)作为参数。基于
action.type
,它决定如何计算并返回新的状态。Reducer 函数不应该修改传入的状态,而是应该返回一个新的状态对象。 - Dispatch 函数:通过
useReducer
返回的 dispatch 函数,可以发送一个 action 到 reducer 函数,从而触发状态的更新。这个过程是同步的,即 dispatch 函数被调用后,reducer 函数会立即执行,并返回新的状态。
示例
以下是一个使用 useReducer
管理简单计数器状态的示例:
import { useReducer } from "react";
// 定义初始状态
const initialState = { count: 0 };
// 定义 reducer 函数
function countReducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return initialState;
default:
throw new Error();
}
}
function App() {
const [state, dispatch] = useReducer(countReducer, initialState)
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</>
);
}
export default App;
与 useState 的对比
useReducer
和 useState
都可以用于状态管理,但它们在适用场景上有所不同:
useState
适用于简单的状态逻辑,它允许直接在组件内部更新状态。useReducer
则适用于复杂的状态逻辑,特别是当状态之间的更新逻辑相互依赖时。通过使用 reducer 函数,可以将状态更新逻辑封装在一个地方,使代码更加清晰和可维护。
二、useRef
useRef
主要用于在函数组件中创建一个可以在组件的整个生命周期内保持不变的引用对象。这个引用对象的 .current
属性是可变的,可以用来存储任何值,包括 DOM 元素的引用或者普通的变量值。
它的主要用途有以下两个:
-
访问 DOM 元素
useRef
最常见的用途之一是获取 DOM 元素的引用。可以通过给元素设置ref
属性,并将useRef
返回的引用对象作为值传递给它。然后通过访问.current
属性来访问这个 DOM 元素。这对于需要直接操作 DOM(如设置焦点、测量尺寸等)的场景非常有用。import { useRef } from "react"; function App() { const inputEl = useRef(null) const onButtonClick = () => { // current 指向已挂载到 DOM 上的文本输入元素 inputEl.current.focus(); } return ( <> <input ref={inputEl} type="text" /> <button onClick={onButtonClick}>Focus the input</button> </> ); } export default App;
-
存储可变值
除了访问 DOM 元素外,
useRef
还可以用来存储任何不会引起组件重新渲染的可变值。这在处理定时器、动画、媒体播放等场景时特别有用,因为可以安全地更新这些值而不用担心触发不必要的渲染。function TimerComponent() { const secondsElapsed = useRef(0); useEffect(() => { const intervalId = setInterval(() => { // 更新 secondsElapsed.current 而不会触发组件重新渲染 secondsElapsed.current += 1; console.log(`Seconds elapsed: ${secondsElapsed.current}`); }, 1000); // 组件卸载时清除定时器 return () => clearInterval(intervalId); }, []); return <div>Timer Component</div>; }
三、forwardRef & useImperativeHandle
forwardRef
和 useImperativeHandle
是两个与 Refs 和组件通信相关的 Hooks,它们主要用于父组件需要直接访问子组件内部 DOM 节点或子组件中定义的函数时。
这两个 Hooks 一起使用,可以创建一个能够暴露给父组件特定 API 的封装组件。
import { forwardRef, useImperativeHandle, useRef } from "react";
const Child = forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({
myFn: () => {
console.log('子组件');
}
}))
return (
<div>子组件</div>
);
})
function App() {
const childRef = useRef(null)
const handleClick = () => {
childRef.current.myFn();
}
return (
<>
<Child ref={childRef}/>
<button onClick={handleClick}>按钮</button>
</>
);
}
export default App;
四、useEffect
useEffect
用于在函数组件中执行副作用操作。这些操作可以是数据获取、设置订阅、手动更改 DOM 等,它们通常发生在组件渲染到屏幕之后。
在类组件中,有生命周期方法(如
componentDidMount
、componentDidUpdate
和componentWillUnmount
)来处理副作用,但在函数组件中,没有这些方法,所以使用useEffect
来替代。
useEffect
接收两个参数:
- 一个函数:这个函数包含了想执行的副作用操作。
- 一个依赖项数组(可选) :这个数组中的值用于决定副作用函数何时应该重新执行。只有当数组中的值变化时,副作用函数才会再次执行。如果省略这个数组,副作用函数会在每次组件渲染后都执行。
import { useState, useEffect } from 'react';
function MyComponent() {
const [data, setData] = useState(null);
// 使用 useEffect 获取数据
useEffect(() => {
// 这个函数会在组件挂载后执行
fetch('https://api.example.com/data')
.then(response => response.json())
.then(json => setData(json));
// 可选:返回一个清理函数,组件卸载时执行
return () => {
console.log('组件卸载');
// 在这里可以执行清理操作,比如取消订阅等
};
}, []); // 依赖项数组为空,意味着副作用只在组件挂载时执行一次
return (
<div>
{data ? <p>{data.message}</p> : <p>Loading...</p>}
</div>
);
}
export default MyComponent;
在这个例子中,useEffect
用于在组件挂载后从 API 获取数据,并在数据获取成功后更新组件的状态。由于依赖项数组为空,所以副作用函数只在组件首次渲染到 DOM 后执行一次。如果依赖项数组中有值,并且这些值在组件的后续渲染中发生了变化,那么副作用函数会再次执行。
五、useMemo
useMemo 类似于 Vue 中的 computed,主要用于缓存数据。它接收两个参数:一个计算函数和一个依赖项数组。
- 计算函数:这是想要缓存其返回值的函数。
- 依赖项数组:这是一个包含所有在计算函数中使用的响应式变量的数组。当这些依赖项发生变化时,
useMemo
会重新调用计算函数并缓存新的返回值。
import { useMemo, useState } from "react";
function App() {
const [num, setNum] = useState(5)
const result = useMemo(() => {
return num * 2
}, [num])
const handleClick = () => {
setNum(num + 1)
}
return (
<>
<div>{result}</div>
<button onClick={handleClick}>按钮</button>
</>
);
}
export default App;
六、useCallback
useCallback
可以缓存一个函数,以避免在组件的每次渲染中都重新创建该函数。
在 React 中,如果父组件将函数作为 prop 传递给子组件,并且这个函数在每次父组件渲染时都重新创建(比如直接定义在父组件的函数体内),那么即使这个函数的内容没有变化,子组件也会因为接收到一个新的函数引用而可能触发不必要的渲染。为了优化这种情况,可以使用 useCallback
来确保函数在依赖项没有变化时保持不变。
useCallback
接收两个参数:一个回调函数和一个依赖项数组。它返回一个新的回调函数,这个回调函数在依赖项没有变化时会保持与上一次渲染时相同。如果依赖项中的任何值发生变化,useCallback
会重新创建回调函数。
假设有一个按钮,每次点击时都会增加一个计数器的值。将这个函数传递给子组件,但不希望子组件因为接收到新的函数引用而每次都渲染。
不使用 useCallback
的代码:
function ParentComponent() {
const [count, setCount] = useState(0);
// 没有使用 useCallback,每次渲染都会创建一个新的 increment 函数
const increment = () => {
setCount(count + 1);
};
return (
<div>
<ChildComponent increment={increment} />
<p>Count: {count}</p>
</div>
);
}
每次 ParentComponent
渲染时,increment
函数都会被重新创建,因为它是在渲染方法内部定义的。
使用 useCallback
的代码:
function ParentComponent() {
const [count, setCount] = useState(0);
// 使用 useCallback 来缓存 increment 函数
// 注意:这里依赖项数组是空的,因为没有在 increment 函数内部直接使用 count
const increment = useCallback(() => {
setCount(count => count + 1); // 使用函数式更新来避免直接依赖 count 的值
}, []);
return (
<div>
<ChildComponent increment={increment} />
<p>Count: {count}</p>
</div>
);
}
由于 increment
函数并没有直接使用 count
的值(而是使用了setCount
的函数式更新),所以依赖项数组应该是空的。因为依赖项数组为空,所以 increment
函数只会在组件首次渲染时创建一次,并在后续的渲染中保持不变。