study / React Hook篇

作者 GaoGe 日期 2020-06-11
study / React Hook篇

最近打算使用React Hook +TypeScript打造一个前端的组件库,所以在此之前先对React Hook进行了相关总结,方便复习和巩固,后续也会更新TS篇。

React 在16.8中推出了Hook的概念,开始使用function组件来替代class组件。

Hook是一个特殊的函数,它提供了相关的钩子函数,用来钩入react的特性。

Hook 是一种复用状态逻辑的方式,它不复用 state 本身。

它实质上解决了以下问题:

  • 1.组件很难复用状态逻辑

  • 2.复杂组件难以理解,尤其是生命周期里,会包含不相关的逻辑

  • 3.高阶组件不能修改函数组件内部的状态,只能在外部修改后,通过props传入

以下介绍hook中提供的几个常用的钩子函数,用来帮助函数组件实现管理内部的state。

- 1. useState

useState函数接受一个参数,作为当前state的默认值。它的返回值为当前 state 以及更新 state 的函数(理解为class中定义的一个state和更新这个state时调用的setState,但是它不会把新的state和旧的state进行合并),state可以是任何简单类型,也可以是复杂类型对象。

首先创建class组件实现以下功能:在button组件上显示点赞次数,且每点击一次都会执行+1:

class LikeButton extends React.Component {
constructor(props) {
super(props);
this.state = { like: 0 };
}
render() {
return <button onClick={() => this.setState({ like: this.state.like + 1 })}>
{this.state.like}
</button>;
}
}

使用函数组件编写如下。直观的区别首先代码量就减少了近一半…

// 栗子①
const LikeButton: React.FC = () => {
const [like, setLike] = useState(0);
return <button onClick={() => setLike(like + 1)}>{like}</button>;
}

可以多次调用useState,创建并管理多个state:

// 栗子②
const LikeButton: React.FC = () => {
const [like, setLike] = useState(0);
const [on, setOn] = useState(true);
return <>
<button onClick={() => setLike(like + 1)}>{like}</button>
<button onClick={() => setOn(!on)}> {on ? "ON" : "PFF"} </button>
</>;
}

也可以在一个useState中定义一个对象,来包含多个属性。按个人需求选择任意方式

// 栗子③
const LikeButton: React.FC = () => {
const [obj, setObj] = useState({ like: 0, on: true });
return <>
<button onClick={() => setObj({ like: obj.like + 1, on: obj.on })}>
{obj.like}
</button>
<button onClick={() => setObj({ like: obj.like, on: !obj.on })}>
{obj.on ? "ON" : "PFF"}
</button>
</>;
}

- 2. useEffect

官网介绍,这个钩子是用来处理组件副作用的。(类似class组件中生命周期函数)

React将按照effect声明的顺序依次调用组件中的每一个effect。使用Hook可以把组件内相关的副作用组织在一起,而不再把它们拆分到不同的生命周期函数里。

(1)首先一个无需清除副作用的栗子,例如:网络请求,手动变更DOM,记录日志等;

class组件实现如下,可以看到在两个周期函数中是完全重复的代码,但这是我们的需求,我们需要在组件挂载完成和更新完成后都执行这个操作:

componentDidMount() {
document.title = `You clicked ${this.state.like} times`;
}

componentDidUpdate() {
document.title = `You clicked ${this.state.like} times`;
}

使用useEffect实现如下,这个钩子的第一个参数是一个函数,用来执行相关操作,无论是挂载还是更新,在重新渲染后都会执行useEffect:

// 栗子④
const LikeButton: React.FC = () => {
const [like, setLike] = useState(0);
const [on, setOn] = useState(true);
useEffect(() => {
document.title = `You clicked ${like} times`;
});
return <>
<button onClick={() => setLike(like + 1)}>{like}</button>
<button onClick={() => setOn(!on)}>{on ? "ON" : "PFF"}</button>
</>;
}

(2)添加监听事件,卸载组件时需要移除监听的栗子:

class组件实现如下,我们需要在挂载阶段添加监听,将要卸载组件时移除:

componentDidMount() {
document.addEventListener("click", this.updateMouse)
}
componentWillUnmount() {
document.removeEventListener("click", this.updateMouse)
}

使用useEffect实现如下,只需要在一个useEffect中,第一个参数的函数里执行添加监听,并且return一个函数,这个函数用来执行卸载时的移除:

// 栗子⑤
const MouseTracker: React.FC = () => {
const [positions, setPositions] = useState({ x: 0, y: 0 });
useEffect(() => {
const updateMouse = (e: MouseEvent) => {
setPositions({ x: e.clientX, y: e.clientY })
}
document.addEventListener("click", updateMouse);
return () => {
document.removeEventListener("click", updateMouse);
}
});
return <p>X: {positions.x},Y: {positions.y}</p>;
}

(3)注意:useEffect会在每次渲染后都执行!这就会导致,其他state的更新也执行这个与当前state不相关的副作用。

这时useEffect的第二个参数就发挥作用了,它是一个数组:

  • 当设置为空数组时,useEffect只会执行一次(可以用来执行数据请求等操作,控制只加载一次)
  • 当设置为一个值时,只有这个值改变才会执行当前effect
  • 也可以设置多个值,这样在每个值改变导致重渲染之后都会执行useEffect
// 栗子⑥
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新

- 3. 自定义hook

在必要的时候我们可以把公共的业务逻辑写成一个自定义的Hook,例如调用api获取数据,当加载过程中需要loading状态,加载完成后loading结束。相关代码如下:

// 栗子⑦
import { useState, useEffect } from "react";
import axios from "axios";

const useURLLoader = (url: string, deps: any[] = []) => {
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
axios.get(url).then(result => {
setData(result.data);
setLoading(false);
})
}, deps);
return [data, loading];
}

export { useURLLoader };

这个自定义的Hook: useURLLoader就可以在任何地方引入调用,传入url和保证钩子执行的依赖后,可以获取到包含加载结果和loading状态的操作。

- 4. useContext

组件之间共享数据(该数据对于组件树来说是全局的数据)

以下栗子,父组件中创建一个context,定义主题相关的对象key:value,引用react提供的createContext创建一个组件ThemeContext,这个组件提供属性Provider,将其他组件包裹在provider内,其他组件就可以拿到全局的样式设置。

// 栗子⑧
import React from "react";

interface IThemeProps {
[key: string]: { color: string; background: string }
}

const themes: IThemeProps = {
"light": {
color: "#000",
background: "#eee",
},
"dark": {
color: "#ff",
background: "#222",
}
}

export const ThemeContext = React.createContext(themes.light);

const App: React.FC = () => {
return <ThemeContext.Provider value={themes.light}>
<button>hello</button>
</ThemeContext.Provider>;
}
export { App };

在包裹的任意子组件中引用:

import React, { useContext } from "react";
import { ThemeContext } from "./App";

const Page: React.FC = () => {
const theme = useContext(ThemeContext);
const style = {
background: theme.background,
color: theme.color
};
return <button style={style}>click</button>;
};

export { Page };

使用hook要注意的两个重点:

● 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
● 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。

还有一些其他的钩子函数这里不逐个介绍,以后用到会继续总结,react钩子函数官网可查看~