阅读 860

猛男必看!我用不足 20 行代码代替了 react-redux

本文假设你具有以下知识使用经验:

  • React >= 16.9
  • React Hooks, 主要是 useContext 和 useReducer
  • reduxreact-redux

我是东墨, 如需新的工作机会, 请联系我 👉 dongmo.cl#alibaba-inc.com

前言

React 16 发布以后, 在提供的 Hooks

API
中, 有一个 React.useReducer, 他是 redux 中 reducer 概念在 Hooks API 上的体现.

本文将展示, 在不引入单独的的 redux 库的情况下, 如何用 .useReducer 将你已有的 reducer 为新的 React App 提供类似react-redux 的 store 注入能力.

阅读本文前, 你可以提前预览 Live Demo. (可能需要科学上网)

Hooks API

useContext

useContext 对应到 react < 16.8 中的 Context 概念, 如下:

import React from 'react'
import ReactDOM from 'react-dom'

const FooContext = React.createContext({ foo: 'bar' });

const FooComponent = () => {
    const fooCtx = React.useContext(FooContext);

    // use fooCtx.foo as you like, here we just `console.log` it
    console.log(fooCtx.foo)

    return <></>
}

const App = () => {
    return (
        <FooContext.Provider value={ foo: 'bar from app' }>
            <FooComponent />
        </FooContext.Provider>
    )
}

ReactDOM.render(<App />, document.getElementById('#app'))
复制代码
Copy

FooComponent 组件里使用了 React.useContext(FooContext), 以从最邻近的祖先 FooContext 中获取上下文的值.

<App /> 中, 我们通过 FooContext.Provider[value], 向其所有的后代组件注入了 value. 因此, 在上述例子中, 我们在 FooComponent 能拿到的 fooCtx.foo'bar from app'.

小结React.createContext 能为你提供一个起来 symbol 作用的 Context 对象, 它指代了具有特定值的上文; React.useContext 则依赖这个 Context, 帮你可以取到来自不同源头的值.

useReducer

useReducer 对应到 redux 中的 reducer 概念, 如下:

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}
复制代码
Copy

无需多言, 这是一个经典的计数器例子.

react-redux 和 redux

我们是怎么在 react 中使用 react-redux 的?

顶层注入:

import React from 'react'
import ReactDOM from 'react-dom'

import { Provider } from 'react-redux'
import store from './store'

import App from './App'

const rootElement = document.getElementById('root')
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  rootElement
)
复制代码
Copy

然后在经过 connect 包装后的组件中使用:

import { connect } from 'react-redux'
import { increment, decrement, reset } from './actionCreators'

// const Counter = ...

const mapStateToProps = (state /*, ownProps*/) => {
  return {
    counter: state.counter
  }
}

const mapDispatchToProps = { increment, decrement, reset }

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Counter)
复制代码
Copy

上面 <Provider /> 的作用, 无非是给 <App /> 及其后代组件提供了一个上下文, 这是不是很像 React.useContext().Provider?

另一方面, connect 起到的核心作用, 也就是把 redux 产生的中心状态的副本用某种不简洁的方式注入到了 中

题外话
实际上, 在 React <= 15 的时代, 我使用 react-redux 时就觉得 connect 这套动作实在是又臭又长: 你至少要定义 mapStateToProps, 如果想在改变状态的时候优雅点, 还得定义 mapDispatchToProps.

react-redux 的核心能力是什么? 归根结底, 是在 React 应用顶层为你

注入
State 对象, 允许你在应用的后代组件中获取到它, 并允许你通过 dispatch 的范式来改变 State —— 继续阅读本文, 你会看到, 只需不足 20 行代码, 我们就可以在 React 16 中用一个更优雅简洁 Hooks API 做到同样的事情.

我们再来看看 redux

下面是一个 redux 的例子:

import { createStore } from 'redux'

function counter(state = 0, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    default:
      return state
  }
}

let store = createStore(counter)

store.subscribe(() => console.log(store.getState()))

store.dispatch({ type: 'INCREMENT' })
store.dispatch({ type: 'INCREMENT' })
store.dispatch({ type: 'DECREMENT' })
复制代码
Copy

我们简单概括下, redux 状态管理的范式 中有三要素:

  • 状态(State)
  • 不可变值(Immutable Value): 提供 State 的副本, 这样, 你无法通过 dispatch 方式以外的方式来更改真正的 State
  • reducer: 一个简单的分流器(你也可以认为它是个状态机), 通过 dispatch 过来的 action.type 分流不同的动作, 你需要根据 action.type 来定制你的业务如何改变 State.

redux 虽然最初因 react 而生, 但它可以用于其它 mvvm 框架, 因为它只是一个通用的中心状态管理器

redux 提供了中心数据管理的三要素, react-redux 提供了将 redux 提供的中心状态注入给 React 组件的能力.

小结

通过上面的讨论, 我们容易得到两个事实:

1. React .useReducer 具备等价于 redux 的三要素提供能力

React 16 的 .useReducer(reducer, state, [lazyInitState]), 其实已经实现了上面两个要素的落地:

  • 它要求你必须提供 reducer 来作为改变状态的分流器
  • 它还要求你必须提供初始的 state(哪怕它是 undefined)

那么"不可变值"呢? 由于 React 自身的组件渲染机制已经决定, 以下对象都是不可变的:

  • 父组件传递给子组件的 props.
  • 通过 React.useContext() 拿到的对象

.useReducer 的返回值是一个元组(也就是具有固定形态的数组) [StateSnapshot, dispatch], 其中:

  • StateSnapshot 就是中心状态的副本
  • dispatch 是 redux 风格的变更函数

注意 不可变值的意味着, 你更改 props[field] 也好, 更改 React.useContext() 返回的对象也好, 都只是在改副本, 并不会影响它实际的中心状态.

2. React 可以通过 .createContext().useContext() 轻松、精准地往 <Provider /> 的所有后代组件中投放不同的值

在上文的例子中我们已经展示了这一点.

useContext + useReducer = Hooks 风格的 react-redux

所以呢?

现在我们把这两个能力结合一下, 将 React.useReducer 返回值, 传给 .useContext().Providervalue 属性, 不就可以在这个 Provider 的所有后代组件中拿到 reducer 的状态副本dispatch 了吗?

我们将上面的本文最初介绍 .useContext 时的例子改一下, 改成一个计数器组件:

// use-redux-store.js
import React from 'react'

export const FooContext = React.createContext();

export const FooCtxProvider = ({ children }) => {
    const reducer = React.useReducer(
        // reducer
        (state, action) => {
            switch (action.type) {
            case "increment":
                return { ...state, count: state.count + 1 };
            case "decrement":
                return { ...state, count: state.count - 1 };
            default:
                return state;
            }
        },
        // initial state
        {
            count: 0
        }
    )

    return (
        <FooContext.Provider value={reducer}>
            {children}
        </FooContext.Provider>
    )
}
复制代码
Copy
// app.js
import React from 'react'
import ReactDOM from 'react-dom'

import { FooContext, FooCtxProvider } from './use-redux-store.js'

const FooComponent = () => {
    const [reduxState, dispatch] = React.useContext(FooContext);

    return (
        <div>
            <p>count: {reduxState.count}</p>
            <button onClick={() => dispatch({ type: 'increment' })}>+</button>
            <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
        </div>
    )
}

const App = () => {
    return (
        <FooCtxProvider>
            <FooComponent />
        </FooCtxProvider>
    )
}

ReactDOM.render(<App />, document.getElementById('#app'))
复制代码
Copy

现在我们有了一个 FooContextFooCtxProvider, 我们总是成对地使用它: 在应用上层让 <FooCtxProvider /> 提供 reducer, 在应用的后代组件中通过 React.useContext(FooContext) 来获取 reducer, 再操作 reducer 去 get 或 update 其中心状态.

通用的 useReduxStore

上一节的 use-redux-store.js 导出的对象都太特别了, 我们希望通用一点, 抽出一个可定制 State, reducer 的 Hooks 风格的 API:

// use-redux-store.js
import React, { useContext, useReducer } from "react";

export default function useReduxStore(reducer, initState) {
  const StateContext = React.createContext();

  const useReduxState = () => useContext(StateContext);

  const inject = function(TargetComponent) {
    return props => (
        // eslint-disable-next-line
        <StateContext.Provider value={useReducer(reducer, initState)}>
            <TargetComponent {...props} />
        </StateContext.Provider>
    );
  };

  return { useReduxState, inject };
}
复制代码
Copy
// app.js
import React from 'react'
import ReactDOM from 'react-dom'

import useReduxStore from './use-redux-store.js'

const { inject, useReduxState } = useReduxStore(
  // reducer
  (state, action) => {
    switch (action.type) {
      case "increment":
        return { ...state, count: state.count + 1 };
      case "decrement":
        return { ...state, count: state.count - 1 };
      default:
        return state;
    }
  },
  // initial state
  {
    count: 0
  }
)

const App = inject(() => {
  const [{ count }, dispatch] = useReduxState();
  return (
    <div>
      <p>count: {count}</p>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
    </div>
  );
})

ReactDOM.render(<App />, document.getElementById('#app'))
复制代码
Copy

以上例子可以在这里 看到. 你可以看到, use-redux-store.js 真的没有 20 行:

例子

其它的注意点

在以上示例代码中, React.createContext({ foo: 'bar' }) 创建了一个具有默认值的 Context 对象 FooContext. 有有默认值, 意味着在之后尝试从 FooContext 中获取 value 的时候, 即便在上下文中无 FooContext.Provider[value] 来提供值的情况下, 依然可以拿到默认值(即 { foo: 'bar' }). 对于某些对 Context 使用有严格要求场景, 这个特点能增强应用的健壮性.

总结

一言以蔽之, React.useContext(...).ProviderReact.useReducer(...) 的结果提供给了其后代组件.

如果你懒得自己再实现一遍上述过程, 也可以使用这个小包 @richardo2016/use-redux-store, 参考这里 使用