一、数据更新驱动
1. useState
1. 基础介绍
useState主要用于声明和操作状态变量,可以使函数组件像类组件一样拥有state
js
const [state, setState] = useState(initialState);
state:状态,作为渲染视图的数据源
setState:改变state的函数。可以直接传递新状态,也可以传递一个根据先前状态来计算新状态的函数(函数式更新)
initialState:初始化值。如果是函数,则将函数的返回值作为初始值
2. 直接传递新状态
点击按钮,count变为1
js
import { useState } from "react";
function App() {
const [count, setCount] = useState(0);
return (
<div>
{count}
<button
onClick={() => {
setCount(1);
}}
>
按鈕
</button>
</div>
);
}
export default App;
3. 函数式更新
根据先前的state更新state。将按钮中的setCount调用方式改为:
js
setCount((prev) => prev + 1);
4. 使用场景:使用key重置状态
会有这么一种业务场景:在一系列筛选或输入后面,增加重置按钮
如果是受控组件,我们可以将值置空。如果是非受控组件,我们可以使用key重置组件的状态
js
import { useState } from "react";
export default function App() {
const [version, setVersion] = useState(0);
const handleReset = () => {
setVersion(version + 1);
};
return (
<>
<button onClick={handleReset}>重置</button>
<Form key={version} />
</>
);
}
const Form = () => {
const hanlderSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log(e.target);
};
return (
<>
<form onSubmit={hanlderSubmit}>
<input type="text" name="user" />
<input type="password" name="password" />
<button type="submit">提交</button>
</form>
</>
);
};
在表单中输入信息,然后点击重置按钮,会发现输入框内容被情况。这是因为当 key 改变时,React 会从头开始重新创建 Form 组件(以及它的所有子组件)。
5. 注意事项
(1)set函数是异步的,调用set函数后,不能立即获取最新的值
js
const handleClick = () => {
setCount(count + 1); //setCount(0+1)
console.log(count); //0
};
(2)获取的是渲染时候的值
即使2s后打印,但当时读取count的时候,count值为0,因此打印出来的结果也为0。可以理解为渲染快照
js
const handleClick = () => {
setCount(count + 1); //setCount(0+1)
setTimeout(() => {
console.log(count); // 还是 0!
}, 2000);
};
如果要获取最新的值,可以使用useRef
js
import { useRef} from "react";
export default function App() {
const countRef = useRef(0)
const handleClick = () => {
countRef.current += 1
setTimeout(() => {
console.log( countRef.current); // 1
}, 2000);
};
return (
<>
<button onClick={handleClick}>按钮</button>
</>
);
}
(3)如果新值与当前state相同(由Object.is比较确定),将跳过重新渲染
点击按钮,因为对象info的引用地址还是指向同一个,因此不会再重新渲染
js
import { useState } from "react";
export default function App() {
const [info, setInfo] = useState({
name: "张三",
age: 20,
});
const handleClick = () => {
setInfo(Object.assign(info, { age: info.age + 1 }));
};
console.log("渲染");
return (
<>
{info.name}--{info.age}
<button onClick={handleClick}>按钮</button>
</>
);
}
要想触发渲染,需传递一个新对象:
js
setInfo(Object.assign({}, info, { age: info.age + 1 }));
//或使用扩展运算符
setInfo({ ...info, age: info.age + 1 });
(4)setState 自动批量处理
React 18 之前,setState 只在合成事件与钩子函数中自动批量处理,在promise、setTimeout或js原生事件中,都不会进行批处理
React 18中,默认所有的更新都将自动批量处理
js
import { useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
const handleClick = () => {
setTimeout(() => {
setCount(1);
setCount(2);
setCount(3);
setCount(2);
});
};
console.log("渲染");
return (
<>
{count}
<button onClick={handleClick}>按钮</button>
</>
);
}
点击按钮,组件只会更新一次,并且值为最后一次调用set传入的值2
2. useReducer
1. 基础介绍
useReducer 是 react-hooks 提供的能够在无状态组件中运行类似redux功能的api
js
const [state,dispatch] = useReducer(reducer,initialState,init?);
state:状态state
dispatch:改变state的函数
reducer:与 redux 中的 reducer相同,一个函数,接收state与action,并返回一个新的state
initialState:初始值
init:将init函数作为useReducer的第三个参数传入,这样初始state将被设置为init(initialState)
2. 使用场景:状态管理
js
import { useReducer } from "react";
const initialCount = {
count: 0,
};
type InitialCount = typeof initialCount;
type ACTIONTYPE =
| { type: "increment" }
| { type: "decrement" }
| { type: "reset"; payload: InitialCount };
/* 对初始值进行处理 */
function init(initialCount: InitialCount) {
/* 如果传入的count初始值小于0,则置为0 */
if (initialCount.count < 0) {
return { count: 0 };
} else {
return initialCount;
}
}
function reducer(state: InitialCount, action: ACTIONTYPE) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
case "reset":
return init(action.payload || initialCount);
default:
throw new Error();
}
}
const App = () => {
const [state, dispatch] = useReducer(reducer, initialCount, init);
return (
<>
Count: {state.count}
<button
onClick={() => dispatch({ type: "reset", payload: initialCount })}
>
Reset
</button>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
</>
);
};
export default App;
useReducer 与 Context 配合使用,可以形成一个小范围的状态管理功能
3. useSyncExternalStore
1. 基础介绍
useSyncExternalStore 可以在外部数据源变化时,自动更新视图。一般是第三方状态管理库使用。
js
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
subscribe:订阅 store 的变化
getSnapshot:返回 store 当前值
getServerSnapshot:用于服务端渲染
2. 使用场景:订阅浏览器 API
订阅外部数据源,当外部数据源更新时,自动更新视图
js
import { useSyncExternalStore } from "react";
export default function ChatIndicator() {
//监听浏览器网络连接状态
const isOnline = useSyncExternalStore(subscribe, () => navigator.onLine);
return <h1>{isOnline ? "Online" : "Disconnected"}</h1>;
}
function subscribe(callback:any) {
window.addEventListener("online", callback);
window.addEventListener("offline", callback);
return () => {
window.removeEventListener("online", callback);
window.removeEventListener("offline", callback);
};
}
4. useTransition
useTransition 是一个帮助你在不阻塞 UI 的情况下更新状态的 React Hook
1. 基础介绍
js
const [ isPending , startTransition ] = useTransition ()
isPending :布尔值,表示是否正在等待;
startTransition:接收一个的函数,可以把里面的更新任务变成过渡任务
2. 使用场景:将状态更新标记为低优先级,先执行其他高优先级任务
页面会先显示list2的内容,之后再显示list1的内容
js
import { useState, useEffect, useTransition } from "react";
const App = () => {
const [list1, setList1] = useState<null[]>([]);
const [list2, setList2] = useState<null[]>([]);
const [isPending, startTransition] = useTransition();
useEffect(() => {
startTransition(() => {
//将状态更新标记为 transition
setList1(new Array(10000).fill(null));
});
}, []);
useEffect(()=>{
setList2(new Array(10000).fill(null));
},[])
return (
<>
{isPending ? "pending" : "nopending"}
{list1.map((_, i) => (
<div key={i}>{i}</div>
))}
-----------------list2
{list2.map((_, i) => (
<div key={i}>6666</div>
))}
</>
);
};
export default App;
注释掉list1相关代码的执行堆栈图:
可以看到layout之后,一直在函数调用,页面会堵塞大约0.3秒
注释掉list2,只看list1相关代码的执行堆栈图:
可以看到我们的任务被拆分到每一帧不同的 task 中,这样浏览器就有剩余时间执行样式布局和样式绘制,减少掉帧的可能性
5. useDeferredValue
1. 基础介绍
可以让我们延迟渲染不紧急的部分,类似于防抖但没有固定的延迟时间
js
const deferrredValue = useDeferredValue(value)
value:想延迟的值
deferrredValue:延迟值。只有当前没有紧急更新任务时,才会更新为最新值,否则返回旧值
2. useDeferredValue 和 useTransition 的区别
相同点:useDeferredValue 本质上和内部实现与 useTransition 一样都是标记成了过渡更新任务
不同点:useTransition是处理了一段逻辑,useDeferredValue是生产一个新的状态
3. 使用场景:受控输入框与长列表
将 input 更新作为紧急的部分优先处理,长列表更新作为不紧急的部分延迟处理。
js
import { useState, useDeferredValue, memo } from "react";
export default function App() {
const [value, setValue] = useState("");
const deferredValue = useDeferredValue(value);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
};
return (
<div>
<input value={value} onChange={handleChange} />
<LongList deferredValue={deferredValue} />
</div>
);
}
const LongList = memo(({ deferredValue }: { deferredValue: string }) => {
return (
<div className="container">
<div className="list">
{Array(10000)
.fill(null)
.map((_,i) => (
<div key={i}>{deferredValue}</div>
))}
</div>
</div>
);
});
注意点:
如果直接在父组件中展示1万个长列表节点,value更新,触发组件渲染,会去处理长列表节点,导致卡顿。
将长列表拆分成子组件,延迟的值传递给子组件,并使用memo包裹,这样只要等deferredValue的值更新,才会重新处理长列表的节点
二、生命周期
1. useEffect
1. 基础介绍
useEffect实现了 componentDidMount、componentDidUpdate 和 componentWillUnmount 三个API的功能
js
useEffect(fn, deps?)
fn:回调函数,会在初始化或依赖项变化时运行
deps:依赖项,是一个数组
2. 实现componentDidMount
第二个参数传空数组,只会在初始化时触发一次
js
useEffect(() => {
//请求接口数据
}, []);
3. 实现componentDidUpdate
在第二个参数传入依赖状态,当依赖状态改变时会重新渲染
js
useEffect(() => {
//请求接口数据
}, [props.name]);
内部是浅比较,源码中用for循环配合Object.is实现
4. 实现componentWillUnmount
return一个回调函数,用来清除副作用
js
import { useCallback, useEffect, useState } from "react";
const App = () => {
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouse = useCallback((e: MouseEvent) => {
setPosition({
x: e.pageX,
y: e.pageY,
});
}, []);
useEffect(() => {
window.addEventListener("mousemove", handleMouse);
return () => {
//取消监听
window.removeEventListener("mousemove", handleMouse);
};
}, [handleMouse]);
return (
<div>
x:{position.x} y:{position.y}
</div>
);
};
export default App;
2. useLayoutEffect
1. 基础介绍
在浏览器layout之后,painting之前执行。回调函数中执行的代码可能会堵塞浏览器绘制。常用来在绘制之前获取DOM节点信息,修改DOM结构,这样浏览器只用绘制一次
js
useLayoutEffect(setup, deps?)
fn:回调函数,会在初始化或依赖项变化时运行
deps:依赖项
2. 使用场景:在浏览器绘制之前获取DOM节点信息
js
import { useState, useRef, useLayoutEffect } from "react";
function App() {
const ref = useRef<HTMLDivElement>(null);
const [tooltipHeight, setTooltipHeight] = useState(0);
useLayoutEffect(() => {
const { height } = ref.current?.getBoundingClientRect() || { height: 0 };
setTooltipHeight(height);
}, []);
return (
<div ref={ref} style={{ height: 300 }}>
容器的高:{tooltipHeight}
</div>
);
}
export default App;
3. useInsertionEffect
1. 基础介绍
useInsertionEffect是一个专为CSS-in-JS 库的开发者打造的钩子,在DOM更新之前执行(比useLayoutEffect早)
js
useInsertionEffect(setup, deps?)
fn:回调函数,会在初始化或依赖项变化时运行
deps:依赖项
2. 使用场景:提前注入style标签
js
import { useInsertionEffect } from "react";
function App() {
useInsertionEffect(() => {
/* 动态创建 style 标签插入到 head 中 */
const style = document.createElement("style");
style.innerHTML = `
.css-in-js{
color: red;
font-size: 20px;
}
`;
document.head.appendChild(style);
}, []);
return <div className="css-in-js"> useInsertionEffect </div>;
}
export default App;
三、状态保存
1. useMemo
1. 基础介绍
在每次重新渲染的时候能够缓存计算的结果
js
const cachedValue = useMemo(calculateValue, deps)
calculateValue:一个函数,函数的返回值作为缓存值
deps:一个数组,存放当前 useMemo 的依赖项。依赖项改变时,会运行calculateValue重新计算
cachedValue:返回值,如果 deps 中有依赖项改变,返回重新执行 calculateValue 产生的值,否则取上一次缓存值
2. 使用场景
(1)缓存计算结果
js
import { useState, useMemo } from "react";
function App() {
const [count, setCount] = useState(0);
const memoizedValue = useMemo(() => {
//创建1000位数组
const list = new Array(1000).fill(null).map((_, i) => i);
//对数组求和
const total = list.reduce((res, cur) => (res += cur), 0);
//返回计算的结果
return count + total;
//添加依赖项,只有count改变时,才会重新计算
}, [count]);
return (
<div>
{memoizedValue}
<button onClick={() => setCount((prev) => prev + 1)}>按钮</button>
</div>
);
}
export default App;
(2)缓存渲染列表
js
import { useState, useMemo } from "react";
function App() {
const [list] = useState(["张三", "李四"]);
const renderList = useMemo(
() => (
<div>
{list.map((i, v) => (
<span key={v}>{i}</span>
))}
</div>
),
[list]
);
return (
<div>
{renderList}
</div>
);
}
export default App;
3. React.memo与useMemo的区别
React.memo:对外部传值props进行浅比较,避免不必要的重复渲染,相当于shouldComponentUpdate;
useMemo:对组件内部状态state进行浅比较,避免不必要的重复渲染
2. useCallback
1. 基础介绍
缓存函数的引用地址,仅在依赖项改变时才会更新
js
const cachedFn = useCallback(fn, deps)
fn:想要缓存的函数
deps:是否更新 fn 的所有响应式值的一个列表
2. 使用场景:避免子组件重复渲染
默认情况下,当一个组件重新渲染时, React 将递归渲染它的所有子组件。我们通常对于有props的子组件会使用React.memo进行包裹
js
import { useState, memo } from "react";
const App = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount((prev) => prev + 1);
};
return (
<div>
{count}
<MyButton handleClick={handleClick} />
</div>
);
};
interface Props {
handleClick: () => void;
}
const MyButton = memo(({ handleClick }: Props) => {
console.log("子组件渲染");
return <button onClick={handleClick}>按钮</button>;
});
export default App;
点击按钮,可以发现即使子组件使用memo包裹了,但还是更新了,控制台打印出"子组件渲染"。这是因为父组件App每次更新时,函数handleClick每次都返回了新的引用地址,因此对于子组件来说每次传入的都是不一样的值,从而触发重渲染。
使用useCallback可以缓存函数的引用地址,将handleClick改为
js
const handleClick = useCallback(()=>{
setCount(prev=>prev+1)
},[])
再点击按钮,会发现子组件不会再重新渲染
3. useMemo与useCallback的区别
useMemo常用来缓存计算结果,useCallback常用来缓存函数的引用地址
useMemo如果返回一个函数,同样能够做到缓存函数的引用地址,与useCallback等效
四、状态获取与传递
1. useContext
1. 基础介绍
向上查找最近的使用context Provider 提供的 value 值
js
const value = useContext(SomeContext)
SomeContext:由React.createContext创建的context
value:获取使用 Provider 提供的 value 值
2. 使用场景:向组件树深层传递数据
js
import {
useState,
useCallback,
createContext,
useMemo,
useContext,
} from "react";
const defaultValue = { count: 0, handleClick: () => {} };
/* 1. 创建Context */
const MyContext = createContext(defaultValue);
const App = () => {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount((prev) => prev + 1);
}, []);
//传递值和改变该值的方法给子组件
const contextValue = useMemo(
() => ({
count,
handleClick,
}),
[count, handleClick]
);
return (
/* 2. 提供Context值 */
<MyContext.Provider value={contextValue}>
<MyButton />
</MyContext.Provider>
);
};
const MyButton = () => {
/* 3. 获取Context值 */
const { count, handleClick } = useContext(MyContext);
return (
<div>
{count}
<button onClick={handleClick}>按钮</button>
</div>
);
};
export default App;
2. useRef
1. 基础介绍
用来创建一个不需要渲染的值
js
const ref = useRef(initialValue)
initialValue:ref 对象的 current 属性的初始值。
ref:一个只有一个属性current的对象,在后续的渲染中,useRef 将返回同一个对象
2. 可以用来访问dom节点或子组件
js
import { useEffect, useRef } from "react";
function App() {
const domRef = useRef(null);
useEffect(() => {
console.log(domRef.current);
}, []);
return <div ref={domRef}>dom</div>;
}
export default App;
3. 值的更改不会触发视图更新
js
import { useRef, useState } from "react";
function App() {
const countRef = useRef(0);
const [count, setCount] = useState(0);
console.log('组件渲染');
return (
<div>
{countRef.current}
{count}
<button
onClick={() => {
countRef.current += 1;
}}
>
ref+1
</button>
<button onClick={() => setCount((prev) => prev + 1)}>state+1</button>
</div>
);
}
export default App;
点击ref+1按钮,countRef值增加,但视图不会更新。点击state+1按钮,视图更新,组件重新渲染,打印出countRef最新的值
4. 返回的引用,在组件更新时不会被改变(返回同一个对象),可以用来清除定时器
js
import { useEffect, useRef } from "react";
function App() {
const timeRef = useRef<NodeJS.Timer>();
useEffect(() => {
timeRef.current = setInterval(() => {
console.log("1");
}, 1000);
//在组件卸载时,清除定时器,防止内存泄漏
return () => {
clearInterval(timeRef.current);
};
}, []);
return <div></div>;
}
export default App;
3. useImperativeHandle
1. 基础介绍
与forwardRef配合,自定义暴露给父组件的实例值或函数
js
useImperativeHandle(ref, createHandle, [deps])
ref:接受forwardRef传递过来的ref
createHandle:处理函数,返回值作为暴露给父组件的ref对象
deps:依赖项deps,依赖项更改形成新的ref对象
2. 使用场景:组件通信中,父组件调用子组件
js
import { forwardRef, useRef, useImperativeHandle } from "react";
/* 定义ref类型 */
interface ForwardObject {
focus: () => void;
}
function App() {
const ref = useRef<ForwardObject>(null);
return (
<div>
<MyInput ref={ref} />
<button onClick={() => ref.current?.focus()}>使输入框获取焦点</button>
</div>
);
}
/* 子组件 */
const MyInput = forwardRef((props, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(
ref,
() => ({
focus: () => {
inputRef.current?.focus();
},
}),
[]
);
return <input ref={inputRef} />;
});
export default App;
五、工具类
1. useDebugValue
1. 基础介绍
在 React 开发工具 中为自定义 Hook 添加标签
js
useDebugValue(value, format?)
value:在 React 开发工具中显示的值
format:一个格式化函数,将接收 value 作为参数,并返回格式化后的显示值
2. 使用场景:在 React 开发工具中为自定义 Hook 添加标签
js
import { useDebugValue, useState } from "react";
const App = () => {
useNetworkStatus();
return <div></div>;
};
function useNetworkStatus() {
const [isOnline] = useState(false);
// 在开发者工具中的这个 Hook 旁边显示标签
// NetworkStatus:"Offline"
useDebugValue(isOnline ? "Online" : "Offline");
return isOnline;
}
export default App;
在开发者工具中查看
2. useID
1. 基础介绍
生成唯一 ID。解决了在服务器渲染中,服务端和客户端产生 id 不一致的问题
js
const id = useId()
2. 使用场景:为属性生成唯一 ID
js
import { useId } from "react";
export default function Form() {
const id = useId();
return (
<div>
<label htmlFor={id}>chose</label>
<input type="checkbox" id={id} name="chose" />
</div>
);
}
点击chose,能够看到复选框被选中
提示:不要使用 useId 来生成列表中的 key,key 应该由你的数据生成
六、结尾
参考文档