React Hooks 是 React 16.8 引入的一项重要特性,它使函数组件能够拥有类组件的一些特性,例如状态管理和生命周期方法的使用。
React Hooks 是一种函数式组件的增强机制,它允许你在不编写类组件的情况下使用 React 的特性。主要的 Hooks 包括 useState, useEffect, useContext, useReducer, useCallback, useMemo, useRef, 和 useImperativeHandle 等。这些 Hooks 提供了访问 React 特性的方式,使得你可以更好地组织和重用你的代码。
下面是几个常用的Hooks
1、useState
允许你在函数组件中使用局部状态。它返回一个状态值和更新该状态值的函数。
import React, { useState } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
const reset = () => setCount(0);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
<button onClick={reset}>Reset</button>
</div>
);
};
它返回一个状态值和一个设置该状态的函数:const [state, setState] = useState(initialState);
每次需要更新状态时,你会调用 setState,并传入新的状态值。
2、useReducer
用于更复杂的 state 逻辑,它接收一个 reducer 函数和初始状态,然后返回当前的状态和派发 action 的 dispatch 函数。
import React, { useReducer } from 'react';
const initialState = { count: 0 };
const counterReducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
case 'RESET':
return { count: 0 };
default:
return state;
}
};
const Counter = () => {
const [state, dispatch] = useReducer(counterReducer, initialState);
return (
<div>
<h1>Count: {state.count}</h1>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button>
<button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
</div>
);
};
它将状态更新的逻辑封装在一个函数中,并根据动作(action)的类型来决定如何更新状态。
使用 useReducer 钩子时,状态更新逻辑被集中在一个地方,使得代码更具可读性和可维护性。
3、useContext
用于访问 React context 在组件树中传递的数据,而不必通过每个组件传递 props。
// 【1】数据组件
import React, { useState, useContext, createContext } from 'react';
// 创建一个上下文
const CounterContext = createContext();
// 创建一个提供者组件
const CounterProvider = ({ children }) => {
const [count, setCount] = useState(0);
// 定义更新状态的函数
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
const reset = () => setCount(0);
// 在上下文中共享状态和更新函数
return (
<CounterContext.Provider value={{ count, increment, decrement, reset }}>
{children}
</CounterContext.Provider>
);
};
// 【2】创建一个消费组件
const Counter = () => {
const { count, increment, decrement, reset } = useContext(CounterContext);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
<button onClick={reset}>Reset</button>
</div>
);
};
// 【3】在应用中使用提供者组件
const App = () => {
return (
<CounterProvider>
<Counter />
{/* 你可以在这里添加其他组件,它们也能访问 CounterContext */}
</CounterProvider>
);
};
export default App;
CounterContext:使用 createContext 创建一个上下文。这是共享状态的方式。
CounterProvider:这是一个负责提供状态的组件。它使用 useState 来管理状态,并将状态和更新函数通过上下文提供给子组件。
useContext(CounterContext):在 Counter 组件中,通过 useContext 钩子访问上下文中的状态和更新函数。
App 组件:在应用中使用 CounterProvider 包裹住需要使用共享状态的组件。这样,Counter 组件及其子组件都可以访问 CounterContext 中的状态。
4、useCallback
用于返回一个 memoized 版本的回调函数,防止不必要的渲染。
import React, { useState, useCallback } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
// 使用 useCallback 来优化函数的创建
const increment = useCallback(() => setCount(count + 1), [count]);
const decrement = useCallback(() => setCount(count - 1), [count]);
const reset = useCallback(() => setCount(0), []);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
<button onClick={reset}>Reset</button>
</div>
);
};
它接收两个参数:一个是要记忆的函数,另一个是依赖数组。
只有当依赖数组中的某个依赖项发生变化时,useCallback 才会返回一个新的函数实例。否则,它会返回之前记忆的函数实例。
使用 useCallback 可以避免在每次组件重渲染时重新创建这些函数。这在某些情况下(例如,当这些函数被传递给子组件时)可以减少不必要的子组件重渲染。
5、useEffect
允许你在函数组件中执行副作用操作(如数据获取、订阅管理、DOM 操作等)。它在每次渲染后都会执行。
import React, { useState, useEffect } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
const reset = () => setCount(0);
// 使用 useEffect 来处理副作用
useEffect(() => {
console.log(`Count has changed to: ${count}`);
// 可以在这里添加清理逻辑,比如取消订阅
// return () => {
// console.log('Cleanup logic here');
// };
}, [count]); // 依赖数组,指定副作用何时执行
return (
<div>
<h1>Count: {count}</h1>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
<button onClick={reset}>Reset</button>
</div>
);
};
依赖数组:
- 在这个例子中,
useEffect的依赖数组是[count],这意味着每次count发生变化时,副作用函数都会执行。 - 如果省略依赖数组(即不传递第二个参数),副作用函数将在每次组件渲染时都执行。
- 如果传递一个空的依赖数组(
[]),副作用函数将只在组件初次渲染时执行一次。
useCallback 和 useEffect 是用于不同场景的钩子。useCallback 主要用于优化性能,而 useEffect 主要用于处理副作用。
6、useMemo
用于对计算结果进行记忆,避免在每次渲染时重复计算。
import React, { useState, useMemo } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
const reset = () => setCount(0);
// 使用 useMemo 来记忆计算值
const doubleCount = useMemo(() => {
console.log('Calculating double count...');
return count * 2;
}, [count]); // 依赖数组,指定何时重新计算
return (
<div>
<h1>Count: {count}</h1>
<h2>Double Count: {doubleCount}</h2>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
<button onClick={reset}>Reset</button>
</div>
);
};
在这个例子中,useMemo 的依赖数组是 [count],这意味着每次 count 发生变化时,doubleCount 都会被重新计算。
如果省略依赖数组,useMemo 将在每次组件渲染时都重新计算值。
如果传递一个空的依赖数组([]),useMemo 将只在组件初次渲染时计算一次值。
useMemo 用于记忆计算结果,适用于需要避免重复计算的场景。
区别:
useEffect 用于处理副作用,适用于需要执行数据获取、订阅、DOM 操作等操作的场景。
两者都通过依赖数组来管理何时重新执行相关逻辑,但 useMemo 返回计算结果,而 useEffect 不返回值。
7、useRef
用于创建对 DOM 元素或值的引用,可以在渲染之间保持状态。
使用 useRef 可以持久化存储一个可变值,这个值在组件的整个生命周期内保持不变,而不会导致组件重新渲染。useRef 通常用于访问 DOM 元素或存储组件内部的可变状态。
import React, { forwardRef } from 'react';
const ChildComponent = forwardRef((props, ref) => {
return <input type="text" ref={ref} />;
});
import React, { useRef } from 'react';
import ChildComponent from './ChildComponent';
const ParentComponent = () => {
const inputRef = useRef(null);
const focusInput = () => {
if (inputRef.current) {
inputRef.current.focus();
}
};
return (
<div>
<ChildComponent ref={inputRef} />
<button onClick={focusInput}>Focus Input</button>
</div>
);
};
解释:
- 在 ParentComponent 中,使用 useRef 创建一个 ref(inputRef)。
- 将 inputRef 传递给 ChildComponent 的 ref 属性。
- 在 focusInput 函数中,通过 inputRef.current 访问
input元素,并调用其 focus() 方法。
如果你希望在父组件中直接访问 ChildComponent 内部的input元素的 ref,你需要使用 React.forwardRef 来包裹你的子组件。这是因为函数组件默认情况下不支持接收 ref 属性,而 forwardRef 可以让你在函数组件中接收并处理 ref。
使用场景:
- DOM 操作:访问和操作 DOM 元素,如获取焦点、测量尺寸等。
- 持有可变状态:持有组件内的某个可变状态,但不需要该状态触发组件重渲染。
- 避免不必要的重渲染:在某些情况下,使用
useRef可以避免不必要的组件重渲染,提高性能。
8、useImperativeHandle
用于使用 ref 时暴露 DOM 元素的方法。
通常在以下场景中使用:
- 你想控制或操作一个函数组件内部的某些方法或属性。
- 你需要将一些内部的 DOM 操作暴露给父组件。
import React, { useImperativeHandle, useRef, forwardRef } from 'react';
// 创建一个函数组件,并使用 forwardRef 以便能够接收 ref
const ChildComponent = forwardRef((props, ref) => {
const inputRef = useRef();
// 使用 useImperativeHandle 自定义暴露出来的值
useImperativeHandle(ref, () => ({
focusInput: () => {
inputRef.current.focus();
},
getValue: () => {
return inputRef.current.value;
}
}));
return (
<input ref={inputRef} type="text" />
);
});
const ParentComponent = () => {
const childRef = useRef();
const handleFocusClick = () => {
// 调用子组件中暴露的方法
if (childRef.current) {
childRef.current.focusInput();
}
};
const handleGetValueClick = () => {
if (childRef.current) {
alert(`Input value: ${childRef.current.getValue()}`);
}
};
return (
<div>
<ChildComponent ref={childRef} />
<button onClick={handleFocusClick}>Focus Input</button>
<button onClick={handleGetValueClick}>Get Input Value</button>
</div>
);
};
export default ParentComponent;
子组件中useImperativeHandle 接受三个参数:
- 第一个参数是传递给组件的
ref。 - 第二个参数是一个回调函数,该函数返回一个对象,其中包含你希望暴露给父组件的方法和属性。
- 第三个参数(可选)是依赖项数组,类似于
useEffect中的依赖项数组,用于决定何时更新暴露的值。
父组件:
-
ParentComponent创建了一个ref并将其传递给ChildComponent。 - 通过调用
childRef.current.focusInput()和childRef.current.getValue(),父组件可以访问ChildComponent中暴露的方法。
9、useLayoutEffect
与 useEffect 类似,但它在所有的 DOM 变更之后同步执行。这在需要读取 DOM 布局并同步触发重渲染时非常有用。
import React, { useLayoutEffect, useRef, useState } from 'react';
const MyComponent = () => {
const divRef = useRef(null);
const [size, setSize] = useState({ width: 0, height: 0 });
useLayoutEffect(() => {
// 在此测量 DOM 元素的尺寸
if (divRef.current) {
const { offsetWidth, offsetHeight } = divRef.current;
setSize({ width: offsetWidth, height: offsetHeight });
}
// 可以在这里进行 DOM 操作,比如调整样式
// divRef.current.style.backgroundColor = 'lightblue';
// 清理函数可选
return () => {
// 例如,清除一些事件监听器
// window.removeEventListener('resize', handleResize);
};
}, []); // 空数组表示仅在组件首次渲染后执行
return (
<div>
<div ref={divRef} style={{ width: '100px', height: '50px', backgroundColor: 'lightcoral' }}>
Measure me!
</div>
<p>Width: {size.width}px, Height: {size.height}px</p>
</div>
);
};
export default MyComponent;
解释:
-
divRef用于获取目标 DOM 元素的引用,size状态用于存储元素的尺寸信息。 - 在组件完成渲染并且 DOM 更新后立即执行。
- 通过
divRef.current获取元素的尺寸,并更新size状态。 - 可以选择返回一个清理函数,用于清除副作用,比如事件监听器。
注意事项
- 性能影响:由于
useLayoutEffect会在浏览器进一步呈现之前同步执行,因此它可能会对性能产生一定影响,尤其是在Effect内容复杂或执行时间较长时。 - 避免不必要的使用:如果不需要在绘制前同步更新 DOM,应优先使用
useEffect。 - 服务器端渲染(SSR) :
useLayoutEffect不会在服务器端渲染时执行,因为它依赖于浏览器的 DOM。确保在 SSR 场景下使用它时考虑到这一点。
10、useDebugValue
用于在 React 开发者工具中显示自定义 hook 的标签。
import React, { useState, useDebugValue } from 'react';
// 自定义 Hook 示例
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
// 使用 useDebugValue 提供调试信息
useDebugValue(count);
// 也可以传递一个格式化函数来显示更详细的信息
// useDebugValue(() => `Count: ${count}`);
return { count, increment, decrement };
}
const CounterComponent = () => {
const counter = useCounter(10);
return (
<div>
<p>Count: {counter.count}</p>
<button onClick={counter.increment}>Increment</button>
<button onClick={counter.decrement}>Decrement</button>
</div>
);
};
export default CounterComponent;
使用场景:
- 自定义 Hook:当你创建自定义 Hook 时,希望在 React 开发者工具中显示更有意义的信息。
- 复杂状态:当 Hook 的状态较为复杂,且默认显示的信息不足以帮助调试时。
- 调试性能问题:帮助识别和分析性能问题,通过显示与性能相关的信息。
代码解释
- 自定义 Hook (
useCounter):
- 这是一个简单的计数器 Hook,提供了
count状态以及increment和decrement函数。
-
useDebugValue:
-
useDebugValue(count);:这行代码告诉 React 开发者工具,当查看这个 Hook 的状态时,显示count的值。 - 你也可以传递一个函数给
useDebugValue,用于返回更复杂的调试信息,例如:useDebugValue(() =>Count: ${count});。
- React 开发者工具:
- 当你在 React 开发者工具中检查使用
useCounter的组件时,会看到count的值作为调试信息。 - 这对于理解组件的状态和行为非常有帮助,尤其是在处理复杂状态时。
注意事项
- 仅用于开发:
useDebugValue仅在开发模式下有效,在生产环境中不会有任何影响。 - 不影响组件逻辑:
useDebugValue不会改变组件的行为或状态,仅用于提供调试信息。 - 适当使用:避免用
useDebugValue来替代清晰的代码逻辑或适当的命名,它仅作为一种辅助调试工具。