使用 React Hooks + Context 打造简版 Redux

avatar
全栈开发工程师 @京东

作者: 陈俊生

React Hooks 在 React@16.8 版本正式发布。我最近在一两个公司的内部项目中也开始用起来尝尝鲜。

不了解 Hooks 的同学先撸一遍文档。本文不对 Hooks 做详细介绍,只阐述一种使用 Hooks 的思路。

一般我们写 React 如果不是特别大的应用,前后端数据交互逻辑不复杂,这样我们直接按照正常流程写组件就能满足简单的业务场景。随着业务场景的深入渐渐地我们组件变大变多,组件与组件之间的数据通讯(也就是状态管理,不过我更愿意称之为数据通讯)变得越来越复杂。所以我们引入了 Redux 来维护我们日趋复杂的数据通讯。

思路

秉承着这种思路,我在开发应用的时候是没有一开始就引入 Redux ,因为一开始我觉得就是个小项目。随着深入项目的开发,其实并没有这么简单。

但是也没有太复杂,这时我把眼光放到了 Context 身上。Context 本意是上下文,它提供一个 Provider 和一个 Consumer,也就是生产者/消费者模式,在某个顶层提供一个 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>
  )
}

深入

我们可以更进一步,让我们的状态管理机制更加精简。

首先,在某个组件层级定义我们需要的 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 重构你的小程序》