使用 React Hooks + Context 打造簡版 Redux

黃金大鍵客發表於2019-04-04

原文地址:github.com/hentaicrack…

React Hooks 在 React@16.8 版本正式釋出。我最近在一兩個公司的內部專案中也開始用起來嚐嚐鮮。

不瞭解 Hooks 的同學先擼一遍文件。本文不對 Hooks 做詳細介紹,只闡述一種使用 Hooks 的思路。

一般我們寫 React 如果不是特別大的應用,前後端資料互動邏輯不復雜,這樣我們直接按照正常流程寫元件就能滿足簡單的業務場景。隨著業務場景的深入漸漸地我們元件變大變多,元件與元件之間的資料通訊(也就是狀態管理,不過我更願意稱之為資料通訊)變得越來越複雜。所以我們引入了 Redux 來維護我們日趨複雜的資料通訊。

思路

秉承著這種思路,我在開發應用的時候是沒有一開始就引入 Redux ,因為一開始我覺得就是個小專案。隨著深入專案的開發,其實並沒有這麼簡單。

但是也沒有太複雜,這時我把眼光放到了 Context 身上。Context 本意是上下文,它提供一個 Provider 和一個 Consumer,這裡和 Angular 裡的 Provider 有點類似,也就是生產者/消費者模式,在某個頂層提供一個 Provider ,下面的子元素通過 Consumer 來消費 Provider 裡的資料和方法。

通過這個概念,我們把不同層級裡的元件共享同一個頂層 Provider,並且元件內部使用 Consumer 來消費共享資料。

當我們能共享資料後,還剩一個問題就是如何更改 Provider 裡的資料呢?答案是:useReducer

好,有了思路,我們來實現一下。

例項

假設我們在某一個層級有個需要共享狀態的父級元素,我們稱它為 Parent,在 Parent 下面不同層級之間有兩個 Child。這裡為了簡單舉例假設兩個 Child 內都是共同的邏輯。

import React from "react"

function Parent() {
  const colors = ['red', 'blue']
  return (
    <>
      <Child1 color={colors[0]} />
      <Child2 color={colors[1]} />
    </>
  )
}

function Child1(props) {
  return (
    <div style={{ background: props.color }}>I am {props.color}</div>
  )
}

function Child2(props) {
  return (
    <div style={{ background: props.color }}>I am {props.color}</div>
  )
}

複製程式碼

我們現在已經構造出了這樣的一個上下級結構,目前通過給子元件傳遞屬性,可以實現父元件的狀態共享。但是這裡如果層級加深,我們傳遞屬性的層級也要跟著加深。這樣顯然不是我們想要的。

現在我們來引入 Context

首先通過 createContext 方法初始化我們需要的 Context

import React, { createContext } from "react"

const Context = createContext({
  colors: ['red', 'blue']
})

複製程式碼

然後我們在 Parent 和 Child 裡引入剛才的 Context,並且使用 useContext 拿到共享的資料:

import React, { useContext, createContext } from "react"

const Context = createContext({
  colors: []
})

function Parent() {
  const initState = {
    colors: ["red", "blue"]
  }

  return (
    <Context.Provider value={{ colors: initState.colors }}>
      <>
        {/* 假裝這些地方有著不同的層級 */}
        <Child1 />
        <Child2 />
      </>
    </Context.Provider>
  )
}

function Child1(props) {
  const { colors } = useContext(Context);

  return (
    <div style={{ background: colors[0] }}>
      I am {colors[0]}
    </div>
  )
}

// 省略 Child2 程式碼,同 Child1 一致
複製程式碼

現在只是拿到了資料並且進行渲染,再進一步,通過點選元素,修改顏色。在這裡我們就需要用 useReducer 來模擬觸發改變。

首先我們需要一個 reducer 來處理觸發的改變。


function reducer(state, action) {
  const { colors } = action
  if (action.type === "CHANGE_COLOR") {
    return { colors: colors }
  } else {
    throw new Error()
  }
}

複製程式碼

這裡我簡化了 action 的處理,當然你也可以進行擴充套件。

現在,我們給 Provider 加上提供改變的方法 dispatch

import React, { useContext, createContext } from "react"

const Context = createContext({
  colors: []
})

function Parent() {
  const initState = {
    colors: ["red", "blue"]
  }

  const [state, dispatch] = useReducer(reducer, initState)

  return (
    <Context.Provider value={{ colors: state.colors, dispatch: dispatch }}>
      <>
        {/* 假裝這些地方有著不同的層級 */}
        <Child1 />
        <Child2 />
      </>
    </Context.Provider>
  )
}

複製程式碼

然後子元件觸發改變:


function Child1(props) {
  const { colors, dispatch } = useContext(Context)

  return (
    <div
      style={{ background: colors[0] }}
      onClick={() =>
        dispatch({
          type: "CHANGE_COLOR",
          colors: ["yellow", "blue"]
        })
      }
    >
      I am {colors[0]}
    </div>
  )
}

// 省略 Child2 程式碼,同 Child1 一致
複製程式碼

至此,這個小型的狀態共享便完成了。這便是我們擺脫 Redux 之後實現的狀態共享思路的雛形。完整的程式碼及例子見 tiny redux

進階

在實際的應用中,我們的業務場景會更復雜,比如我們的資料是動態獲取的。

這種情況下你可以把 Provider 抽出來,當 Parent 資料回來之後再初始化 Context


function Provider (props) {
  const { colors } = props
  const initState = {
    colors,
  }
  const [state, dispatch] = useReducer(reducer, initState)

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

複製程式碼

然後我們在 Parent 中做非同步操作,並把動態資料傳給 Provider :


import React, { useState, useEffect } from "react"

function Parent (props) {
  const [data, setData] = useState()
  const [url, setUrl] = useState('https://example.com')

  useEffect(() => {
    fetch(url).then(res => setData(data))
  }, [url])

  if (!data) return <div>Loading ...</div>

  return (
    <Provider colors={data}>
      <>
        {/* 假裝這些地方有著不同的層級 */}
        <Child1 />
        <Child2 />
      </>
    </Provider>
  )
}

複製程式碼

結語

這樣小型的狀態管理機制你甚至可以放在某個元件裡,而不用放到如 Redux 全域性的環境中去。這樣使得我們寫的應用更加靈活,而不是一味的往 store 裡丟狀態。當然你也可以寫一個 AppProvider 來管理全域性的狀態,React Hooks + Context 給了我們這樣的便利。

Hooks 真香!

相關文章