React Hook之useState、useEffect和useContext

前言

一週的磚又快搬完了,又到了開心快樂的總結時間~這兩週一直在 hook 函數的「坑」裏,久久不能自拔。應該也不能叫作「坑」吧,仍是本身太菜了,看文檔不仔細,不少覺得不重要,但在實際應用中卻很關鍵的點老是被本身忽略。因此我準備多花點時間,把官網的一些 hook 函數,再回過頭看一遍,整理整理(在整理的過程,我以爲更容易發現問題和總結經驗)。html

這篇文章主要整理一下 React 中的三個基礎 Hook:前端

  • useState
  • useEffect
  • useContext

useState

useState 相比其餘 hooks 仍是很簡單的,主要就是用來定義變量。官方文檔描述的也很清楚,對此已經很熟練的看官大大能夠跳過哦~react

相遇--初識

const [count, setCount] = useState(0)
複製代碼
  • 調用 useState 方法作了什麼?git

    定義一個「state變量」。github

  • useState 須要什麼參數?npm

    useState 方法接收一個參數,做爲變量初始化的值。(示例中調用 useState 方法聲明一個 「state變量」 count,默認值爲 0。)redux

  • useState 方法的返回值是什麼?數組

    返回當前 state 以及更新 state 的函數。瀏覽器

相知--使用useState

React 會確保 setState 函數的標識是穩定的,而且不會在組件從新渲染時發生變化。這就是爲何能夠安全地從 useEffect 或 useCallback 的依賴列表中省略 setState。安全

首先咱們先經過 useState 方法定義三個變量(包含基本類型和引用類型的數據)分別爲:countstudentInfosubjectList,而後對它們的值進行修改。

const [count, setCount] = useState(0)

const [studentInfo, setStudentInfo] = useState({name: '小文', age: 18, gender: '女'})

const [subjectList, setSubjectList] = useState([
  { id: 0, project_name: '語文' },
  { id: 1, project_name: '數學' }
])
複製代碼
  • 修改 count 值爲1
setCount(1)
複製代碼
  • 修改 studentInfo 對象的 age 屬性,值爲 20;並添加 weight 屬性,值爲 90
setStudentInfo({
  ...studentInfo,
  age: 20,
  weight: 90
})
複製代碼
  • 修改 subjectList 數組的第二項的 project_name 屬性,值爲體育;並添加第三項 { id: 2, project_name: '音樂' }
# 忽略這裏的深拷貝,優雅的方式有不少:immutable.js、immer.js、loadsh
let temp_subjectList = JSON.parse(JSON.stringify(subjectList))
temp_subjectList[1].project_name = '體育'
temp_subjectList[2] = { id: 2, project_name: '音樂' }
setSubjectList(temp_subjectList)
複製代碼

咱們在實際的開發中,會用到 React 提供的 Eslint 插件來檢查 Hook 的規則和 effect 的依賴,當檢測出某一塊的代碼缺乏依賴時,會給出警告,若是給出的警告是缺乏 setState 函數,那咱們就能夠忽略它。(後面講到 useEffect 的時候會再補充)

React 會確保 setState 函數的標識是穩定的,而且不會在組件從新渲染時發生變化。這就是爲何能夠安全地從 useEffectuseCallback 的依賴列表中省略 setState。(官網)

函數式更新

若是新的 state 須要經過使用先前的 state 計算得出,那麼能夠將函數傳遞給 setState。該函數將接收先前的 state,並返回一個更新後的值。(對於引用類型的數據,上面同樣,例如數組。也是不能夠直接對變量進行操做的)

使用上面定義的變量:

  • 點擊按鈕累加 count
<button onClick={() => setCount(prevCount => ++prevCount)}>+ 累加</button>
複製代碼
  • 修改 studentInfo 對象的 age 屬性,值爲 20
setStudentInfo(prevState => {
  # 也可使用 Object.assign
  return {...prevState, age: 20}
})
複製代碼

惰性初始 state

若是初始 state 須要經過複雜計算得到,則能夠傳入一個函數,在函數中計算並返回初始的 state,此函數只在初始渲染時被調用:

const [state, setState] = useState(() => {
  const initialState = someExpensiveComputation(props)
  return initialState
})
複製代碼

實際應用

咱們在實際應用中,常常會遇到一些結構比較複雜的數據,若是每一個地方都使用 useState 去定義這些複雜結構的數據,估計會累死。

這裏就分享一下我在項目中使用的一個插件 use-immer,一些基本類型數據和「直接替換的數據」(例若有一個數組 arr,對 arr 修改是直接賦值 setArr([...]),這樣的數據我會選擇用 useState 來聲明,對於大部分引用類型的數據我會使用 use-immer 提供的 useImmer 方法來聲明。下面咱們就來看看它是如何使用的吧~

  1. 安裝
npm install immer use-immer
複製代碼
  1. 引用
import { useImmer } from 'use-immer'
複製代碼
  1. 從新聲明上面用到的 subjectList
const [subjectList, setSubjectList] = useImmer([
  { id: 0, project_name: '語文' },
  { id: 1, project_name: '數學' }
])
複製代碼
  1. 修改 subjectList 數組的第二項的 project_name 屬性,值爲體育;並添加第三項 { id: 2, project_name: '音樂' }
setSubjectList(draft => {
  draft[1].project_name = '體育'
  draft[2] = { id: 2, project_name: '音樂' }
})
複製代碼

須要注意的是,這裏的 setSubjectList 方法接收的是一個函數,該函數接收一個參數 draft,能夠理解爲是變量 subjectList 的副本。這種寫法是否是有種 「家」 的感受呢,感興趣的能夠深刻了解一下哦(immutable、immer、use-immer)。

useEffect

關於 useEffect 函數,我我的的建議是先把官網上的介紹看一遍,再多研讀研讀《useEffect 完整指南》。看完會發現,咱們對它已經有了更加深入的認識。這裏也僅僅是我在學習的過程整理的筆記,內容就是《useEffect 完整指南》簡化。

useEffect 會在瀏覽器繪製後延遲執行,但會保證在任何新的渲染前執行。React 將在組件更新前刷新上一輪渲染的 effect。

每一次渲染

重點:關於每一次渲染(rendering),組件都會擁有本身的:

  1. Props and State
  2. 事件處理函數
  3. Effects

Props and State

寫一個計數器組件 Counter

function Counter() {
  const [count, setCount] = useState(0)

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  )
}
複製代碼

Counter組件第一次渲染的時候,從 useState() 拿到 count 的初始值爲 0。當咱們調用 setCount(1)時,React 會再次渲染該組件,此時 count 的值爲 1,以此類推,每一次渲染都是獨立的。

# During first render
function Counter() {
  const count = 0; # Returned by useState()
  # ...
  <p>You clicked {count} times</p>
  # ...
}

# After a click, our function is called again
function Counter() {
  const count = 1; # Returned by useState()
  # ...
  <p>You clicked {count} times</p>
  # ...
}

# After another click, our function is called again
function Counter() {
  const count = 2; # Returned by useState()
  # ...
  <p>You clicked {count} times</p>
  # ...
}
複製代碼

Counter 組件中的 count 僅僅是一個常量,這個常量由 React 提供。當調用 setCount 的時候,React 會帶着一個不一樣的 count 值再次調用組件。而後,React會更新DOM以保持和渲染輸出一致。

最關鍵 的就是:任意一次渲染中的 count 常量都不會隨着時間改變。渲染輸出會變是由於 Counter 組件被調用,而在每一次調用引發的渲染中,它包含的 count 常量都是獨立的。也就是說組件的每次渲染,props 和 state 都是獨立的。

事件處理函數

修改一下計數器組件 Counter 的例子。

組件內容:有兩個按鈕,一個按鈕用來修改 count 的值,另外一個按鈕在 3s 延遲後展現彈窗。

function Counter() {
  const [count, setCount] = useState(0);

  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + count)
    }, 3000)
  }

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
      <button onClick={handleAlertClick}>
        Show alert
      </button>
    </div>
  )
}
複製代碼
  • 點擊按鈕修改 count 的值爲 3。
  • 點擊另外一個按鈕打開彈窗。
  • 在彈窗彈出前,點擊按鈕修改 count 的值爲 5。

此時,彈窗中的展現的 count 值爲 3。

分析:

首先整個過程進行了 6 次渲染。

  1. 初始化渲染:render0;
  2. 修改 count 值爲 3,進行 3 次渲染:render1 -> render2 -> render3;
  3. 點擊按鈕打開彈窗,此時組件是 「render3 狀態」;
  4. 修改 count 值爲 5,進行 2 次渲染:render3 -> render5 -> render5;
# 組件狀態:render0 -> render1 -> render2 -> render3
function Counter() {
  const count = 3
  # ...
  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + count)
    }, 3000)
  }
  # ...
}

# 組件狀態: render3
# 觸發事件處理函數 handleAlertClick,此時該函數捕獲 count 值爲 3,並將在 3 秒後打開彈窗。
function Counter() {
  const count = 3
  # ...
  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + 3)
    }, 3000)
  }
  # ...
}

# 組件狀態:render3 -> render5 -> render5
function Counter() {
  const count = 5
  # ...
  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + count)
    }, 3000)
  }
  # ...
}
複製代碼

Effects

修改 Counter 組件,點擊 3 次按鈕:

function Counter() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    setTimeout(() => {
      console.log(`You clicked ${count} times`)
    }, 3000)
  })

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  )
}
複製代碼

分析: 整個過程組件進行了四次渲染:

  1. 初始化,render0:打印 You clicked 0 times
  2. 修改 count 值爲1,render1:打印 You clicked 1 times
  3. 修改 count 值爲2,render2:打印 You clicked 2 times
  4. 修改 count 值爲3,render3:打印 You clicked 3 times

經過整個例子咱們能夠知道,在每次渲染中,useEffect 也是獨立的。

並非 coun t的值在「不變」的 effect 中發生了改變,而是 effect 函數自己在每一次渲染中都不相同。

清除 effect

當咱們在 useEffect 中使用了定時器或者添加了某些訂閱,能夠經過 useEffect 返回一個函數,進行清除定時器或者取消訂閱等操做。但咱們須要知道的是,清除是 「滯後」 的。(這裏是我的的理解,可能描述的不許確)

看一下例子,在 useEffect 中打印點擊的次數:

function Example() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    console.log(`You clicked ${count} times`)
    return() => {
      console.log('銷燬')
    }
  })

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  )
}
複製代碼

點擊按鈕 3 次,控制檯中打印的結果以下:

  1. You clicked 0 times
  2. 銷燬
  3. You clicked 1 times
  4. 銷燬
  5. You clicked 2 times
  6. 銷燬
  7. You clicked 3 times

從打印結果咱們能夠很容易看出,上一次的 effect 是在從新渲染時被清除的。

補充:那麼組件的整個從新渲染的過程是怎麼樣的呢?

假設如今有 render0 和 render1 兩次渲染:

  1. React 渲染 render1 的UI;
  2. 瀏覽器繪製,並呈現 render1 的UI;
  3. React 清除 render0 的 effect;
  4. React 運行 render1 的 effect;

React 只會在瀏覽器繪製後運行 effects。這使得你的應用更流暢由於大多數effects並不會阻塞屏幕的更新。

經過下面這個例子,來印證一下這個結論吧~

function Example() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    setCount(99)
    console.log(count)
    return() => {
      console.log('銷燬')
    }
  })

  console.log('我確定最早執行!')

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  )
}
複製代碼

運行代碼,在控制檯咱們能夠看到如下輸出(此時組件狀態爲初始化:render0):

  1. 我確定最早執行!
  2. 0
  3. 我確定最早執行!
  4. 銷燬
  5. 99
  6. 我確定最早執行!

根據打印結果,咱們能夠分析出:

  1. React 渲染初始化的UI(render0);
  2. 執行 useEffect
  3. 調用 setCount 方法修改 count 值爲 99,組件從新渲染(render1);
  4. 根據執行順序,繼續執行 render0 狀態下的 useEffect,打印 count 的值爲 0。(render0 下 count 值爲0)
  5. React 從新渲染UI(render1);
  6. 執行 render0 的 useEffect 的清除函數;
  7. 執行 render1 的 useEffect
  8. 調用 setCount 方法修改 count 值爲 99(因爲傳入的值沒有改變,因此組件沒有從新渲染);
  9. 打印 count 的值爲 99;

其中 我確定最早執行!,這個打印我理解的是:組件被調用了,React 判斷是否須要渲染。而後纔有了上面的一系列步驟,若是理解有誤還請幫忙指出。

設置依賴

實際應用中,咱們不須要在每次組件更新時,都去執行某些 effects,這個時候咱們能夠給 useEffect 設置依賴,告訴 React 何時去執行 useEffect

看下面這個例子,只有在 name 發生改變時,纔會執行這個 useEffect。若是將依賴設置爲空數組,那麼這個 useEffect 只會執行一次。

useEffect(() => {
  document.title = 'Hello, ' + name
}, [name])
複製代碼

正確地設置依賴

引出問題:首先需求很簡單,經過定時器,每過一秒就將 count 的值累加 1。

const [count, setCount] = useState(0)

useEffect(() => {
  const id = setInterval(() => {
    setCount(count + 1)
}, 1000)
  return () => clearInterval(id)
}, [])
複製代碼

咱們只但願設置一次 setInterval 定時器,因此將依賴設置爲了 [],可是因爲組件每次渲染擁有獨立的 state 和 effects,因此上面代碼中的 count 值,一直是 0,當一次執行完 setCount後,後續的setCount操做都是無效的。

那既然這樣,咱們能夠在依賴裏面添加依賴 count就能夠解決問題了吧?思路是正確的,可是這樣就違背了咱們 「咱們只但願 setInterval 執行一次」 的初衷,且極可能形成一些沒必要要的bug。

解決方案:使用函數式更新(前面有講到的哦)。

const [count, setCount] = useState(0)

useEffect(() => {
  const id = setInterval(() => {
    setCount(preCount +> preCount + 1)
}, 1000)
  return () => clearInterval(id)
}, [])
複製代碼

可是在實際應用中,這種方式還遠遠不能知足咱們的需求。好比在依賴多個數據的時候:

function Counter() {
  const [count, setCount] = useState(0)
  const [step, setStep] = useState(1)

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + step)
    }, 1000);
    return () => clearInterval(id)
  }, [step])

  return (
    <>
      <h1>{count}</h1>
      <input value={step} onChange={e => setStep(Number(e.target.value))} />
    </>
  )
}
複製代碼

當咱們在修改 step 變量時,會從新設置定時器。這是咱們不肯意看到的,那應該怎麼去優化呢?這個時候咱們就須要用到useReducer了。

當咱們想更新一個狀態,而且這個狀態更新依賴於另外一個狀態的值時,咱們可能須要使用useReducer去替換它們。

import React, { useReducer, useEffect } from 'react'
import ReactDOM from 'react-dom'

const initialState = {
  count: 0,
  step: 1,
}

function reducer(state, action) {
  const { count, step } = state
  if (action.type === 'tick') {
    return { count: count + step, step }
  } else if (action.type === 'step') {
    return { count, step: action.step }
  } else {
    throw new Error()
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState)
  const { count, step } = state

  useEffect(() => {
    const id = setInterval(() => {
      dispatch({ type: 'tick' })
    }, 1000)
    return () => clearInterval(id)
  }, [dispatch])

  return (
    <>
      <h1>{count}</h1>
      <input value={step} onChange={e => {
        dispatch({
          type: 'step',
          step: Number(e.target.value)
        })
      }} />
    </>
  )
}
複製代碼

React 會保證 dispatch 在組件的聲明週期內保持不變。

關於函數

useEffect 中調用了定義在外部的函數時,咱們可能會遺漏依賴。因此咱們能夠將函數的定義放到useEffect中。

可是當咱們有一些可複用的函數定義在外部,此時應該怎麼處理呢?

  1. 若是這個函數沒有使用組件內的任何值,咱們能夠將它放到組件外部定義。
function getFetchUrl(query) {
  return 'xxx?query=' + query
}

function SearchResults() {
  useEffect(() => {
    const url = getFetchUrl('react')
  }, [])

  useEffect(() => {
    const url = getFetchUrl('redux')
  }, [])
}
複製代碼
  1. 使用 useCallback 包裝。
function SearchResults() {
  const [query, setQuery] = useState('react')

  const getFetchUrl = useCallback(() => {
    return 'xxx?query=' + query
  }, [query])

  useEffect(() => {
    const url = getFetchUrl()
  }, [getFetchUrl])
}
複製代碼

若是 query 保持不變,getFetchUrl也會保持不變,咱們的 effect 也不會從新運行。可是若是 query 修改了,getFetchUrl 也會隨之改變,所以會從新請求數據。

useCallback本質上是添加了一層依賴檢查。它以另外一種方式解決了問題 - 咱們使函數自己只在須要的時候才改變,而不是去掉對函數的依賴。

關於 useReduceruseCallback 的更多內容,我會在後面的筆記中整理出來。

useContext

useContext的使用場景。

在一個典型的 React 應用中,數據是經過 props 屬性自上而下(由父及子)進行傳遞的,但這種作法對於某些類型的屬性而言是極其繁瑣的(例如:地區偏好,UI 主題),這些屬性是應用程序中許多組件都須要的。Context 提供了一種在組件之間共享此類值的方式,而沒必要顯式地經過組件樹的逐層傳遞 props。(引用自官網)

直接看一個例子吧~

  1. 建立頂層組件 Container
import React, { useState, createContext } from 'react'
import Child1 from './Child1'
import Child2 from './Child2'

// 建立一個 Context 對象
export const ContainerContext = createContext({})

function Container() {
  const [state, setState] = useState({child1Color: 'pink', child2Color: 'skyblue'})
  const changeChild1Color = () => {
    setState({
      ...state,
      child1Color: 'lightgreen'
    })
  }

  return (
    <>
      <ContainerContext.Provider value={state}>
        <Child1></Child1>
        <Child2></Child2>
      </ContainerContext.Provider>
      <button onClick={changeChild1Color}>修改child1顏色</button>
    </>
  )
}

export default Container
複製代碼
  1. 建立子組件Child1
import React, {useContext} from 'react'
import { ContainerContext } from './Container'

function Child1() {
  const value = useContext(ContainerContext)
  return <h1 style={{color: value.child1Color}}>我是Child1組件</h1>
}

export default Child1
複製代碼
  1. 建立子組件Child2
import React, {useContext} from 'react'
import { ContainerContext } from './Container'

function Child2() {
  const value = useContext(ContainerContext)
  return <h1 style={{color: value.child2Color}}>我是Child2組件</h1>
}

export default Child2
複製代碼

咱們能夠經過這個簡單的 demo 來了解一下 useContext 相關的基礎知識。

基本使用方法分析:

  1. 經過 React 提供的 createContext 方法建立一個 Context 對象。

    Container組件中,經過export const ContainerContext = createContext({})建立了一個 Context 對象,並設置了默認值爲 {}

    注意:默認值只有在組件所處的樹中沒有匹配到 Provider 時,默認值纔會生效。

    稍微修改一下 Container 中返回的組件,這個時候 Child1Child2 讀取的 context 就是建立 Context 對象時的默認值了。

    <>
      <Child1></Child1>
      <Child2></Child2>
      <button onClick={changeChild1Color}>修改child1顏色</button>
    </>
    複製代碼
  2. 每一個 Context 對象都會返回一個 Provider React 組件,它容許消費組件訂閱 context 的變化。

    Container 組件中的 Context 對象返回 ContainerContext.Provider 組件,它接收一個value 屬性,傳遞給消費組件。同時包裹在其內部的消費組件(Child1Child2)能夠訂閱 context 的變化。

    一個 Provider React 組件能夠和多個消費組件有對應關係。多個 Provider React 組件 也能夠嵌套使用,裏層的會覆蓋外層的數據。

  3. 在消費組件中,使用 useContext 訂閱 context。

    注意useContext 的參數必須是 context 對象自己。

基本使用方式就是這樣了,因爲useContext我用的還比較少,這裏就先不作過多的介紹了。

值得注意的是:

  1. 訂閱了 context 的組件,總會在 context 值變化時從新渲染。若是重渲染組件的開銷較大,能夠經過使用 memoization 來優化。
  2. 使用 Context 必定程度上會使組件的複用性下降,咱們須要合理的取捨。

總結

學習的過程當中,我常常會有種錯覺:我會了。其實這種「會」,也只是對某個知識點的「眼熟」。真正須要動手去完成的時候,就會發現一頭霧水。就像剛開始接觸前端的時候,看到別人的代碼,總會恍然大悟,但本身卻寫不出來同樣。再加上不少知識點學過的那兩天能夠記得,可是一段時間不用就會遺忘,又要從新學習。

因此我想要改變這種困境,經過整理本身的學習過程,加深印象的同時也方便之後查閱。

但願這篇文章對你一樣也有所幫助,若是有建議歡迎留言~

囉嗦了這麼多,小夥伴們給我留個贊吧,謝謝~

參考文章:

useEffect 完整指南
react 官網

相關文章
相關標籤/搜索