React官方推荐使用钩子(Hooks)而不是高阶组件或者render props来复用状态逻辑。解释说函数组件比类组件更简洁,易于理解和调试,而且能够确保组件之间的渲染逻辑保持同步。
React Hooks在React16.8
版本之后出现的。之前还是使用类组件更方便写,因为之前的函数组件无法维护内部状态。
有哪些Hooks?
下面,我们就来先介绍下React的Hooks有哪些?
- 组件状态处理相关: useState、useReducer、useContext;
- 处理副作用: useEffect、useLayoutEffect;
- 性能优化相关:useMemo、useCallback;
- DOM相关: useRef
- redux相关: useSelector、useDispatch、useStore
- 用户自定义hook或第三方库带的hook等
useState
在函数组件保存数据的主要方法
前面章节有详细介绍了useState的用法——快速跳转
useEffect
类组件通常在生命周期中执行需要的操作,useEffect的作用,是补充函数组件无法正确执行副作用的问题。
React中的副作用,是指在组件生命周期中发生的操作,不直接与组件渲染相关,但可以对组件的状态、DOM或外部资源产生影响。
useEffect接收两个参数:
- 回调函数: 定义需要执行的副作用逻辑。
- 依赖数组: 用于指定在依赖项发生变化时,才重新运行副作用函数。
如果依赖数组为空,useEffect作用等同于类组件的componentDidMount
函数
useEffect还可以提供组件卸载等事项的副作用,这个我们下面会详细介绍。
- 如何在DOM渲染后完成后执行?
示例代码
// 依赖参数为空
useEffect(()=>{
// 执行网络请求
// TODO
// 执行DOM操作
// TODO
})
- 如何对对依赖项进行监听?
示例代码
export default function TestUseEffect(props ){
const [text, setText] = useState("");
useEffect(()=>{
// DOM渲染完成后、依赖项发生变化时执行
console.log('组件渲染 text:', text);
}, [text])
const handleChange = (e)=>{
setText(e.target.value)
}
return (
<div>
<input type="text" value={text} onChange={handleChange}/>
{text}
</div>
);
}
- 如何在组件将要卸载前执行?
useEffect(()=>{
// DOM渲染完成后执行
// TODO
return ()=>{
// 组件将要卸载时执行
// TODO
}
})
注意:如果出现useEffect执行了两次的情况,请注释掉
React.strictMode
标签即可。
useMemo
useMemo
的基本用法:
const memoizedValue = useMemo(()=>computeExpensiveValue(a, b), [a,b]);
// computeExpensiveValue是根据a、b的当前值进行计算的函数。当a或b发生改变时,computeExpensiveValue才会被重新执行。
- 有依赖项
与vue的computed相同,用于缓存计算过的值,当依赖项数组中的值发生变化,useMemo会重新计算并缓存结果,但是在语法上面略有不同。
- 无依赖项
类似useEffect
区别在于:
- useEffect是无返回值,而useEffect是返回缓存的值。
- useMemo会在组件渲染之前执行,会比useEffect先执行。
useCallback
useCallback返回一个记忆化的回调函数,它只在其依赖项改变时才会改变。可以帮助我们避免在每次渲染时都创建新的函数,从而提高性能。
useCallback(fn, deps)
就是useMemo(()=>fn, deps)
的语法糖
useCallback
的基本用法:
const callback = React.useCallback(fn, [dependencies]);
useRef
useRef
是一个React Hook,帮助引用一个不需要渲染的值
基本用法
const ref = useRef(initialValue)
返回值
useRef返回一个只有一个属性的对象:
current
: 初始值为initialValue
,之后可以将其设置为其他的值。
使用ref引用一个值
在组件顶层调用useRef
声明一个或多个ref
在后续的渲染中,useRef
将返回相同的对象。可以改变它的current
属性来存储信息,并在之后读取它。
- 与 useState的区别:
改变ref不会触发重新渲染,ref是存储一些不影响组件视图输出的信息的完美选择。比如一个interval ID
并在以后检索它。
const intervalRef = useRef(null);
function handleStartClick(){
const intervalId = setInterval(()=>{// TODO}, 1000)
intervalRef.current = intervalId;
}
function handleStopClick(){
const intervalId = intervalRef.current;
clearInterval(intervalId)
}
注意事项
在渲染期间读取或写入ref会破坏预期行为。
function MyComponent(){
// 🚩 不要在渲染期间写入 ref
myRef.current = 123;
// ✅ 在Effect中读取和写入 ref
useEffect(()=>{
myRef.current = 123;
})
function handleClick() {
// ✅ 可以在事件处理程序中读取和写入 ref
doSomething(myOtherRef.current);
}
// 🚩 不要在jsx里面读取 ref
return <h1>{myOtherRef.current}</h1>;
}
通过Ref操作DOM
使用ref操作DOM是非常常见的行为。React内置了对它的支持。
import {useRef} from 'react';
function MyComponent(){
const inputRef = useRef(null);
return <input ref={inputRef} />
}
当React创建DOM节点并将其渲染时,React会将DOM节点设置到Ref对象的current属性
。可以借助ref对象访问<input>
的DOM节点
function handleClick(){
inputRef.current.focus();
}
当节点被移除时,React将把current
属性设置为null
;
避免重复创建Ref的内容
React 会保存 ref 初始值,并在后续的渲染中忽略它。
// 不合适
function Video(){
const playerRef = useRef(new VideoPlayer());
}
// 建议
function Video(){
const playerRef = useRef(null);
if(playerRef.current === null){
playerRef.current = new VideoPlayer();
}
}
问题: 无法获取自定义组件的Ref
// 错误的写法
const inputRef = useRef(null);
return <MyInput ref={inputRef} />;
// 警告: Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
// 正确的写法
// 将子组件包装在`forwardRef`里面:
import {forwardRef} from 'react';
const MyInput = forwardRef(({value, onChange}, ref)=>{
return (
<input
value={value}
onChange={onChange}
ref={ref}
)
})
export default MyInput;
最后,父级组件就可以得到它的 ref。
useContext
context类似vue provide的概念,无需将组件传递到每个组件,跨组件级传递变量,实现数据传递共享。
基本用法
- 使用createContext创建一个Context
首先需要创建一个Context对象。这个通常在组件树的顶层完成。
import React from "react";
const ThemeContext = React.createContext(null);
export default function Page() {
return (
<ThemeContext.Provider value="dark">
<Toolbar theme="dark" />
</ThemeContext.Provider>
)
}
function Menu(){
return (
// 中间组件不需要在传递ThemeContext的值
<Button />
)
}
// 使用useContext
function Button(){
const theme = React.useContext(ThemeContext)
return <button>{{theme}}</button>
}
// 使用Consumer也可以的
// function Button() {
// return <ThemeContext.Consumer>
// {
// theme => <button>{theme}</button>
// }
// </ThemeContext.Consumer>
// }
useReducer
reducer类似于状态管理控制器,通过不同的指令对状态做出不同的操作。
参数
reducer
:用于更新state的纯函数,参数为state和action,返回值是更新后的state。initialArg
:用于初始化state的任意值init?
:可选参数,用于计算初始值的函数,如果存在则调用init(initialArg)
, 返回结果作为初始值。
返回值
useReducer
返回由两个值组成的数组
- state: state初始化的值
dispatch函数
,用于更新state并触发组件渲染dispatch函数只有一个参数
action
,代码用户执行的操作,并没有返回值。
基本用法
import { useReducer } from 'react';
function reducer(state, action) {
swtich(action){
case "add":
return state.age + 1;
case "sub":
return state.age - 1;
default:
return state;
}
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
const handleAge = function(){
dispatch("add");
}
return (
<div onClick={handleAge} >修改年龄:{state.age}</div>
)
欢迎关注,免费讨论并解答相关问题