跳至主要內容

8.6 React Hook 🎉

刘春龙...大约 11 分钟RRACTWEB前端react

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 看做 componentDidMountcomponentDidUpdate ,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>
  )
}
上次编辑于:
贡献者: 刘春龙
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.7