React hooks备忘录
前言
废话不多说,本文主要是记录日常经常使用的hooks。
hooks的使用规则
- 只能在函数式组件中使用。
- 只能在最顶层使用hooks,无法在判断、循环等等中使用。
- 只能在自定义的hooks中使用其他hooks。
useState
useState可以创建一个状态数据。
什么是状态数据?
视图是由状态组成的,但是并不是所有的数据改变都会触发重新渲染完成驱动视图更新,普通的数据不会触发更新,只有状态数据才会触发更新。在react中通过useState
创建状态数据。
const [num, setNum] = useState(0);
const handleClick = () => {
setNum(num + 1)
}
num
就是状态数据,在handleClick
事件中通过setNum
可以更新num数据,从而重新更新页面。
useState中不仅可以传入一个普通的数据,而且还可以传入一个函数。
const [numList, setNumList] = useState(() => {
const a = [1,2]
const b = [3,4]
return [...a,...b]
});
那么什么时候需要传入函数呢?当useState创建出的状态数据的初始值需要经过复杂计算时传入一个函数。(不常用)
num
是状态数据这个没啥好说的,setNum
可以改变状态数据,不仅可以传入一个新的值,而且还可以传入一个函数,这个函数的参数是上一次的state,而且这个函数要返回一个值,目的是作为state的新值。
const handleClick = () => {
setNum((preNum) => preNum + 1)
}
useEffect
useEffect在react组件中可以用来处理副作用,它会在dom渲染完成后执行。一般在useEffect中可以用来发请求获取服务端数据,操作dom。
useEffect(() => {
// 获取服务器端数据
const getRemoteData = async () => {
const data = await fetch('/json')
console.log(data);
}
getRemoteData()
}, [])
useEffect接受的第二个参数被称为依赖,上面的例子是一个空数组。
useEffect第二个参数有下面这几种情况
-
空数组
useEffect(() => { console.log('执行useEffect,只执行一次'); }, [])
只在组件渲染完成后执行一次。
-
不传
useEffect(() => { console.log('执行useEffect,每次渲染,都会执行'); })
在每次组件渲染后都会执行。
-
传递一个或多个依赖
useEffect(() => { console.log('执行useEffect,根据条件执行'); }, [num, count])
第一次组件渲染完成后执行,而且当数组中的值发生变化时,也会执行。
在useEffect接收的回调中,可以返回一个函数,该函数会在组件卸载或者依赖发生变化时执行,当依赖发生变化时,会先执行返回的清理函数,然后再重新执行回调函数。
useEffect(() => {
console.log('执行 effect')
const timer = setInterval(() => {
console.log(num);
}, 1000);
return () => {
console.log('清理 定时器 or其他')
clearInterval(timer);
}
}, [num]);
清理函数可以用来清理定时器,销毁实例,移除事件监听或者订阅。
useReducer
useReducer和useState有一样的作用,都是用来管理状态数据,但是useReducer更适合管理复杂逻辑的状态。
useReducer接收两个参数,第一个是reducer函数,用来处理逻辑,另一个是initialState表示初始数据,返回两个参数,第一个是状态数据,另外一个是改变状态数据的函数。
const [result, dispatch] = useReducer(reducer, { num: 0 });
dispatch
函数会派发一个action到reducer函数中,reducer函数是一个纯函数,接收一个状态和一个action,dispatch的时候只需要传入action即可,state会自动获取。
interface Data {
num: number;
}
interface Action {
type: "INCREMENT" | "DECREMENT";
payload: number;
}
function App() {
function reducer(state: Data, action: Action) {
switch (action.type) {
case "INCREMENT":
return {
num: state.num + action.payload,
};
case "DECREMENT":
return {
num: state.num - action.payload,
};
}
return state;
}
const [result, dispatch] = useReducer(reducer, { num: 0 });
return (
<div>
<button onClick={() => dispatch({ type: "INCREMENT", payload: 2 })}>
{result.num}
</button>
</div>
);
}
useReducer还有另外一种写法:
const [result, dispatch] = useReducer<Reducer<Data, Action>, string>(
reducer,
"zero",
(param) => {
return {
num: param === "zero" ? 0 : 1,
};
},
);
useReducer可以传入三个参数,传入的第二个参数就是第三个函数的参数。第三个参数的返回值就是该useReducer返回的初始数据。
这种写法的灵活性增加了。但是并不常用,一般的足以满足需要了。
useReducer相比较useState而言, 把处理逻辑和数据通过reducer组合,不像useState这么单一,可以满足复杂逻辑的使用场景,但是在一般的业务逻辑下,useState就够用了。
useRef
useRef主要有两个作用
- 在组件中持久性保存数据
- 可以保存DOM引用
持久化保存数据:
React在组件就是一个函数,在组件重新渲染时会重新执行这个函数,如果是普通的变量则会重新赋值,但是如果是useState的变量会在上下文中保存,useRef产生的变量也会被保存,与useState不同的是,用setState修改state的值会重新渲染组件,但是修改useRef的值不会重新保存数据。
所以useRef一般是用来存一些不是用于渲染的内容的,useRef类似于这个组件的全局变量。
useRef获取最新的state的值
const [count, setCount] = useState(0);
const countRef = useRef(0);
const handleIncrement = () => {
setCount(count + 1);
countRef.current++;
console.log("State:", count); // 打印0
console.log("Ref:", countRef.current); // 打印1
};
return (
<div className="tutorial">
Count: {count}
<button onClick={handleIncrement}>Increment</button>
</div>
);
点击Increment
- 执行
setCount(count + 1)
; 更新state,将count的值变为1 countRef.current++;
将countRef.current
的值赋为1- 打印state的值为0, 最新的state需要下次渲染前才能拿到
- 打印
countRef.current
的值为1,这是最新的。 - 页面渲染
Count: 1
, 但是此时打印count的log语句已经执行完了。
保存DOM引用:
import { useEffect, useRef } from "react";
function App() {
const inputRef = useRef<HTMLInputElement>(null);
// 进入页面时input输入框聚焦
useEffect(() => {
inputRef.current?.focus();
},[]);
return (
<div>
<input ref={inputRef}></input>
</div>
);
}
export default App;
在dom上通过ref属性保存dom的引用。可以通过inputRef.current
来获取到dom引用,然后就可以dom上的方法了。比如上面就是调用了input元素的focus属性。
forwardRef
useRef更多的使用场景还是保存DOM引用,但是父组件怎么获取到子组件的dom呢?通过forwardRef。
forwardRef可以在组件之间传递 ref,这个功能就很强了,可以在父组件拿到子组件的dom。
const ChildComp: React.ForwardRefRenderFunction<HTMLInputElement> = (
props,
ref,
) => {
return (
<div>
<input ref={ref}></input>
</div>
);
};
const WrapedChild = React.forwardRef(ChildComp);
function App() {
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
console.log("ref", ref.current);
ref.current?.focus();
}, []);
return (
<div className="App">
<WrapedChild ref={ref} />
</div>
);
}
这是固定写法,给子组件绑定ref,这个子组件通过forwardRef包裹起来。在子组件内部在通过参数的ref绑定到dom上。这个时候父组件就可以通过ref.current
获取到子组件中绑定的dom了。
useImperativeHandle
通过forwardRef可以获取到子组件的dom,但是如果想要获取子组件中的方法或者属性呢?这个时候就要用到useImperativeHandle
, useImperativeHandle
通常与forwardRef
一起配合使用。向父组件自定义暴露属性和方法。(有没有像vue3中的defineExpose)
import { useRef, useState } from "react";
import { useEffect } from "react";
import React from "react";
import { useImperativeHandle } from "react";
interface RefProps {
getFocus: () => void;
increment: (num: number) => void;
}
const Child: React.ForwardRefRenderFunction<RefProps> = (props, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
const [num, setNum] = useState(0);
const increment = (n) => {
setNum(num + n);
};
useImperativeHandle(
ref,
() => {
return {
getFocus() {
inputRef.current?.focus();
},
increment,
};
},
[inputRef, increment],
);
return (
<div>
<input ref={inputRef}></input>
<div> child: {num}</div>
</div>
);
};
const WrapedChild = React.forwardRef(Child);
function App() {
const ref = useRef<RefProps>(null);
useEffect(() => {
console.log("ref", ref.current);
ref.current?.getFocus();
}, []);
return (
<div className="App">
<WrapedChild ref={ref} />
<button onClick={() => ref.current?.increment(2)}>
调用子组件的方法
</button>
</div>
);
}
export default App;
而且在上面的例子中,把inputRef
放在了子组件内部,这样父组件中不用定义ref
了。完成了解藕。
useContext
父子组件之间传入数据一般使用props,但是如果多层级组件或者任意层级组件之间传递用props就太麻烦了,这个时候可以使用Context
上下文进行传递数据。
使用Context
一般分为3步
-
创建上下文
通过createContext创建上下文对象和并提供默认值。
const countContext = createContext({ num: 10, });
-
使用上下文对象的Provider
在应用的顶层使用Provider
function App() { return ( <div> <countContext.Provider value={{ num: 20 }}> <Bbb></Bbb> </countContext.Provider> </div> ); }
-
使用useContext
useContext的作用就是在函数组件中获取上下文对象提供的数据。
const count = useContext(countContext); // { num: 20 }
完整示例
import { createContext, useContext } from "react";
const countContext = createContext({ num: 1 });
function App() {
return (
<div>
<countContext.Provider value={{ num: 20 }}>
<Bbb></Bbb>
</countContext.Provider>
</div>
);
}
function Bbb() {
return (
<div>
<Ccc></Ccc>
</div>
);
}
function Ccc() {
const count = useContext(countContext);
return <h2>context 的值为:{count.num}</h2>
}
export default App;
配置一般用context传递
Context对象也可以嵌套使用, 下面countContext与msgContext进行嵌套
import { createContext, useContext } from "react";
const countContext = createContext({ num: 1 });
const msgContext = createContext({ msg: "" });
function App() {
return (
<div>
<countContext.Provider value={{ num: 20 }}>
<msgContext.Provider value={{ msg: "test" }}>
<Bbb></Bbb>
</msgContext.Provider>
</countContext.Provider>
</div>
);
}
function Bbb() {
return (
<div>
<Ccc></Ccc>
</div>
);
}
function Ccc() {
const count = useContext(countContext);
const msgData = useContext(msgContext);
return (
<>
<h2>context 的值为:{count.num}</h2>
<h2>msg 的值为:{msgData.msg}</h2>
</>
);
}
export default App;
memo
父组件状态数据发生变化时,会重新渲染子组件,但是如果父组件状态的变化对子组件没有影响,那么子组件还继续渲染,那么就会造成性能上的浪费。
react推出了memo api用来包裹函数式组件,对传入子组件的props进行Object.is
算法比较,如果props变了,就会重新渲染子组件,如果没变则不会重新渲染。
import { useState, memo } from "react";
function ChildComponent(props) {
console.log("ChildComponent 渲染" + props.msg);
return <p>{props.msg}</p>;
}
const MemoizedChildComponent = memo(ChildComponent);
function ParentComponent() {
const [count, setCount] = useState(0);
return (
<div>
<p>Current count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<ChildComponent msg="非memo组件" />
<MemoizedChildComponent msg="memo组件" />
</div>
);
}
export default ParentComponent;
点击increment按钮,状态数据count改变,ParentComponent
组件重新渲染, 这个时候ChildComponent
会重新渲染,但是MemoizedChildComponent
组件不会重新渲染,因为组件被memo包裹了 。
但是如果给MemoizedChildComponent
组件传入count,那么当count发生改变时,也会重新渲染这个组件。
<MemoizedChildComponent msg="memo组件" count={count} />
memo 是用来防止 props 没变的时候造成的重新渲染
useMemo
useMemo也是用来做性能优化的,它的作用是在组件重新渲染时,可以缓存该组件中的属性的值。可以避免昂贵的计算。
import { useState, memo, useEffect, useMemo } from "react";
function App() {
// 复杂的计算
const computedValue = () => {
console.log("jisuan....");
const arr = [];
for (let i = 0; i < 10000000; i++) {
arr.push(i);
}
return arr;
};
const startTime = Date.now();
useEffect(() => {
const endTime = Date.now();
console.log(endTime - startTime);
});
const value = computedValue();
const [state, setState] = useState(0);
return (
<div>
<div>
<button onClick={() => setState(Math.random())}>{state}</button>
</div>
</div>
);
}
export default App;
多次点击按钮在控制台打印的结果是:

如果把 const value = computedValue();
这个耗时计算使用useMemo进行包裹之后。
const value = useMemo(() => computedValue(), []);
打印的结果是:

除了第一次慢点之外,后面几次都是几毫秒,说明使用了缓存结果,这样就避免了每次重新渲染的时候都会执行类似的昂贵的计算。
使用useMemo缓存属性还可以避免子组件的重新渲染。
当在组件中需要定义一个普通的对象数据,这个普通的对象数据需要传递给子组件,父组件重新渲染, 子组件的props没有改变,而且子组件使用了memo进行包裹,子组件仍然会重新渲染,这是定义的普通数据每次渲染都会生成新的对象。
import { useState, memo, useEffect, useMemo } from "react";
function Child(props) {
console.log("组件重新渲染");
return <div>{props.info.a}</div>;
}
const MemoChild = memo(Child);
function App() {
const info = { a: 1, b: 2 };
const [state, setState] = useState(0);
return (
<div>
<MemoChild info={info}></MemoChild>
<button onClick={() => setState(state + 1)}>{state}</button>
</div>
);
}
export default App;
点击按钮,发现每次子组件都会重新渲染。

将info使用useMemo包裹之后
const info = useMemo(() => ({ a: 1, b: 2 }), []);
不会造成重新渲染了。

useMemo有点像vue中的computed。
useMemo的第二个参数就是依赖,当依赖发生改变的时候(也是Object.is
算法比较),就会重新执行useMemo的回调函数,产生新的值。
useCallback
useCallback的用法和useMemo是一样的,而且useCallback和useMemo一样也是做性能优化的,不同的是useCallback缓存的是函数,useMemo缓存的是值。还记得上面例子定义的info变量吗?我们使用useMemo成功缓存了info的值,避免了子组件的重新渲染,但是如果info变量是一个函数呢?
import { useState, memo, useEffect, useMemo } from "react";
function Child(props) {
console.log("组件重新渲染");
return <div>111</div>;
}
const MemoChild = memo(Child);
function App() {
const info = () => {
console.log("info函数");
};
const [state, setState] = useState(0);
return (
<div>
<MemoChild info={info}></MemoChild>
<button onClick={() => setState(state + 1)}>{state}</button>
</div>
);
}
export default App;
按钮点击,子组件还是会重新渲染。
将函数使用useCallback缓存起来。
const info = useCallback(() => {
console.log("info函数");
}, []);
按钮点击,子组件就不会重新渲染了。

useMemo和useCallback都是用来做性能优化的,但是如果为了避免子组件重新渲染的性能优化,那么一定要和memo配合。
useCallback的第二个参数也是一个依赖数组,Object.is
算法比较数组中值的变化,如果发生改变那么就会重新执行,产生一个新的函数。
useLayoutEffect
useLayoutEffect
的功能与useEffect功能一样,但是这个hooks不太常用,通常用来解决useEffect会造成页面闪烁问题,这与浏览器绘制有关。为了简单的理解它,看一个例子:
import { useState, useEffect } from "react";
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
if (count === 0) {
const randomNum = 1 + Math.random() * 1000;
setCount(randomNum);
}
}, [count]);
return <button onClick={() => setCount(0)}>{count}</button>;
}
export default App;
- 启动项目
- 连续点击button按钮
- 发现页面闪烁
为什么会出现页面闪烁呢?
useEffect会在页面渲染完成后执行。
- 点击按钮后,setCount(0)会触发页面重新渲染,将页面上的数字改为0
- 渲染完成后,执行useEffect方法, 将页面上的数字改为一个随机数,页面重新渲染
- 两次页面渲染的间隔太短,但是还是需要一定时间去绘制的,所以就会出现页面闪烁的情况。
使用useLayoutEffect
来解决,这个hooks会在浏览器重新绘制屏幕之前触发,也就是会先执行useLayoutEffect然后在去重新渲染页面。
useLayoutEffect(() => {
if (count === 0) {
const randomNum = 1 + Math.random() * 1000;
setCount(randomNum);
}
}, [count]);
- 点击按钮后,setCount(0)将count的值改为0,但是不会重新渲染页面,这是会先等待useLayoutEffect执行
- 执行useLayoutEffect,将count的值改为一个随机数。
- 页面重新渲染
页面不会闪烁了。
因为页面渲染会等待useLayoutEffect的执行,所以可能会降低页面渲染的性能。谨慎使用
那么什么时候会使用呢?
当出现页面渲染发生闪烁的场景时,尝试使用useLayoutEffect是否能够解决。
比如当A元素需要依赖X元素的位置信息时,X元素是时常变动的,如果使用useEffect,会造成A元素的闪烁,这个时候使用useLayoutEffect计算X元素的位置信息后,在显示A元素。
总结:
- useState:可以定义和改变状态数据,返回两个值,第一个是当前state 的值,第二个是一个改变state值的函数,通过setState修改state的值会重新渲染该组件。
- useEffect:执行副作用(side effect),如:发送请求、记录日志、定时器等。有两个参数,第一个是想在useEffect中执行什么代码逻辑,返回一个函数,返回的函数会在会在组件卸载或者依赖发生变化时执行,用来清理副作用; 第二个参数依赖,当依赖发生变化时会重新执行useEffect。
- useLayoutEffect:与
useEffect
是一样的使用方式,useLayoutEffect会阻塞页面渲染,但是能解决页面闪烁问题,它可能会造成性能损耗,用的比较少。 - useReducer:useState功能上一样,但是可以用来处理更复杂的逻辑。有两个参数,第一个是reducer,第二个为初始值。返回两个值,state 的值和
dispatch
方法。dispatch方法会触发reducer函数,reducer中改变数据重新触发页面渲染。 - useCallback:性能优化的hooks,用来缓存函数,第一个参数是要缓存的函数,第二个参数是一个依赖数组依赖数变了,会重新执行,产生一个新的函数,如果useCallback缓存的函数要传入子组件,为了防止子组件重新渲染,需要memo进行配合。
- useMemo:性能优化的hooks,用来缓存值,第一个参数是要缓存的值,第二个参数是一个依赖数组依赖数变了,会重新执行,产生一个新的值,如果useMemo缓存的函数要传入子组件,为了防止子组件重新渲染,需要memo进行配合。
- useRef:可以把useRef看成组件的全局变量, 可以储存不需要渲染但是需要保存的值。useRef也可以保存DOM引用,与ref关键字配合使用。
- useImperativeHandle:
useImperativeHandle
通常与forwardRef
一起配合使用。子组件向父组件自定义暴露属性和方法。父组件中可以调用。 - useContext: 父子组件之间传入数据一般使用props,多层级组件或者任意层级组件传递数据用
Context
上下文进行传递数据。createContext
创建上下文,useContext
使用上下文数据。
转载自:https://juejin.cn/post/7386969940940767266