今天我们来学习一下React
在V16.8.0
,发布的神器React Hooks
。熟悉React
的同学肯定知道我们在编写React
应用会面临以下几个问题
这些问题在React Hooks
出来之后将变的非常渺小,下面我们一起来学习下!
Hooks
是一些可以让我们在函数组件
里使用React state
及生命周期等特性的函数。我们来看个栗子
Hooks
写法
import React, { useState } from 'react';
function Example() {
// 声明一个叫 “count” 的 state 变量。
const [count, setCount] = useState(0);
return (
You clicked {count} times
);
}
class
写法
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
render() {
return (
You clicked {this.state.count} times
);
}
}
我们观察上面代码和我们写Class组件
对比,没有了this.state
而多了useState
。 useState
就是一个Hooks
。通过在函数组件
调用它给来给组件内部添加state
。React
会在重复渲染时保留这个 state
。useState
会返回一对值:当前状态和一个让你更新它的函数,你可以在事件处理函数中或其他一些地方调用这个函数。它类似 class
组件的 this.setState
,但是它不会把新的 state
和旧的 state
进行合并。
我们的state
是可以声明多个变量的,Hooks
同样也支持
function ExampleWithManyStates() {
// 声明多个 state 变量!
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
// ...
}
我们在数组解构时,可以给state
变量取不同的名字。
Hooks
?我们在编写函数组件并意识到需要向其添加一些 state
,以前的做法是必须将其它转化为 class
。现在你可以在现有的函数组件中使用 Hooks
。
useState
执行过程const [count, setCount] = useState(0);
我们在调用useState
时,声明了一个变量叫count
,并赋值0
给它。这与我们在Class
中的this.state = { count: 0 };
是一样的。 而useState
给我们返回了count, setCount
对应的就是我们Class
的this.state.count
和 this.setState
。
Hooks
。不要在循环、条件判断或者子函数中调用。React
的函数组件中调用 Hooks
。不要在其他 JavaScript 函数中调用。(还有一个地方可以调用 Hooks
—— 就是自定义的 Hooks
中,我们稍后会学习到。)那我们怎么监听数据变化呢?这里就要说一下useEffect
了。 useEffect
就是一个 Effect Hooks
,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMount
、componentDidUpdate
和 componentWillUnmount
具有相同的用途,只不过被合并成了一个 API。
ps: 与 componentDidMount
或 componentDidUpdate
不同,使用 useEffect
调度的 effect
不会阻塞浏览器更新屏幕,这让我们的应用看起来响应更快。大多数情况下,effect
不需要同步地执行。在个别情况下,有单独的 useLayoutEffect Hooks
使用,其 API
与 useEffect
相同。
看下面代码-_-
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// 相当于 componentDidMount 和 componentDidUpdate:
useEffect(() => {
// 使用浏览器的 API 更新页面标题
document.title = `You clicked ${count} times`;
});
return (
You clicked {count} times
);
}
当调用 useEffect
时,就是在告诉 React
在完成对 DOM 的更改后运行监听函数。由于监听函数是在组件内声明的,所以它们可以访问到组件的 props
和 state
。默认情况下,React
会在每次渲染后调用副作用函数 —— 包括第一次渲染的时候。
useEffect(() => {
const subscription = props.source.subscribe();
return () => {
// 清除订阅
subscription.unsubscribe();
};
});
非常简单,只需要在useEffect
中返回一个函数即可。这是 effect
可选的清除机制。每个 effect
都可以返回一个清除函数。React
会在组件卸载的时候执行清除操作。
在某些情况下,每次渲染后都执行清理或者执行 effect
可能会导致性能问题。在 class
组件中,我们可以通过在 componentDidUpdate
中添加对 prevProps
或 prevState
的比较逻辑解决:
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}
而在useEffect
的 Hooks API
中, 只要传递数组作为 useEffect
的第二个可选参数即可:
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
上面这个示例中,我们传入 [count]
作为第二个参数。这个参数是什么作用呢?如果 count
的值是 5,而且我们的组件重渲染的时候 count
还是等于 5,React
将对前一次渲染的 [5] 和后一次渲染的 [5] 进行比较。因为数组中的所有元素都是相等的(5 === 5),React
会跳过这个 effect
,这就实现了性能的优化。
当渲染时,如果 count
的值更新成了 6,React
将会把前一次渲染时的数组 [5] 和这次渲染的数组 [6] 中的元素进行对比。这次因为 5 !== 6,React
就会再次调用 effect
。如果数组中有多个元素,即使只有一个元素发生变化,React
也会执行 effect
。
ps: 此方法同样对清除Effect
有用
在前言中我们说到了使用Class
组件的痛点。那我们来看一下怎么使用Hooks
解决上面的问题。
自定义 Hooks
是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hooks
。(必须使用use, 否则无法判定某个函数是否包含对其内部 Hooks
的调用)
import { useState, useEffect } from 'react';
function useCountState(props) {
const [count, setCount] = useState(0);
setCount(count + 1)
return {count, setCount};
}
与 React 组件不同的是,自定义 Hook 不需要具有特殊的标识。我们可以自由的决定它的参数是什么,以及它应该返回什么。换句话说,它就像一个正常的函数。但是它的名字应该始终以 use 开头。
function useCountCom(props) {
const {count, setCount} = useCountState(props.count);
return (
You clicked {count} times
);
}
这里解释一下:
Hooks
不会共享 state
?useCountCom
从 React
的角度来看,我们的组件只是调用了 useState
和 useEffect
。我们可以在一个组件中多次调用 useState
和 useEffect
,它们是完全独立的。const [state, dispatch] = useReducer(reducer, initialArg, init);
useState
的替代方案。它接收一个形如 (state, action) => newState 的 reducer
,并返回当前的 state
以及与其配套的 dispatch
方法。(与Redux类似)
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}
>
);
}
有两种不同初始化 useReducer state
的方式,你可以根据使用场景选择其中的一种。将初始 state
作为第二个参数传入 useReducer
是最简单的方法;
需要将 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}
>
);
}
这么做可以将用于计算 state 的逻辑提取到 reducer 外部,这也为将来对重置 state 的 action 做处理提供了便利
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
返回一个 memoized
回调函数。
把内联回调函数及依赖项数组作为参数传入 useCallback
,它将返回该回调函数的 memoized
版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate
)的子组件时,它将非常有用。
useCallback(fn, deps)
相当于 useMemo(() => fn, deps)
。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
返回一个 memoized
值。
把“创建”函数和依赖项数组作为参数传入 useMemo
,它仅会在某个依赖项改变时才重新计算 memoized
值。这种优化有助于避免在每次渲染时都进行高开销的计算。
记住,传入 useMemo
的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect
的适用范畴,而不是 useMemo
。
如果没有提供依赖项数组,useMemo
在每次渲染时都会计算新的值。
可以把 useMemo
作为性能优化的手段,将来,React
可能会选择“遗忘”以前的一些 memoized
值,并在下次渲染时重新计算它们,比如为离屏组件释放内存。先编写在没有 useMemo
的情况下也可以执行的代码 —— 之后再在你的代码中添加 useMemo
,以达到优化性能的目的。
const refContainer = useRef(initialValue);
useRef
返回一个可变的 ref
对象,其 .current
属性被初始化为传入的参数(initialValue
)。返回的 ref
对象在组件的整个生命周期内保持不变。
一个常见的用例便是命令式地访问子组件:
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus();
};
return (
<>
>
);
}
本质上,useRef
就像是可以在其 .current
属性中保存一个可变值的“盒子”。
ref
这一种访问 DOM
的主要方式。如果你将 ref
对象以 形式传入组件,则无论该节点如何改变,
React
都会将 ref
对象的 .current
属性设置为相应的 DOM
节点。
然而,useRef()
比 ref
属性更有用。它可以很方便地保存任何可变值,其类似于在 class
中使用实例字段的方式。
这是因为它创建的是一个普通 Javascript
对象。而 useRef()
和自建一个 {current: ...}
对象的唯一区别是,useRef
会在每次渲染时返回同一个 ref
对象。
请记住,当 ref
对象内容发生变化时,useRef
并不会通知你。变更 .current
属性不会引发组件重新渲染。如果想要在 React
绑定或解绑 DOM
节点的 ref
时运行某些代码,则需要使用回调 ref
来实现。
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()。
其函数签名与 useEffect
相同,但它会在所有的 DOM
变更之后同步调用 effect
。可以使用它来读取 DOM
布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect
内部的更新计划将被同步刷新。
尽可能使用标准的 useEffect
以避免阻塞视觉更新。
如果你正在将代码从 class
组件迁移到使用 Hook
的函数组件,则需要注意 useLayoutEffect
与 componentDidMount
、componentDidUpdate
的调用阶段是一样的。但是,我们推荐你一开始先用 useEffect
,只有当它出问题的时候再尝试使用 useLayoutEffect
。
如果你使用服务端渲染,请记住,无论 useLayoutEffect
还是 useEffect
都无法在 Javascript
代码加载完成之前执行。这就是为什么在服务端渲染组件中引入 useLayoutEffect
代码时会触发 React
告警。解决这个问题,需要将代码逻辑移至 useEffect
中(如果首次渲染不需要这段逻辑的情况下),或是将该组件延迟到客户端渲染完成后再显示(如果直到 useLayoutEffect
执行之前 HTML
都显示错乱的情况下)。
若要从服务端渲染的 HTML
中排除依赖布局 effect
的组件,可以通过使用 showChild &&
进行条件渲染,并使用 useEffect(() => { setShowChild(true); }, [])
延迟展示组件。这样,在客户端渲染完成之前,UI
就不会像之前那样显示错乱了。
useDebugValue(value)
useDebugValue 可用于在 React 开发者工具中显示自定义 hooks 的标签。
例如,自定义 Hooks:
function useCountState(props) {
const [count, setCount] = useState(0);
// 在开发者工具中的这个 Hook 旁边显示标签
// e.g. "useCountCom: 0"
useDebugValue(count);
return {count, setCount};
}
不推荐向每个自定义 Hooks
添加 debug
值。当它作为共享库的一部分时才最有价值。
在某些情况下,格式化值的显示可能是一项开销很大的操作。除非需要检查 Hooks
,否则没有必要这么做。
因此,useDebugValue
接受一个格式化函数作为可选的第二个参数。该函数只有在 Hooks
被检查时才会被调用。它接受 debug
值作为参数,并且会返回一个格式化的显示值。
例如,一个返回 Date
值的自定义 Hooks
可以通过格式化函数来避免不必要的 toDateString
函数调用