本文用于记录自己学习和使用 React Hook 笔记。
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。Hook 在 class 内部是不起作用的。但你可以使用它们来取代 class 。
Hook 可以做什么?
- 在函数组件中使用
state
(useState) - 将组件的状态逻辑进行抽离(自定义 hook)
初始化
使用 create-react-app 初始化支持 TypeScript 语法的工程。
npx create-react-app base-demo --typescript
useState
在 Hook 提出之前,函数组件(无状态组件)通常用于 UI 渲染,通过 props
传递的数据进行展示。如果组件想要有自己的状态 ( state ) 数据的话,只能通过 class 组件来实现了。useState
提供了函数组件中保留自己状态的方法。用法如下:
import React, {useState} from 'react';
const Counter: React.FC = ()=>{
const [counter, setCounter] = useState(0);
return <div>
<span>{counter}</span><button onClick={()=>{setCounter(counter+1)}}>+1</button>
</div>
}
export default Counter;
counter 是定义的变量,setCounter 用来修改 counter 的值的方法。
useEffect
Effect Hook 可以让你在函数组件中执行副作用操作。
如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。
无需清除的 effect
我们希望在组件加载和更新时执行同样的操作。从概念上说,我们希望它在每次 渲染 之后执行 —— 但 React 的 class 组件没有提供这样的方法。即使我们提取出一个方法,我们还是要在两个地方调用它。
import React, { useState, useEffect } from 'react';
const Counter: React.FC = ()=>{
const [counter, setCounter] = useState(0);
const [flag, setFlag] = useState(true);
useEffect(()=>{
document.title = `You clicked ${counter} times`;
console.log("useEffect...")
});
return <div>
<span>{counter}</span>
<button onClick={()=>{setCounter(counter+1)}} style={{marginLeft: "20px"}}>+1</button>
<button onClick={()=>{setFlag(!flag)}} style={{marginLeft: "20px"}}>{flag ? 'ON':'OFF'}</button>
</div>
}
export default Counter;
useEffect
函数在每次 渲染 的时候都会执行,啥意思? 例子中,即使我们修改的是 flag 的值的时候,我们定义的useEffect
函数也会被执行。这不是我们想要的,useEffect
函数中第二个参数可以用来控制执行的时机。
控制 useEffect 执行
useEffect
其实有两个参数:
- 如果第二个参数默认不写的话,
useEffect(()=>{});
那么 useEffect 函数每次页面 渲染 都会执行。 - 如果第二个参数是空数组
useEffect(()=>{}, []);
那么说明 useEffect 函数的执行跟其他变量无关,只会在第一次页面渲染时执行。 - 如果 useEffect 函数的执行跟某个变量相关,变量变化的时候需要相应的一些操作,那么可以将该变量传入数组中。
useEffect(()=>{}, [counter]);
useEffect(()=>{
document.title = `You clicked ${counter} times`;
console.log("useEffect...")
}, [counter]);
这样页面只有 counter 数据变化的时候才会执行 useEffect 函数。
与 componentDidMount 或 componentDidUpdate 不同,使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。
需要清除的 effect
还有一些副作用是需要清除的。例如 document 上绑定的事件,setTimeout 函数等。这种情况下,清除工作是非常重要的,可以防止引起内存泄露!现在让我们来比较一下如何用 Class 和 Hook 来实现。以记录鼠标点击位置为例来说明,在 Class 组件中,我们是这么实现的:
import React from 'react';
class MouseTrigger extends React.Component{
constructor(props){
super(props);
this.state = {
position: {
x: 0,
y: 0
}
}
}
handleMouseMove = (e)=>{
this.setState({
position: {
x: e.clientX,
y: e.clientY
}
})
}
// 添加监听事件
componentDidMount(){
document.addEventListener("click", this.handleMouseMove);
}
// 取消监听事件
componentWillUnmount(){
document.removeEventListener("click", this.handleMouseMove);
}
render(){
return (
<div>
<p>Mouse position is ({this.state.position.x}, {this.state.position.y})</p>
</div>
)
}
}
export default MouseTrigger;
使用 useEffect 方式实现, useEffect 函数返回一个函数,用于清除 “副作用”。
import React, { useState, useEffect} from "react";
const MouseTrigger: React.FC = ()=>{
const [position, setPosition] = useState({x: 0, y:0})
const handleMouseMove = (e: MouseEvent)=>{
setPosition({
x: e.clientX,
y: e.clientY
})
}
useEffect(()=>{
document.addEventListener("click", handleMouseMove);
return ()=>{
document.removeEventListener("click", handleMouseMove);
}
}, [])
return <p>
Mouse position is ({position.x}, {position.y})
</p>
}
export default MouseTrigger;
useEffect(()=>{... return ()=>{}}, [])
中,第二个参数是空数组 [] ,使得 useEffect 函数只在第一次页面加载执行;返回一个函数,会在组件卸载的时候执行,清除掉添加的 “副作用”。
没有对比,就没有伤害。明显使用 useEffect 定义的函数组件逻辑更加清晰简洁,给人一种清清爽爽的感觉。
自定义 Hook
通过自定义 Hook,可以将组件逻辑提取到可重用的函数中。在自定义 Hook 提出之前,我们是怎么实现的呢?常用的方式是 2 种,HOC 和 render props。我们一起看一下用法的有哪些不一样呢
HOC
HOC 是 High Order Component 的缩写,意思是 “高阶组件” 。高阶组件本质是一个函数,接收一个组件作为参数,返回一个新的组件。
业务中,往往会有请求数据的需求。在数据请求时,展示 loadin 动画,请求完成显示对应的数据。这个逻辑就可以进行剥离,如下:
// FetchData.js
import React from 'react';
import axios from 'axios';
const FetchData = (Component, url)=>{
class WithFetchData extends React.Component{
constructor(props){
super(props);
this.state = {
data: [],
isLoading: true
}
}
componentDidMount(){
this.setState({
isLoading: true
});
axios.get(url).then(res=>{
if(res.status===200){
this.setState({
data: res.data,
isLoading: false
})
}
})
}
render(){
const {isLoading, data} = this.state;
return <>
{ isLoading ? <p>data is loading</p>: <Component data={data}/>}
</>
}
}
return WithFetchData;
}
export default FetchData;
FetchData 就是一个高阶组件,本质是一个函数,接收一个组件,然后返回一个新的组件。在需要的组件中使用:
// ShowData.js
import React from 'react';
import FetchData from './FetchData';
const ShowData = (props)=>{
return <>
{props.data.map((item, key)=><p key={key}>{item.name}, {item.price}</p>)}
</>
}
export default FetchData(ShowData, "/api/mockData.json");
HOC 其实是在将组件逻辑提取到了父组件中,包裹在组件外面,这样内部组件就可以复用使用外部定义的数据和逻辑了。
render props
render props 是一种利用父子组件传递的技巧。通过 render 函数将要显示的组件传递给父组件中。
// RenderPropsDemo.js
import React from 'react';
import PropTypes from 'prop-types';
import axios from 'axios';
class RenderPropsDemo extends React.Component{
constructor(props){
super(props);
this.state = {
data: [],
isLoading: true
}
}
componentDidMount(){
this.setState({
isLoading: true
});
const {url} = this.props;
axios.get(url).then(res=>{
if(res.status===200){
this.setState({
data: res.data,
isLoading: false
})
}
})
}
render(){
const {isLoading, data} = this.state;
const {render} = this.props;
return (
<>
{ isLoading ? <p>data is loading</p> : <>{render(data)}</>}
</>
)
}
}
RenderPropsDemo.prototypes = {
render: PropTypes.func.isRequired
}
export default RenderPropsDemo;
使用:
// ShowData.js
import React from 'react';
import RenderPropsDemo from './RenderPropsDemo';
const ShowData = ()=>{
const render = (data)=>{
return data.map((item, key)=><p key={key}>{item.name}, {item.price}</p>)
}
return <>
<RenderPropsDemo render={render} url="/api/mockData.json"/>
</>
}
export default ShowData;
自定义 Hook
通过自定义 Hook,可以将组件逻辑提取到可重用的函数中。相比 HOC 和 render props 的方式,自定义 Hook 是纯净的函数模式,不牵扯父子组件。逻辑更加清晰。
//useFetchData.tsx
import { useState, useEffect } from 'react';
import axios from 'axios';
// 默认只在第一次发起请求
const useFetchData = (url: string, deps: any[] = [])=>{
const [data, setData] = useState<any>([]);
const [loading, setLoading] = useState(false);
useEffect(()=>{
setLoading(true);
axios.get(url).then(res=>{
if(res.status===200){
setData(res.data);
setLoading(false);
}
})
}, deps);
// 返回
return [data, loading];
}
export default useFetchData;
使用:
//FetchData.tsx
import React from 'react';
import useFetchData from './useFetchData';
interface IShowDataResult{
name: string;
price: number;
}
type resultType = Array<IShowDataResult>;
const FetchData: React.FC = ()=>{
// 直接执行 useFetchData 获取
const [data, loading] = useFetchData("/api/mockData.json");
return <>
{
loading ? <p>data is loading</p> :
<>
{data.map((item:IShowDataResult, key: number)=><p key={key}>{item.name}</p>)}
</>
}
</>
}
export default FetchData;
自定义 Hook 实现的效果和 Vue 3.0 中的 Composition API 思想是相通的。就是将组件中重复的逻辑剥离出来,以更简洁清晰的组织方式去管理代码。
自定义 Hook 使用注意点:
- 自定义 Hook 必须以 “use” 开头。 这个约定非常重要。不遵循的话,由于无法判断某个函数是否包含对其内部 Hook 的调用,React 将无法自动检查你的 Hook 是否违反了 Hook 的规则。
- 两个组件中使用相同的 Hook 不会共享 state。自定义 Hook 是一种重用状态逻辑的机制(例如设置为订阅并存储当前值),所以每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全隔离的。
useRef
获取最新的值
看下面的例子,对比 useState
和 useRef
使用的不同:
import {useState, useRef, useEffect} from 'react';
const Counter = ()=>{
const [counter, setCounter] = useState(0);
const likeRef = useRef(0);
const handleAlert = ()=>{
setTimeout(()=>{
alert(counter);
alert(likeRef.current);
}, 2000);
}
return (
<div>
<span>{counter}</span>
<button onClick={()=>{setCounter(counter+1); likeRef.current++;}} style={{marginLeft: '20px'}}>+1</button>
<button onClick={handleAlert} style={{marginLeft: '20px'}}>alert</button>
</div>
)
}
export default Counter;
当我们点击 alert 按钮的时候,可以发现 counter 的值并不是最新的,而 likeRef.current 却是最新的。
这是为什么呢?
这是因为我们修改状态的时候, React 每次会重新渲染组件,每一次渲染都会拿到独立的 counter 的值,并且重新渲染一个 handleAlert 函数。每一个 handleAlert 函数闭包中保存着渲染时的 counter。也就是上一次的渲染不会和下一次有什么关联。下一次的渲染也不会影响上一次的数据。这个是 state
和 props
的特点。
而 useRef
返回一个可变的 ref 对象。ref 在所有 render 都保持着唯一的引用,所以 ref 取值拿到的都是 最终 的状态,而不会隔离。
获取
useRef 可以用于获取 DOM,对DOM进行操作。 一个例子:页面加载时 input 输入框自动聚焦。
import {useState, useRef, useEffect} from 'react';
const Counter = ()=>{
const inputRef = useRef(null);
useEffect(()=>{
if(inputRef && inputRef.current){
inputRef.current.focus();
}
}, [])
return (
<div>
<input type="text" ref={inputRef}/>
</div>
)
}
export default Counter;
useContext
在全局定义的state,在子组件中可以获取。当这些 state 修改的时候,会触发使用的子组件重新渲染。
React.createContext(themes.light);
初始化全局状态,默认值 themes.light- <ThemeContext.Provider value={themes.dark}> 赋值 themes.dark
useContext
子组件使用全局状态。
// App.js 中初始化全局状态
import './App.css';
import React, {useState} from 'react';
import Counter from "./Counter";
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
}
const ThemeContext = React.createContext(themes.light);
// 需要导出出去给子组件使用。
export {ThemeContext};
function App() {
const [curTheme, setCurTheme] = useState(true);
const changeTheme = ()=>{
setCurTheme(!curTheme);
}
return (
<ThemeContext.Provider value={curTheme ? themes.light: themes.dark}>
<Counter/>
<button onClick={changeTheme}>change</button>
</ThemeContext.Provider>
);
}
export default App;
在子组件中使用:
import {useState, useRef, useEffect, useContext} from 'react';
import { ThemeContext } from './App';
const Counter = ()=>{
//部分代码省略
const theme = useContext(ThemeContext);
const themeStyle = {
background: theme.background,
color: theme.foreground
}
return (
<div>
<input type="text" style={themeStyle}/>
</div>
)
}
export default Counter;
useMemo 和 useCallback
背景
一个问题引发的一系列优化故事,看下面的例子:
class Foo extends React.Component{
render(){
console.log("render...")
return (
<p>{this.props.count}</p>
)
}
}
class App extends React.Component{
constructor(props){
super(props);
this.state = {
count: 0,
double: 1
}
}
changeDouble = ()=>{
this.setState({
double: this.state.double * 2
})
}
render(){
return (
<>
<Foo count={this.state.count}/>
<p>count: {this.state.count}, double: {this.state.double}</p>
<button onClick={this.changeDouble}>Double</button>
</>
)
}
}
正常我们想要的效果是子组件 Foo 应该只有在父组件 count 的值改变的时候才需要重新 render, 但实际中每次父组件一旦 render 自己,子组件也会被 render。当我们修改 double的值的时候,子组件也会重新 render。要怎么解决这个问题呢?有以下的方法:
- 方案一:
shouldComponentUpdate(nextProps, nextState)
生命周期函数中重新定义是否需要 update - 方案二: 使用
PureComponent
组件解决 方案一:shouldComponentUpdate(nextProps, nextState)
生命周期函数返回一个 boolean 值,默认是 true,意思是组件每次都会重新 render。我们可以在这个函数中根据将要修改的 nextProps 值和当前 props 的值进行对比,如果一样,也就是没有改变,则返回 false,组件不会被重新渲染。否则,返回 true,组件需要重新渲染。
class Foo extends React.Component{
shouldComponentUpdate(nextProps, nextState){
if(nextProps.count === this.props.count){
return false;
}
return true;
}
}
方案二: 使用 PureComponent
组件解决。
class Foo extends React.PureComponent{
render(){
console.log("render...")
return (
<p>{this.props.count}</p>
)
}
}
PureComponent
组件内部是在 shouldComponentUpdate 生命周期函数中对 nextProps 和 this.props 进行了浅层比较。需要注意的是因为是浅层比较,如果我们传给子组件的是回调函数,会有问题。看下面的例子:
class App extends React.Component{
render(){
return (
<>
<Foo count={this.state.count} cb={()=>{}}/>
<button onClick={this.changeDouble}>Double</button>
</>
)
}
}
每次父组件 APP 去修改 double 的值的时候,导致自身 render 函数被执行。render 函数执行的时候,cb={()=>{}}
会重新执行,所以每次都是不同的回调对象。这就导致子组件重复渲染。
解决方案:我们需要将回调函数作为一个 class 中的一个变量,可以避免这种问题。
class App extends React.Component{
cb = ()=>{}
render(){
return (
<>
<Foo count={this.state.count} cb={this.cb}/>
</>
)
}
}
至此,在 class 组件中可以避免子组件无用的重复渲染的问题了。但是在函数组件中,这些问题依然存在。
我们通常把自身没有状态的组件使用函数组件表达。函数组件没有shouldComponentUpdate
和 PureComponent
,那要怎么实现呢?对应的解决方案就是 memo
了。
import React, {memo} from 'react';
const Foo = memo((props)=> {
console.log("render...")
return (
<p>{props.count}</p>
)
})
这就完事了吗?并没有。在 React Hook 使用过程中,我们的函数组件有了 state 的特性,那这个时候我们的父组件 App 不再只是 class 组件了,也可以是函数组件。如下:
const Foo = memo((props)=> {
console.log("render...")
return (
<p>{props.count}</p>
)
})
const App = ()=>{
const [count, setCount] = useState(0);
const [double, setDouble] = useState(1);
const cb = ()=>{}
return (
<>
<Foo count={count} cb={cb}/>
<p>count: {count}, double: {double}</p>
<button onClick={()=>{setDouble(double*2)}}>Double</button>
</>
)
}
可以看到即使我们把回调函数使用一个变量存储,每次子组件还是会重复渲染。这是因为 App 本身是函数组件,每次执行也是相互独立的。每次变量 cb 不会被保留。这个时候 useMemo
就要粉墨登场了。
useMemo
返回一个新的数据
useMemo
的语法同 useEffect
是一样的。但是 useMemo
的执行时机在渲染前执行,这点跟 useEffect
不一样。
const App = ()=>{
const [count, setCount] = useState(0);
const [double, setDouble] = useState(1);
const sum = useMemo(()=>{
return count + double;
}, [count])
return (
<>
<p>count: {count}, double: {double}, sum:{sum}</p>
<button onClick={()=>{setDouble(double*2)}}>Double</button>
<button onClick={()=>{setCount(count+1)}}>count++</button>
</>
)
}
当 count 和 double 任意发生变化的时候, sum 也会修改。有点 Vue 中 computed
的感觉。
返回一个函数
回到前面的问题,那就是如果父组件给子组件传递函数的时候,在函数组件中,怎么保证子组件不会重复渲染呢?那就是使用 useMemo
返回一个函数,useMemo(()=>{return ()=>{}}, [])
第二个参数传入空数组 [],表示只会在第一次触发执行,这样可以保证函数是同一个,子组件也不会重新渲染。
const App = ()=>{
const [count, setCount] = useState(0);
const [double, setDouble] = useState(1);
const cb = useMemo(()=>{
return ()=>{
console.log("cb...")
}
}, [])
return (
<>
<Foo count={count} cb={cb}/>
</>
)
}
那么, useCallback
怎么用呢? 那就是 useMemo
返回一个函数的时候,我们可以使用 useCallback
进行简写。如下
const App = ()=>{
const [count, setCount] = useState(0);
const [double, setDouble] = useState(1);
const cb = useCallback(()=>{
console.log("cb..")
}, [])
return (
<>
<Foo count={count} cb={cb}/>
</>
)
}
最后
如果有错误或者不严谨的地方,烦请给予指正,十分感谢。如果喜欢或者有所启发,欢迎点赞,对作者也是一种鼓励。