一文學會 React Hooks

React Hooks

最近接觸了hooks這個好東西,用了之後才發現是那麼的好用,想一想總結一下最近使用的一些思考和見解.我以爲hooks是一個趨勢,用了hooks之後就回不去了的感受vue

React Hooks介紹

hooks的出現使得原來要用類聲明組件的方式變爲函數式聲明,原來有狀態和無狀態,如今一概都爲無狀態組件了.也讓單元測試更加方便.正由於沒有了類的聲明方式,也就沒有了生命週期.可是聲明週期是咱們一直以來在react很是重要的概念.不論是react仍是vue.聲明週期一直是很重要的一塊知識.可是hooks它用另一種思考方式幫助咱們用更少的代碼,更優雅的理念去實現咱們的業務.換句話說,hooks也實現了生命週期,可能它作的更好.react

Hooks到底哪好了?

首先,不說別的,它寫的代碼簡潔git

class寫法:github

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寫法:ajax

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基本就👋了redux

剛纔介紹了一下hooks的基本使用,就是大夥見個面留個好印象,接下來咱們看看useState是如何使用的數組

useState

useState是用來聲明狀態變量的bash

聲明方式

const [num, setNum] = useState(0)
複製代碼
const [變量名, 修改變量名函數名] = useState(初始值)
複製代碼

經過ES6解構賦值,這個變量名和函數名是你隨便取的哈,可是爲了咱們本身好辨認,函數名就都習慣性的加setFunc的方式寫.dom

固然咱們的變量能夠聲明多個異步

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 父組件

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;

複製代碼
相關文章
相關標籤/搜索