使用 React Hooks + Context 打造簡版 Redux

做者: 陳俊生javascript

React Hooks 在 React@16.8 版本正式發佈。我最近在一兩個公司的內部項目中也開始用起來嚐嚐鮮。html

不瞭解 Hooks 的同窗先擼一遍文檔。本文不對 Hooks 作詳細介紹,只闡述一種使用 Hooks 的思路。java

通常咱們寫 React 若是不是特別大的應用,先後端數據交互邏輯不復雜,這樣咱們直接按照正常流程寫組件就能知足簡單的業務場景。隨着業務場景的深刻漸漸地咱們組件變大變多,組件與組件之間的數據通信(也就是狀態管理,不過我更願意稱之爲數據通信)變得愈來愈複雜。因此咱們引入了 Redux 來維護咱們日趨複雜的數據通信。react

思路

秉承着這種思路,我在開發應用的時候是沒有一開始就引入 Redux ,由於一開始我以爲就是個小項目。隨着深刻項目的開發,其實並無這麼簡單。redux

可是也沒有太複雜,這時我把眼光放到了 Context 身上。Context 本意是上下文,它提供一個 Provider 和一個 Consumer,也就是生產者/消費者模式,在某個頂層提供一個 Provider ,下面的子元素經過 Consumer 來消費 Provider 裏的數據和方法。小程序

經過這個概念,咱們把不一樣層級裏的組件共享同一個頂層 Provider,而且組件內部使用 Consumer 來消費共享數據。後端

當咱們能共享數據後,還剩一個問題就是如何更改 Provider 裏的數據呢?答案是:useReducer異步

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

實例

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

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>
  )
}

複製代碼

深刻

咱們能夠更進一步,讓咱們的狀態管理機制更加精簡。

首先,在某個組件層級定義咱們須要的 Context 。假如,咱們這裏是在頂層(也就是全局的狀態管理)。

import React from 'react'

// 建立咱們須要的 Context
export const AppContext = React.createContext(null)
複製代碼

而後咱們將 useReducer 的返回值直接傳給 AppContext.Provider

import React, { useReducer } from 'react'

// 全局 Provider
export function AppProvider ({reducer, initValue, children}) {
  return (
    <AppContext.Provider value={useReducer(reducer, initValue)}> {children} </AppContext.Provider> ) } 複製代碼

最後,添加一個自定義 hooks 來獲取 AppContext 裏的狀態和方法。Write Once, Run Anywhere :)

import React, { useReducer, useContext } from 'react'

export const useAppState = () => useContext(AppContext)
複製代碼

最後咱們的 state.js 完整代碼以下:

import React, { useContext, useReducer } from 'react'

export const AppContext = React.createContext(null)

export function AppProvider ({reducer, initValue, children}) {
  return (
    <AppContext.Provider value={useReducer(reducer, initValue)}> {children} </AppContext.Provider> ) } export const useAppState = () => useContext(AppContext) 複製代碼

組件裏使用:

import { AppProvider, useAppState } from "./state"

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

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

  return (
    <AppProvider initValue={initState} reducer={reducer}> <div> {/* 僞裝這些地方有着不一樣的層級 */} <Child1 /> <Child2 /> </div> </AppProvider>
  )
}

function Child1(props) {
  const [state, dispatch] = useAppState()

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

完整的代碼及例子見 tiny redux

結語

這樣小型的狀態管理機制你甚至能夠放在某個組件裏,而不用放到如 Redux 全局的環境中去。這樣使得咱們寫的應用更加靈活,而不是一味的往 store 裏丟狀態。固然你也能夠寫一個 AppProvider 來管理全局的狀態,React Hooks + Context 給了咱們這樣的便利。

Hooks 真香!

小程序也可使用 hooks 開發,更多瞭解《使用 React Hooks 重構你的小程序》

相關文章
相關標籤/搜索