8.6 React Hook 🎉
8.6 React Hook 🎉
Hook 是 React 16.8 的新增特性
Hook 都是些函数,这些函数能让你不使用class的情况下还能 “钩入” React state 及生命周期等特性。
提示
如果你在编写函数组件并意识到需要向其添加一些 state,以前的做法是必须将其它转化为 class。现在你可以在现有的函数组件中使用 Hook。
React 没有计划移除 class。
学了Hook之后,建议大家在以后的代码书写中,尽可能优先的使用函数组件。
使用Hook之前
import React from 'react'
class App extends React.Component<any, any> {
constructor(props: any) {
super(props);
this.state = {
count: 1,
}
}
add = () => {
this.setState({
count: this.state.count + 1
})
}
render() {
return (
<div>
<p>{this.state.count}</p>
<button onClick={this.add}>变数</button>
</div>
)
}
}
export default App
使用Hook之后
import React, { useState } from 'react'
const App = () => {
const [count, setCount] = useState(0)
const add = () => {
setCount(count + 1)
}
return (
<div>
<p>{count}</p>
<button onClick={add}>变数</button>
</div>
)
}
export default App
相信大家看了上边的案例,能后明显发现HooK的优势,这也是我建议大家使用函数组件的原因
State Hook 💎
useState是一个Hook,它本质上是一个函数,这个函数可以用来定义组件的state变量。
useState() 方法里面唯一的参数就是初始 state。不同于 class 的是,我们可以按照需要使用数字或字符串对其进行赋值,而不一定是对象。
useState返回值为:当前 state 以及更新 state 的函数。
import React, { useState } from 'react'
const App = () => {
const [count, setCount] = useState(0)
//声明叫做color的state变量
const [color, setColor] = useState('red')
const colorStyle = `rgb(${Math.random() * 255},${Math.random() * 255},${Math.random() * 255})`
return (
<div>
我是使用hook的组件
<br />
<button onClick={() => setCount(count + 1)}>变量</button>
<button onClick={() => setColor(colorStyle)}>变色</button>
<h3 style={{ color }}>数量:{count}</h3>
</div>
)
}
export default App
Effect Hook 💎
Effect Hook 可以让你在函数组件中进行数据获取,设置订阅以及进行操作 DOM等,我们称他为副作用
useEffect 可以接收一个参数,是一个回调函数,此函数就是我们的 effect。默认情况下,它在第一次渲染之后和每次更新之后都会执行。
提示
你可以把 useEffect Hook 看做 componentDidMount
,componentDidUpdate
,componentWillUnmount
三个函数的组合。
传统的Class组件
没使用EffectHook之前,相应代码需要写在不同的指定位置
import React from 'react'
class App extends React.Component<any, any> {
inputRef: any
constructor(props: any) {
super(props);
this.inputRef = React.createRef()
this.state = {
count: 1,
}
}
change = () => {
this.setState({
count: this.state.count + 1
})
}
componentDidMount() {
this.inputRef.current.value = `点击了${this.state.count}次`
}
componentDidUpdate() {
this.inputRef.current.value = `点击了${this.state.count}次`
}
render() {
return (
<div>
我是一个使用class定义的组件<br />
<h3>数量:{this.state.count}</h3>
<input ref={this.inputRef} />
<button onClick={this.change}>变数</button>
</div>
)
}
}
export default App
使用EffectHook之后
import React, { useState, useEffect } from 'react'
const App = () => {
const [count, setCount] = useState(0)
const inputRef: any = React.createRef()
const add = () => {
setCount(count + 1)
}
useEffect(() => {
inputRef.current.value = `点击了${count}次`
})
return (
<div>
<p>数量:{count}</p>
<input ref={inputRef} />
<button onClick={add}>变数</button>
</div>
)
}
export default App
清除 Effect 👻
每个 effect 都可以返回一个清除函数。它们都属于 effect 的一部分,我们期望这个清除函数在组件被卸载的时候被调用。
首先,父组件控制两个子组件的显示
import React, { useState } from 'react'
import One from './components/one'
import Two from './components/two'
const App = () => {
const [showOne, setShowOne] = useState(true)
const [showTwo, setShowTwo] = useState(true)
const downOne = () => {
setShowOne(false)
}
const downTwo = () => {
setShowTwo(false)
}
return (
<div>
<button onClick={downOne}>卸载Couter1</button>
<button onClick={downTwo}>卸载Couter2</button>
{showOne && <One />}
{showTwo && <Two />}
</div>
)
}
export default App
不使用Hook,当组件1卸载时清除定时器
import React from 'react'
export default class One extends React.Component<any, any> {
timer: any
constructor(props: any) {
super(props)
this.state = {
time: new Date().toLocaleTimeString()
}
}
componentDidMount() {
this.timer = setInterval(() => {
console.log(new Date().toLocaleTimeString())
this.setState({ time: new Date().toLocaleTimeString() })
}, 1000)
}
componentWillUnmount() {
//组件卸载的时候清除定时器
clearInterval(this.timer)
}
render() {
return (
<div>
<h3>{this.state.time}</h3>
</div>
)
}
}
使用Hook,当组件2卸载时清除定时器
import React, { useState, useEffect } from 'react'
const Two = () => {
const [time, setTime] = useState(new Date().toLocaleTimeString())
useEffect(() => {
console.log('effect')
const timer = setInterval(() => {
console.log(new Date().toLocaleTimeString());
setTime(new Date().toLocaleTimeString())
}, 1000)
return () => {
console.log('clear')
clearInterval(timer)
}
})
return (
<div>
<p>{time}</p>
</div>
)
}
export default Two
跳过 Effect 👻
在某些情况下,每次渲染后都执行或者清理 effect 可能会导致性能问题(在上边的打印中就已经看出来了)。要清除上边案例的定时器,还有另一种方式,就是只运行一次 effect
如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组[]
作为第二个参数。
import React, { useState } from 'react'
import Two from './components/two'
const App = () => {
const [showTwo, setShowTwo] = useState(true)
const downTwo = () => {
setShowTwo(false)
}
return (
<div>
<button onClick={downTwo}>卸载Couter2</button>
{showTwo && <Two />}
</div>
)
}
export default App
import React, { useState, useEffect } from 'react'
const Two = () => {
const [time, setTime] = useState(new Date().toLocaleTimeString())
useEffect(() => {
console.log('effect')
const timer = setInterval(() => {
console.log(new Date().toLocaleTimeString());
setTime(new Date().toLocaleTimeString())
}, 1000)
return () => {
console.log('clear')
clearInterval(timer)
}
}, [])
return (
<div>
<p>{time}</p>
</div>
)
}
export default Two
第二个参数也可以是非空数组
根据依赖数据的变化调用effect
传递数组作为 useEffect
的第二个可选参数,数组中可以设置要监听发生变化的数据,可以是一个也可以是多个,只要有一个元素发生变化,React 就会执行 effect。
import React, { useState, useEffect } from 'react'
const Two = () => {
const [time, setTime] = useState(new Date().toLocaleTimeString())
const [a, setA] = useState("a")
const [b, setB] = useState("a")
const changeTime = () => {
setTime(new Date().toLocaleTimeString())
}
const changeA = () => {
setA("A")
}
const changeB = () => {
setB("B")
}
useEffect(() => {
console.log(`新的时间${time}`);
return () => {
console.log(1111111111111111);
}
}, [time])
return (
<div>
<p>{time}</p>
<p>{a}</p>
<p>{b}</p>
<button onClick={changeTime}>改变Time</button>
<button onClick={changeA}>改变A</button>
<button onClick={changeB}>改变B</button>
</div>
)
}
export default Two
上述代码我只监听了time, 其它的state发生变化不会重新执行Effect, 只有time发生变化之后才会重新执行Effect
除了监听state,还可以监听props
import React, { useState } from 'react'
import Two from './components/two'
const App = () => {
let [age, setAge]: any = useState(13)
const changeAge = () => {
setAge(age++)
}
return (
<div>
<button onClick={changeAge}>改变props</button>
<Two age={age} />
</div>
)
}
export default App
import React, { useState, useEffect } from 'react'
const Two = (props: any) => {
const [time, setTime] = useState(new Date().toLocaleTimeString())
const [a, setA] = useState("a")
const [b, setB] = useState("a")
const changeTime = () => {
setTime(new Date().toLocaleTimeString())
}
const changeA = () => {
setA("A")
}
const changeB = () => {
setB("B")
}
useEffect(() => {
console.log(props.age);
console.log(`新的时间${time}`);
}, [time, props.age])
return (
<div>
<p>{props.age}</p>
<p>{time}</p>
<p>{a}</p>
<p>{b}</p>
<button onClick={changeTime}>改变Time</button>
<button onClick={changeA}>改变A</button>
<button onClick={changeB}>改变B</button>
</div>
)
}
export default Two
使用多个Effect实现关注点分离 👻
使用 Hook 其中一个目的就是要解决 class 中生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到了几个不同方法中的问题。
可以使用多个 effect。这会将不相关逻辑分离到不同的 effect 中。
未分离之前
import React from 'react'
class App extends React.Component<any, any> {
inputRef: any
constructor(props: any) {
super(props);
this.inputRef = React.createRef()
this.state = {
count: 1,
}
}
change = () => {
this.setState({
count: this.state.count + 1
})
}
timer: any
componentDidMount() {
this.timer = setInterval(() => {
console.log(new Date().toLocaleTimeString());
}, 1000)
this.inputRef.current.value = `点击了${this.state.count}次`
}
componentDidUpdate() {
this.inputRef.current.value = `点击了${this.state.count}次`
}
render() {
return (
<div>
我是一个使用class定义的组件<br />
<h3>数量:{this.state.count}</h3>
<input ref={this.inputRef} />
<button onClick={this.change}>变数</button>
</div>
)
}
}
export default App
如下图,设置定时器和重新赋值是两个不同的逻辑,但是都放在了在componentDidMount()函数内。而两个重新赋值是一模一样的,却分别放到了不同的函数内,这显然不可理
import React, { useState, useEffect } from 'react'
const App = () => {
const inputRef: any = React.createRef()
let [count, setCount] = useState(0)
const change = () => {
setCount(count++)
}
useEffect(() => {
inputRef.current.value = `点击了${count}次`
}, [count])
useEffect(() => {
const timer = setInterval(() => {
console.log(new Date().toLocaleTimeString());
}, 1000)
return () => {
clearInterval(timer)
}
}, [])
return (
<div>
<h3>数量:{count}</h3>
<input ref={inputRef} />
<button onClick={change}>变数</button>
</div>
)
}
export default App
这样我们就做到了分离,设置(取消)定时器的逻辑放在了一起,打印的逻辑放到了一起,使用Hook与使用生命周期相比较,业务逻辑更分明。
Hook 使用规则 💎
只在组件最顶层调用Hook
不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层调用Hook。
提示
这么做的目的是保证组件在初次渲染或者重新渲染的时候调用Hook的顺序保持一致。
相关信息
const [count, setCount] = useState(0);
const [color, setColor] = useState('red');
if (props.name) {
//在条件语句中调用Hook不能确保每次渲染都调用该hook或者不调用该hook,所以没法保证整个组件所有hook的调用顺序每次都一致
const [name, setName] = useState(props.name)
}
if (props.name) {
// const [name, setName] = useState(props.name)
useEffect(()=>{
const name=props.name;
})
}
const onClick=()=>{
//在函数当中也不能调用hook
const [name, setName] = useState(props.name)
}
return (
<div>
<button onClick={onClick}>点击</button>
</div>
);
提示
eslint-plugin-react-hooks
的 ESLint 插件会检测hook的使用规则。
此插件默认已添加到Create React App工具包中。
自定义 Hook 💎
当我们想在两个或者多个函数之间共享逻辑时,我们会把它提取到第三个函数中。这实现共享逻辑的第三个函数就是Hook
规则
- 自定义 Hook 是一个函数。
- 其名称以 use开头。
- 函数内部可以调用其他的 Hook。
- 在两个组件中使用相同的 Hook 不会共享 state 。
现在我们自定义一个HooK,起名为useUshook
//自定义的hook /src/utils/ushook.tsx
import { useEffect, useState } from "react";
const useUshook = () => {
const [userInfo, setUserInfo] = useState({})
useEffect(() => {
// const info = JSON.parse(localStorage.getItem('userInfo') as any)
const info = {
name: 'lcl',
age: 30
}
setUserInfo(info)
}, [])
return [userInfo, setUserInfo]
}
export default useUshook
使用自定义HooK
import React from 'react'
import useUshook from './utils/ushook'
import One from './components/one'
const App = () => {
const [user, setUser] = useUshook() as any
const change = () => {
setUser({
name: "第一个组件"
})
}
return (
<div>
<p>{user.name}</p>
<button onClick={change}>第一个组件改变的</button>
<One />
</div>
)
}
export default App
import React from 'react'
import useUshook from '../utils/ushook'
export default function One() {
const [user, setUser] = useUshook() as any
const change = () => {
setUser({
name: "第2个组件"
})
}
return (
<div>
<p>{user.name}</p>
<button onClick={change}>第2个组件改变的</button>
</div>
)
}
至此,我们看到了把获取本地存储数据的业务单独整理成了一个HooK,并且,在第一个组件中修改并不会影响其他组件发生变化
useContext 💎
useContext(MyContext)
,参数为 React.createContext
的返回值
useContext(MyContext)
用来读取 context 的值以及订阅 context 的变化。我们仍然需要在上层组件树中使用 <MyContext.Provider>
来为下层组件提供 context
提示
useContext(MyContext)
相当于 class 组件中的 myClass.contextType = MyContext
或者 <MyContext.Consumer>
首先,我们应该定义一个插件
// /src/plugins/MyContext.tsx
import React from 'react'
export default React.createContext({})
其次,实现组件传值
//父组件
import React from 'react'
import One from './components/one'
import Two from './components/two'
import MyContext from './plugins/MyContext'
export default function App() {
return (
<div>
<MyContext.Provider value={{ name: 'lcl', age: '04' }}>
<One />
</MyContext.Provider>
<MyContext.Provider value={{ name: 'haha', age: '14' }}>
<Two />
</MyContext.Provider>
</div>
)
}
//子组件
import React, { useContext } from 'react';
import MyContext from '../plugins/MyContext';
export default function One() {
const info = useContext(MyContext) as any;
return (
<div>
<p>{info.name}</p>
<p>{info.age}</p>
</div>
)
}
//子组件
import React, { useContext } from 'react';
import MyContext from '../plugins/MyContext';
export default function Two() {
const info = useContext(MyContext) as any;
return (
<div>
<p>{info.name}</p>
<p>{info.age}</p>
</div>
)
}
useRef 💎
useRef
返回一个可变的 ref 对象。
参数为其 .current
属性的初始值。
返回的 ref 对象在组件的整个生命周期内(每一次渲染)保持不变。
提示
使用useRef
可以代替React.createRef()
import React, { useRef } from 'react';
export default function App() {
const inputRef: any = useRef(null);
//访问inputRef的current属性就可以访问到inputDOM
return (
<div>
<input ref={inputRef} />
<button onClick={() => inputRef.current.focus()}>获得焦点</button>
</div>
);
}
useRef还有另一种用法,具有缓存数据的功能,看下边举例
import React, { useState } from 'react'
export default function UseRefTest() {
let count = 0
const [time, setTime]: any = useState()
const changeCount = () => {
count++
setTime(new Date().toLocaleTimeString())
}
const showCount = () => {
console.log(count)
}
return (
<div>
<button onClick={changeCount}>更改count</button>
<button onClick={showCount}>查看count</button>
<h3>当前时间是:{time}</h3>
<h3>当前count是:{count}</h3>
</div>
)
}
按理来说,当我点击三次更改后,再次点击查看,看到的count值为3才对,可结果并不是,引用调用了changeCount,组件会重新渲染,则组件的代码会重新执行,那么count就一直被重置为0,所以永远拿不到count更改后的值,解决办法如下:
import React, { useState, useRef } from 'react'
export default function UseRefTest() {
const count: any = useRef(0)
const [time, setTime]: any = useState()
const changeCount = () => {
count.current++
setTime(new Date().toLocaleTimeString())
}
const showCount = () => {
console.log(count)
}
return (
<div>
<button onClick={changeCount}>更改count</button>
<button onClick={showCount}>查看count</button>
<h3>当前时间是:{time}</h3>
<h3>当前count是:{count.current}</h3>
</div>
)
}
useMemo 💎
useMemo(fn, deps)
第一个参数为函数,用来返回需要缓存的值,第二个参数为依赖项数组。
它仅会在某个依赖项改变时才重新调用函数进行计算,改变缓存值的状态(如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值)。这种优化有助于避免在每次渲染时都进行高开销的计算。
useMemo跟useEfect区别
useMemo用于去缓存数据,诸如副作用这类的操作属于 useEffect
的适用范畴,而不是 useMemo
,你可以把它理解成Vue中的计算属性
import React, { useState, useMemo } from 'react';
export default function App() {
const [num1, setNum1] = useState(0)
const [num2, setNum2] = useState(0)
const [num3, setNum3] = useState(0)
let num = useMemo(() => {
console.log(12);
return num1 + num2
}, [num1, num2]); //当num1或者num2改变时nun才会发生变化
return (
<div>
<button onClick={() => setNum1(num1 + 1)}>num1 +</button>
<button onClick={() => setNum2(num2 + 1)}>num2 +</button>
<button onClick={() => setNum3(num3 + 1)}>num3 +</button>
<h3>{num1}+{num2}={num}======{num3}</h3>
</div>
)
}
useCallback 💎
useCallback(fn, deps)
第一个参数为函数,是需要被缓存的函数,第二个参数为依赖项数组
它仅会在某个依赖项改变时才重新定义被缓存函数,(如果没有提供依赖项数组,useCallback 在每次渲染时都会重新定义函数)
提示
useCallback(fn, deps)
相当于 useMemo(fn, deps)
import React, { useState, useMemo, useCallback } from 'react';
export default function App() {
const [num1, setNum1] = useState(0)
const [num2, setNum2] = useState(0)
const [num3, setNum3] = useState(0)
// 缓存值
const num = useMemo(() => {
console.log(num1 + num2)
return num1 + num2
}, [num1, num2])
// 缓存函数
const changeNum1 = useCallback(() => {
setNum1(num1 + 1)
}, [num1]) //只有num1改变函数才会重新定义
return (
<div>
<button onClick={() => setNum1(num1 + 1)}>num1 +</button>
<button onClick={() => setNum2(num2 + 1)}>num2 +</button>
<button onClick={() => setNum3(num3 + 1)}>num3 +</button>
<button onClick={changeNum1}>更改num1</button>
<h3>{num1}+{num2}={num}-----------{num3}</h3>
</div>
)
}