一文学会 React Hooks

573 阅读12分钟

React Hooks

最近接触了hooks这个好东西,用了以后才发现是那么的好用,想想总结一下最近使用的一些思考和看法.我觉得hooks是一个趋势,用了hooks以后就回不去了的感觉

React Hooks介绍

hooks的出现使得原来要用类声明组件的方式变为函数式声明,原来有状态和无状态,现在一律都为无状态组件了.也让单元测试更加方便.正因为没有了类的声明方式,也就没有了生命周期.但是声明周期是我们一直以来在react非常重要的概念.不管是react还是vue.声明周期一直是很重要的一块知识.但是hooks它用另外一种思考方式帮助我们用更少的代码,更优雅的理念去实现我们的业务.换句话说,hooks也实现了生命周期,可能它做的更好.

Hooks到底哪好了?

首先,不说别的,它写的代码简洁

class写法:

import React, { Component } from 'react';

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      num: 0
    }
  }
  addNum = () => {
    this.setState((prevState) => ({
      num: prevState.num + 1
    }))
  }
  render() { 
    return (
      <div>
        <p>{this.state.num}</p>
        <button onClick={this.addNum}>Chlick me</button>
      </div>
    );
  }
}
 
export default App;

再来看看hooks写法:

import React, { useState } from 'react';

const App = () => {
  const [num, setNum] = useState(0)

  return (
    <div>
      <p>{num}</p>
      <button onClick={() => {setNum(num + 1)}}>Add</button>
    </div>
  )
}
 
export default App;

告别了state,告别了生命周期,没有了class,最重要的是我们不用绑定this了!跟this基本就👋了

刚才介绍了一下hooks的基本使用,就是大伙见个面留个好印象,接下来我们看看useState是如何使用的

useState

useState是用来声明状态变量的

声明方式

const [num, setNum] = useState(0)
const [变量名, 修改变量名函数名] = useState(初始值)

通过ES6解构赋值,这个变量名和函数名是你随便取的哈,但是为了我们自己好辨认,函数名就都习惯性的加setFunc的方式写.

当然我们的变量可以声明多个

const [num1, setNum1] = useState(0)
const [num2, setNum2] = useState({age: 25})
const [num3, setNum3] = useState([1,2,3])
...

useState不仅仅接受基本类型和对象,数组,还可以传入一个函数,但是这个函数只执行一次.

import React, { useState } from 'react';

const App = ({price}) => {
  const [num, setNum] = useState(() => price || 5)

  return (
    <div>
      <p>{num}</p>
      <button onClick={() => {setNum(num + 1)}}>Add</button>
    </div>
  )
}
 
export default App;

这个获取就简单了,就直接把变量名放在JSX中就OK了,只不过不会像之前一样还有this.state.

<p>{num}</p>

修改变量

原来要通过setState,现在要用到我们刚才声明的setNum来修改

<button onClick={() => {setNum(num + 1)}}>Add</button>

此外useState每一次渲染都会记住上一次的值,因此如果我们想获取这次渲染前的值的时候,我们可以传入匿名函数来获取

<button onClick={() => {setNum((num) => num + 1)}}>Add</button>

最后一点说明,hooks不要在条件语句等环境下去使用hooks,因为声明useState的位置是一个数组,你改变了useState的顺序的时候,这个useState的数据就会出现混乱.导致报错

import React, { useState } from 'react';

const App = ({price}) => {
  let num, setNum
  if (Math.random > 0.5) {
    [num, setNum] = useState(1)
  } else {
    [num, setNum] = useState(2)
  }

  return (
    <div>
      <p>{num}</p>
      <button onClick={() => {setNum(num + 1)}}>Add</button>
    </div>
  )
}
 
export default App;

useEffect

当数据产生变化useEffect,会执行一系列的副作用

其实useEffect可以当做componentDidMount, componentDidUpdate, componentWillUnmount生命周期的结合,但是,如果想用好useEffect还是要费一些功夫的.但是用好了你会发现useEffect确实很好用,一个useEffect三个生命周期全部搞定.

既然useEffect可以用来实现生命周期,那么就看看到底怎么实现生命周期

使用

import React, { useState, useEffect } from 'react';

const App = ({price}) => {
  const [num, setNum] = useState(() => price || 5)

  useEffect(() => {
    console.log('num改变执行了useEffect')
  })

  return (
    <div>
      <p>{num}</p>
      <button onClick={() => {setNum(num + 1)}}>Add</button>
    </div>
  )
}
 
export default App;

useEffect 在第一次渲染和数据发生改变的时候就会执行一次,因此我们可以总结一下
useEffect此时相当于componentDidMount + componentDidUpdate
**
此时需注意一个问题,useEffect是异步的,因此,它不会阻碍页面的渲染视图,但是componentDidMount + componentDidUpdate是同步的.如果想测量宽高等布局的时候可以使用useLayoutEffect

下面我们来实现一下componentWillUnmount

想实现componentWillUnmount,必须要返回一个函数的方式来进行解绑.具体看代码

import React, { useState, useEffect } from 'react';

const App = () => {
  const [num, setNum] = useState(0)
  const [width, setWidth] = useState(document.body.clientWidth)

  const onChangeSize = () => {
    setWidth(document.body.clientWidth)
  }
	
  useEffect(() => {
    console.log('初次渲染和改变数据都会执行')
    window.addEventListener('resize', onChangeSize)
    return () => {
      console.log('卸载组件和改变数据执行')
      window.removeEventListener('resize', onChangeSize)
    }
  })

  return (
    <div>
      <p>{width}</p>
      <button onClick={() => {setNum(num + 1)}}>Add</button>
    </div>
  )
}
 
export default App;

从代码中我们可以看到,通过返回一个函数我们可以去进行解绑操作,但是,如果你修改num的数据的时候return也会执行这里先详细说一下具体的useEffect执行顺序

  1. 页面渲染,执行 console.log('初次渲染和改变数据都会执行'), return 的函数不执行
  2. 改变数据后,先执行return 的函数 console.log('卸载组件和改变数据执行'),再执行console.log('初次渲染和改变数据都会执行')

我们可以看到,我们的需求实现是有问题的,我想在**componentDidMount **实现监听, componentWillUnmount 实现解绑,但是由于改变数据也进行了解绑操作,这是有问题的,因此需要useEffect的第二个函数

那么我们来实现一下上面的需求吧

import React, { useState, useEffect } from 'react';

const App = () => {
  const [num, setNum] = useState(0)
  const [width, setWidth] = useState(document.body.clientWidth)

  const onChangeSize = () => {
    setWidth(document.body.clientWidth)
  }
	
  // 实现需求的useEffect
  useEffect(() => {
    // componentDidMount
    window.addEventListener('resize', onChangeSize)
    return () => {
      // componentWillUnmount
      window.removeEventListener('resize', onChangeSize)
    }
  }, [])

  useEffect(() => {
    // componentDidMount + componentDidUpdate
    document.title = num
  })

  useEffect(() => {
    // componentDidMount
    setTimeout(() => {
      console.log('ajax请求')
    }, 1000)
  }, [])

  useEffect(() => {
    console.log(`num改变为${num}`)
  }, [num])

  return (
    <div>
      <p>{width}</p>
      <button onClick={() => {setNum(num + 1)}}>Add</button>
    </div>
  )
}
 
export default App;

传入空数组的意思是,这个useEffect已经和数据无关了.

其实这个数组是用来告诉useEffect到底被谁影响,既然你写了空数组,它就和任何state数据无关了.如果想关联上state数,上面代码的第33行明确指出了,只有num才能影响其useEffect.你也写多个state数据,去关联这个useEffect.

useEffect是可以写多个的.不会像之前的声明周期一样要都写在一起,做很多的判断了

下面我们做一下总结

useEffect的第二个参数有六种情况

  1. 没有第二个参数,这是最简单的,不写return函数,就相当于 componentDidMount + componentDidUpdate
  2. 没有第二个参数,写return函数,就相当于 componentDidMount + componentDidUpdate + 不严谨的componentWillUnmount, 但是由于改变数据会执行return的函数产生干扰,因此没有实现真正意义上的三种生命周期
  3. 只传入一个空数组[], 不写return函数,那么它只会调用一次,它的含义就告诉我们这个useEffect和数据已经没有关系了.但是第一次渲染的时候会执行,相当于 componentDidMount
  4. 只传入一个空数组[], 写return函数,和第三种方式相同,唯一不同的是由于return 函数,在卸载组件的时候也会执行,相当于 componentDidMount + componentWillUnmount
  5. 传入一个数组,其中包括变量时,没有return函数,只有数组中的变量改变了,useEffect才会执行
  6. 传入一个数组,其中包括变量时,又有return函数,那么它相当于 componentDidMount + 特定的componentDidUpdate + 不严谨的componentWillUnmount,因为它又因为数据去执行return函数了

因此.如果想实现生命周期的作用采用1,3,4. 第二种不做推荐,逻辑比较混乱,第五种可以在你做一些逻辑需求的时候可以使用,第六种的话我没怎么尝试过,因为我尽量都拆分着写.

其实我认为没有必要去写的那么复杂,能拆分的还是拆分吧

useContext

帮助我们获取跨层级组件传递变量

下面说一个🌰:

首先,我们有这样一个情景
爷爷组件App, 儿子组件Detail,孙子组件Btn
爷爷有个变量传给孙子,但是中间隔着儿子,传递很麻烦,一般我们传递给儿子就需要props就可以了.但是明显如果爷爷年龄比较大,还有重孙,那么我们岂不是更麻烦了.这个时候可以用到context了
PS: context和redux所解决的不是一个事情,一个是解决传值,一个是解决全局数据管理

// 爷爷组件 App.js

import React, { useState, createContext } from 'react';
import Detail from './Detail'
const NumContext = createContext()

const App = () => {
  const [num, setNum] = useState(0)

  return (
    <div>
      <p>{num}</p>
      <button onClick={() => {setNum(num + 1)}}>Add</button>
      <NumContext.Provider value={num}>
        <Detail />
      </NumContext.Provider>
    </div>
  )
}
 
export {App, NumContext};
// 儿子孙子组件 Detail Btn

import React, {useContext} from 'react'
import { NumContext } from './App'

const Detail = () => {
  return <div id="Detail"><Btn /></div>
}

const Btn = () => {
  const num = useContext(NumContext)
  return <button>{num}</button>
}

export default Detail

想实现context总共分

  1. 创建createContext
const NumContext = createContext()
  1. 利用创建好得到的组件我们写一个闭合标签, value是你想提供的变量
<NumContext.Provider value={num}>
	// 组件..
</NumContext.Provider>
  1. 在孙子组件引入你在某某长辈那创建的context组件,再使用我们的hooks来获取到 value值
import React, {useContext} from 'react'
import { NumContext } from './App'

const Btn = () => {
  const num = useContext(NumContext)
  return <button>{num}</button>
}

由此我们可以发现,不管多少层级,都是可以获取到的

useReducer

useReducer其实就是模拟了Redux的Reducer,而且它经常和useContext结合使用,起到 Redux的效果

👇看🌰:

import React, { useReducer } from 'react';

const App = () => {
  const [state, dispatch] = useReducer((state, action) => {
    switch (action.type) {
      case 'increase':
        return {num: state.num + 1}
      case 'decrease':
        return {num: state.num - 1}
      default:
        return state
    }
  }, {
    num: 0
  })

  return (
    <div>
      <p>{state.num}</p>
      <button onClick={() => {dispatch({type: 'increase'})}}>increase</button>
      <button onClick={() => {dispatch({type: 'decrease'})}}>decrease</button>
    </div>
  )
}
 
export default App;

第一个参数就是一个reducer函数,第二个参数是默认state值,它可以返回两个值,一个是state,另一个就是dispatch

useContext UseReducer 实现 Redux

下面来简单分享一下我的一写理解, 利用hooks特性实现 Redux 相似的功能

目录如下:

image.png

其实跟之前写法差不多,稍微有那么点去区别

首先是index - 这个文件是根文件,我们在整个项目中让所有组件都能获取store中的数据:

// index.js 
import React from 'react'
import {Color} from './store/state'
import Home from './components/Home'

const App = () => {
  return (
      <div>
        <Color>
	        {/* 里面包含所有的子组件, 这里以 Home 为例 */}
          <Home />
        </Color>
      </div>
  )
}

export default App

👇看store中的内容,其实上面已经引入了store中文件了

// state.js

import React, { createContext, useReducer } from 'react'
import {reducer} from './reducer'

// 1. 默认 state
const defaultState = {
  color: 'blue'
}

// 2. 创建一个 context, 作用是让所有受包裹的组件都能够得到 state, dispatch
export const DataContext = createContext()

export const Color = props => {
  // 3. useReducer 创建出 state, dispatch, 将其放入 Provider 的 value 中
  const [state, dispatch] = useReducer(reducer, defaultState)

  return (
    <DataContext.Provider value={{state, dispatch}}>
      {props.children}
    </DataContext.Provider>
  )
}

此时已经成功一大半了.因为我们已经把state, dispatch 全部放入 value 中, 那么所有组件都已经可以全局的拿到 state了

下面就简单了.

// reducer.js

import { UPDATE_COLOR } from './constants'

export const reducer = (state, action) => {
  switch (action.type) {
    case UPDATE_COLOR:
      return {color: action.color}
    default:
      return state
  }
}
// constants.js

export const UPDATE_COLOR = 'UPDATE_COLOR'
// actionCreators.js

import { UPDATE_COLOR } from './constants'

export const updateColor = (color) => ({
  type: UPDATE_COLOR,
  color
})

至此我们已经完成了

代码demo

useMemo

useMemo 的用处在于可以帮助我们节约资源

先举个🌰:

import React, { useState, useMemo } from 'react';

const App = () => {
  const [a, setA] = useState('a')
  const [b, setB] = useState('b')

  return (
    <div>
      <p>{a} App</p>
      <p>{b} App</p>
      <button onClick={() => {setA(a + a)}}>aaa</button>
      <button onClick={() => {setB(b + b)}}>bbb</button>
      <Children theA={a}>{b}</Children>
    </div>
  )
}

const Children = ({theA, children}) => {

  console.log('children 重新渲染')

  const aChange = (getA) => {
    console.log('useMemo')
    return getA + ' useMemo'
  }

  return (
    <div>
      <p>{aChange()}</p>
      <p>{children}</p>
    </div>
  )
}

export default App;

我们发现,如果不使用useMemo,在你改变 b 的值的时候, aChange 也会执行,也就是说,只要你改变父组件的任何变量,都会影响 Children. a 变量其实也没有发生任何变化,但是 aChange 依然执行了

因此我们需要使用 useMemo 控制一下

import React, { useState, useMemo } from 'react';

const App = () => {
  const [a, setA] = useState('a')
  const [b, setB] = useState('b')

  return (
    <div>
      <p>{a} App</p>
      <p>{b} App</p>
      <button onClick={() => {setA(a + a)}}>aaa</button>
      <button onClick={() => {setB(b + b)}}>bbb</button>
      <Children theA={a}>{b}</Children>
    </div>
  )
}

const Children = ({theA, children}) => {

  console.log('children 重新渲染')

  const aChange = (getA) => {
    console.log('useMemo')
    return getA + ' useMemo'
  }

  const aVal = useMemo(() => aChange(theA), [theA])

  return (
    <div>
      <p>{aVal}</p>
      <p>{children}</p>
    </div>
  )
}

export default App;

使用 useMemo, 第一个为你所去计算值的函数,第二个参数为数组中的变量值的变化将会执行 useMemo 的函数

useRef

useRef可以帮助我们一个获得一个整个生命周期不变的对象

import React, { useState, useRef } from 'react';

const App = () => {
  let [num, setNum] = useState(0);
  return (
      <div>
          <Children />
          <button onClick={() => setNum({ num: num + 1 })}>+</button>
      </div>
  )
}

let input;
function Children() {
    const inputRef = useRef()
    console.log(input === inputRef)
    input = inputRef
    return <input type="text" ref={inputRef} />
}

export default App;

自定义 Hook

自定义 hook 有点像我们写的函数, 但是自定义 hook 有自己的 state, 它只是帮助我们实现复用逻辑,但是它每次调用所得到的状态都是它自身.各个自定义 hook 之间的 state 相互无关 此外,自定义 hook 返回的结果的变化也会重新 render 父组件 在命名方面,我们通常用use做为自定义hook的命名方式,当然,这是一种不成文的规定

import React, { useState, useEffect } from 'react';

const useEnter = (key) => {
  const [hasPressed, setHasPressed] = useState(false)

  const keyDown = ({keyCode}) => {
    if (key === keyCode) {
      setHasPressed(true)
    }
  }

  const keyUp = ({keyCode}) => {
    if (key === keyCode) {
      setHasPressed(false)
    }
  }

  useEffect(() => {
    // console.log('addEventListener')
    document.addEventListener('keydown', keyDown)
    document.addEventListener('keyup', keyUp)
    return () => {
      // console.log('removeEventListener')
      document.removeEventListener('keydown', keyDown)
      document.removeEventListener('keyup', keyUp)
    }
  })
  return hasPressed
}

const App = () => {
  console.log('render')
  const [name, setName] = useState('kun')
  const isEnter = useEnter(13)
  useEffect(() => {
    if (isEnter) {
      setName('flower')
    } else {
      setName('kun')
    }
  }, [isEnter])
  return (
    <div>
      <p style={{fontSize: '50px'}}>{name}</p>
    </div>
  )
}

export default App;