本文是官方文档的总结,主要提取一些重要的概念信息,相比于官方文档,更为简洁,方便记忆。适用于面试前复习,和快速查找相关知识点。刚入门的同学还是从官方文档入手。
Hook 概念:
Hook 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。Hook 不能在 class 组件中使用 。
我们给 Hook 设定的目标是尽早覆盖 class 的所有使用场景。目前暂时还没有对应不常用的 getSnapshotBeforeUpdate
,getDerivedStateFromError
和 componentDidCatch
生命周期的
hook优势:
1、组件是DOM结构复用,hook是为了解决逻辑复用,相比于HOC和render props,避免了嵌套地狱。
2、更好地组织相关代码
3、避免了class 不能很好的压缩,并且会使热重载出现不稳定的情况,以及需要理解this等复杂概念的情况。
Hook使用规则
hook api Hook API 索引 – React
各中内置hook的用法和注意点,看上面hook api即可。下面内容只是为了方便记忆。
内置hook
useState
用于定义变量和相关的更新变量的函数
1: import React, { useState } from 'react';
2:
3: function Example() {
4: const [count, setCount] = useState(初始值);
5:
6: return (
7:
8: You clicked {count} times
9:
12:
13: );
14: }
1、将state按照相关性进行划分,这样有利于独立出自定义hook
function Box() {
// 将state按照相关性进行划分
const [position, setPosition] = useState({ left: 0, top: 0 });
const [size, setSize] = useState({ width: 100, height: 100 });
useEffect(() => {
function handleWindowMouseMove(e) {
setPosition({ left: e.pageX, top: e.pageY });
}
)
}
注意: 多次调用相同的hook返回的是不同的state是独立的。
2、如果 state 很复杂, 用 reducer 来管理它,或使用自定义 Hook。
如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState
。该函数将接收先前的 state,并返回一个更新后的值。下面的计数器组件示例展示了 setState
的两种用法:
function Counter({initialCount}) {
const [count, setCount] = useState(initialCount);
return (
<>
Count: {count}
>
);
}
与 class 组件中的 setState
方法不同,useState
不会自动合并更新对象。你可以用函数式的 setState
结合展开运算符来达到合并更新对象的效果。
setState(prevState => {
// 也可以使用 Object.assign
return {...prevState, ...updatedValues};
});
useReducer
是另一种可选方案,它更适合用于管理包含多个子值的 state 对象。
惰性初始 state
initialState
参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用:
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});
跳过 state 更新
调用 State Hook 的更新函数并传入当前的 state 时,React 将跳过子组件的渲染及 effect 的执行。(React 使用 Object.is 比较算法 来比较 state。)
useEffect
可以让你在函数组件中执行副作用操作
useEffect(() => {
// 相当于 componentDidMount 和 componentDidUpdate:
// 相当于componentWillUnmount
return () => {
// 清除函数 };
}
});
hook都可以在函数组件中重复调用,区分成多个,把相关的代码写在一起,使代码可维护性更好。
function ExampleWithManyStates() {
const [age, setAge] = useState(初始值);
const [name, setName] = useState(初始值);
useEffect(()=>{}))
useEffect(()=>{}))
}
如果组件多次渲染(通常如此),则在执行下一个 effect 之前,上一个 effect 就已被清除,这是为了避免了在 class 组件中因为没有处理更新逻辑而导致常见的 bug。
这有时会导致性能问题,采用传递第二个参数,判断是否需要重新执行
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
如果你要使用此优化方式,请确保数组中包含了所有外部作用域中会随时间变化并且在 effect 中使用的变量,否则你的代码会引用到先前渲染中的旧变量
如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([]
)作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值。
大多数情况下,effect 不需要同步地执行。在个别情况下(例如测量布局),有单独的 useLayoutEffect Hook 供你使用.
这是个比较罕见的使用场景。如果你需要的话,你可以 使用一个可变的 ref 手动存储一个布尔值来表示是首次渲染还是后续渲染,然后在你的 effect 中检查这个标识。(如果你发现自己经常在这么做,你可以为之创建一个自定义 Hook。)
一般来说,不安全。
function Example({ someProp }) {
function doSomething() {
console.log(someProp); }
useEffect(() => {
doSomething();
}, []); // 这样不安全(它调用的 `doSomething` 函数使用了 `someProp`)}
要记住 effect 外部的函数使用了哪些 props 和 state 很难。这也是为什么 通常你会想要在 effect 内部 去声明它所需要的函数。 这样就能容易的看出那个 effect 依赖了组件作用域中的哪些值:
function Example({ someProp }) {
useEffect(() => {
function doSomething() {
console.log(someProp); }
doSomething();
}, [someProp]); // ✅ 安全(我们的 effect 仅用到了 `someProp`)}
如果你指定了一个 依赖列表 作为 useEffect
、useLayoutEffect
、useMemo
、useCallback
或 useImperativeHandle
的最后一个参数,它必须包含回调中的所有值,并参与 React 数据流。这就包括 props、state,以及任何由它们衍生而来的东西。
只有 当函数(以及它所调用的函数)不引用 props、state 以及由它们衍生而来的值时,你才能放心地把它们从依赖列表中省略。
如果出于某些原因你 无法 把一个函数移动到 effect 内部,还有一些其他办法:
useContext
用于跨组件传递数据
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
// 1、创建context
const ThemeContext = React.createContext(themes.light);
function App() {
// 2、Provider 提供数据
return (
);
}
function Toolbar(props) {
return (
);
}
function ThemedButton() {
// 3、useContext获取数据
const theme = useContext(ThemeContext);
return (
);
}
useReducer
const [state, dispatch] = useReducer(reducer, initialArg, init);
useState 的替代方案。它接收一个形如 (state, action) => newState
的 reducer,并返回当前的 state 以及与其配套的 dispatch
方法。 一般用于state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等
// 初始化state
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
>
);
}
惰性初始化
你可以选择惰性地创建初始 state。为此,需要将 init
函数作为 useReducer
的第三个参数传入,这样初始 state 将被设置为 init(initialArg)
function init(initialCount) {
return {count: initialCount};
}
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
case 'reset':
return init(action.payload);
default:
throw new Error();
}
}
function Counter({initialCount}) {
const [state, dispatch] = useReducer(reducer, initialCount, init);
return (
<>
Count: {state.count}
>
);
}
useCallback
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
useCallback(fn, deps)
相当于 useMemo(() => fn, deps) 见下面useMemo
useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
返回一个 memoized 值。
把“创建”函数和依赖项数组作为参数传入 useMemo
,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。
传入 useMemo
的函数会在渲染期间执行,不能在里面执行副作用操作,那是useEffect的适用范畴。
如果没有提供依赖项数组,useMemo
在每次渲染时都会计算新的值。
你可以把 useMemo
作为性能优化的手段,但不要把它当成语义上的保证。将来,React 可能会选择“遗忘”以前的一些 memoized 值,并在下次渲染时重新计算它们,比如为离屏组件释放内存。先编写在没有 useMemo
的情况下也可以执行的代码 —— 之后再在你的代码中添加 useMemo
,以达到优化性能的目的。
useRef
const refContainer = useRef(initialValue);
useRef
返回一个可变的 ref 对象,其 .current
属性被初始化为传入的参数(initialValue
)。返回的 ref 对象在组件的整个生命周期内保持不变。
一个常见的用例便是命令式地访问子组件:
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus();
};
return (
<>
>
);
}
useRef
会在每次渲染时返回同一个 ref 对象,类似于class组件的this,可用于保存变量
function Timer() {
const intervalRef = useRef();
useEffect(() => {
const id = setInterval(() => {
// ...
});
intervalRef.current = id;
return () => {
clearInterval(intervalRef.current);
};
});
// ...
}
请记住,当 ref 对象内容发生变化时,useRef
并不会通知你。变更 .current
属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。
获取 DOM 节点的位置或是大小的基本方式是使用 callback ref。每当 ref 被附加到一个另一个节点,React 就会调用 callback。这里有一个 小 demo:
function MeasureExample() {
const [height, setHeight] = useState(0);
const measuredRef = useCallback(node => { if (node !== null) { setHeight(node.getBoundingClientRect().height); } }, []);
return (
<>
Hello, world
The above header is {Math.round(height)}px tall
>
);
}
useImperativeHandle
useImperativeHandle(ref, createHandle, [deps])
useImperativeHandle
可以让你在使用 ref
时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle
应当与 forwardRef 一起使用:
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return ;
}
FancyInput = forwardRef(FancyInput);
在本例中,渲染
的父组件可以调用 inputRef.current.focus()
。
看此处例子:https://www.csdn.net/tags/NtTaUgwsMDg5OTQtYmxvZwO0O0OO0O0O.html
自定义hook
自定义 Hook 是一个函数,其名称以 “use
” 开头,函数内部可以调用其他的 Hook
import { useState, useEffect } from 'react';
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
hook本质上是函数,他们之间的通讯通过参数进行。
只能在客户端渲染的组件,可以采用下面判断方式
showChild &&
useEffect(() => { setShowChild(true); }, [])
React Redux 从 v7.1.0 开始支持 Hook API 并暴露了 useDispatch
和 useSelector
等 hook。
React Router 从 v5.1 开始支持 hook。
目前,你可以 通过 ref 来手动实现:
function Counter() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count; });
const prevCount = prevCountRef.current;
return Now: {count}, before: {prevCount}
;
}
这或许有一点错综复杂,但你可以把它抽取成一个自定义 Hook:
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count); return Now: {count}, before: {prevCount}
;
}
function usePrevious(value) { const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
注意看这是如何作用于 props, state,或任何其他计算出来的值的。
function Counter() {
const [count, setCount] = useState(0);
const calculation = count + 100;
const prevCalculation = usePrevious(calculation); // ...
考虑到这是一个相对常见的使用场景,很可能在未来 React 会自带一个 usePrevious
Hook。
组件内部的任何函数,包括事件处理函数和 effect,都是从它被创建的那次渲染中被「看到」的。例如,考虑这样的代码:
function Example() {
const [count, setCount] = useState(0);
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}
return (
You clicked {count} times
);
}
如果你先点击「Show alert」然后增加计数器的计数,那这个 alert 会显示在你点击『Show alert』按钮时的 count
变量。这避免了那些因为假设 props 和 state 没有改变的代码引起问题。
如果你刻意地想要从某些异步回调中读取 最新的 state,你可以用 一个 ref 来保存它,修改它,并从中读取。
最后,你看到陈旧的 props 和 state 的另一个可能的原因,是你使用了「依赖数组」优化但没有正确地指定所有的依赖。举个例子,如果一个 effect 指定了 []
作为第二个参数,但在内部读取了 someProp
,它会一直「看到」 someProp
的初始值。解决办法是要么移除依赖数组,要么修正它。 这里介绍了 你该如何处理函数,而这里介绍了关于如何减少 effect 的运行而不必错误的跳过依赖的 一些常见策略。
可以的。参见 条件式的发起 effect。注意,忘记处理更新常会 导致 bug,这也正是我们没有默认使用条件式 effect 的原因。
有时候,你的 effect 可能会使用一些频繁变化的值。你可能会忽略依赖列表中 state,但这通常会引起 Bug:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // 这个 effect 依赖于 `count` state }, 1000);
return () => clearInterval(id);
}, []); // Bug: `count` 没有被指定为依赖
return {count}
;
}
传入空的依赖数组 []
,意味着该 hook 只在组件挂载时运行一次,并非重新渲染时。但如此会有问题,在 setInterval
的回调中,count
的值不会发生变化。因为当 effect 执行时,我们会创建一个闭包,并将 count
的值被保存在该闭包当中,且初值为 0
。每隔一秒,回调就会执行 setCount(0 + 1)
,因此,count
永远不会超过 1。
指定 [count]
作为依赖列表就能修复这个 Bug,但会导致每次改变发生时定时器都被重置。事实上,每个 setInterval
在被清除前(类似于 setTimeout
)都会调用一次。但这并不是我们想要的。要解决这个问题,我们可以使用 setState 的函数式更新形式。它允许我们指定 state 该 如何 改变而不用引用 当前 state:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1); // ✅ 在这不依赖于外部的 `count` 变量 }, 1000);
return () => clearInterval(id);
}, []); // ✅ 我们的 effect 不使用组件作用域中的任何变量
return {count}
;
}
(setCount
函数的身份是被确保稳定的,所以可以放心的省略掉)
此时,setInterval
的回调依旧每秒调用一次,但每次 setCount
内部的回调取到的 count
是最新值(在回调中变量命名为 c
)。
在一些更加复杂的场景中(比如一个 state 依赖于另一个 state),尝试用 useReducer Hook 把 state 更新逻辑移到 effect 之外。这篇文章 提供了一个你该如何做到这一点的案例。 useReducer
的 dispatch
的身份永远是稳定的 —— 即使 reducer 函数是定义在组件内部并且依赖 props。
万不得已的情况下,如果你想要类似 class 中的 this
的功能,你可以 使用一个 ref 来保存一个可变的变量。然后你就可以对它进行读写了。举个例子:
function Example(props) {
// 把最新的 props 保存在一个 ref 中
const latestProps = useRef(props);
useEffect(() => {
latestProps.current = props;
});
useEffect(() => {
function tick() {
// 在任何时候读取最新的 props
console.log(latestProps.current);
}
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []); // 这个 effect 从不会重新执行
}
shouldComponentUpdate
?你可以用 React.memo
包裹一个组件来对它的 props 进行浅比较:
const Button = React.memo((props) => {
// 你的组件
});
这不是一个 Hook 因为它的写法和 Hook 不同。React.memo
等效于 PureComponent
,但它只比较 props。(你也可以通过第二个参数指定一个自定义的比较函数来比较新旧 props。如果函数返回 true,就会跳过更新。)
React.memo
不比较 state,因为没有单一的 state 对象可供比较。但你也可以让子节点变为纯组件,或者 用 useMemo 优化每一个具体的子节点。
useMemo Hook 允许你通过「记住」上一次计算结果的方式在多次渲染的之间缓存计算结果:
useMemo
也允许你跳过一次子节点的昂贵的重新渲染:
function Parent({ a, b }) {
// Only re-rendered if `a` changes:
const child1 = useMemo(() => , [a]);
// Only re-rendered if `b` changes:
const child2 = useMemo(() => , [b]);
return (
<>
{child1}
{child2}
>
)
}
注意这种方式在循环中是无效的,因为 Hook 调用 不能 被放在循环中。但你可以为列表项抽取一个单独的组件,并在其中调用 useMemo
。
如果依赖数组的值相同,useMemo
允许你 记住一次昂贵的计算。但是,这仅作为一种提示,并不 保证 计算不会重新运行。但有时候需要确保一个对象仅被创建一次。
第一个常见的使用场景是当创建初始 state 很昂贵时:
function Table(props) {
// ⚠️ createRows() 每次渲染都会被调用
const [rows, setRows] = useState(createRows(props.count));
// ...
}
为避免重新创建被忽略的初始 state,我们可以传一个 函数 给 useState
:
function Table(props) {
// ✅ createRows() 只会被调用一次
const [rows, setRows] = useState(() => createRows(props.count));
// ...
}
React 只会在首次渲染时调用这个函数。参见 useState API 参考。
你或许也会偶尔想要避免重新创建 useRef()
的初始值。举个例子,或许你想确保某些命令式的 class 实例只被创建一次:
function Image(props) {
// ⚠️ IntersectionObserver 在每次渲染都会被创建
const ref = useRef(new IntersectionObserver(onIntersect));
// ...
}
useRef
不会 像 useState
那样接受一个特殊的函数重载。相反,你可以编写你自己的函数来创建并将其设为惰性的:
function Image(props) {
const ref = useRef(null);
// ✅ IntersectionObserver 只会被惰性创建一次
function getObserver() {
if (ref.current === null) {
ref.current = new IntersectionObserver(onIntersect);
}
return ref.current;
}
// 当你需要时,调用 getObserver()
// ...
}
这避免了我们在一个对象被首次真正需要之前就创建它。
不会。在现代浏览器中,闭包和类的原始性能只有在极端场景下才会有明显的差别。
除此之外,可以认为 Hook 的设计在某些方面更加高效:
传统上认为,在 React 中使用内联函数对性能的影响,与每次渲染都传递新的回调会如何破坏子组件的 shouldComponentUpdate
优化有关。Hook 从三个方面解决了这个问题。
useCallback Hook 允许你在重新渲染之间保持对相同的回调引用以使得 shouldComponentUpdate
继续工作:
// 除非 `a` 或 `b` 改变,否则不会变
const memoizedCallback = useCallback(() => { doSomething(a, b);
}, [a, b]);
我们已经发现大部分人并不喜欢在组件树的每一层手动传递回调。尽管这种写法更明确,但这给人感觉像错综复杂的管道工程一样麻烦。
在大型的组件树中,我们推荐的替代方案是通过 context 用 useReducer 往下传一个 dispatch
函数:
const TodosDispatch = React.createContext(null);
function TodosApp() {
// 提示:`dispatch` 不会在重新渲染之间变化 const [todos, dispatch] = useReducer(todosReducer);
return (
);
}
TodosApp
内部组件树里的任何子节点都可以使用 dispatch
函数来向上传递 actions 到 TodosApp
:
function DeepChild(props) {
// 如果我们想要执行一个 action,我们可以从 context 中获取 dispatch。 const dispatch = useContext(TodosDispatch);
function handleClick() {
dispatch({ type: 'add', text: 'hello' });
}
return (
);
}
useDeferredValue
const deferredValue = useDeferredValue(value);
useDeferredValue接受一个值并返回该值的新副本,该副本将延迟到更紧急的更新完成后更新。如果当前渲染是紧急的更新(如用户输入),React将返回以前的值,然后在紧急渲染完成后渲染新值。
这个hook类似于节流和防抖,用于延迟更新. 使用useDeferredValue的好处是,其他工作完成后,React将立即处理更新(而不是等待任意时间), 和 startTransition 一样, 延迟值可以暂停触发 fallback.
延迟子组件
useDeferredValue 只能验证值. 如果你想在一个紧急更新中延迟子组件, 你需要在组件中使用 React.memo 或 React.useMemo:
function Typeahead() {
const query = useSearchQuery('');
const deferredQuery = useDeferredValue(query);
// Memoizing tells React to only re-render when deferredQuery changes,
// not when query changes.
const suggestions = useMemo(() =>
,
[deferredQuery]
);
return (
<>
{suggestions}
>
);
}
记忆子组件告诉react仅需在 deferredQuery 改变的时候重新渲染,而不是 query 改变的时候. 这个模式不是 useDeferredValue 特有的, 你在使用节流防抖之类的钩子时,也有相同的模式.
另附函数组件类型校验、默认值参考:
React:Props类型校验&默认值_前端卡卡西呀的博客-CSDN博客_react的props设置默认值
hook 获取更新后的值,参考
自定义hooks实现在useState改变值之后立刻获取到最新的值