本文假设你具有以下知识使用经验:
- React >= 16.9
- React Hooks, 主要是 useContext 和 useReducer
redux和react-redux
我是东墨, 如需新的工作机会, 请联系我 👉 dongmo.cl#alibaba-inc.com
前言
React 16 发布以后, 在提供的 Hooks
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'))
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>
</>
);
}
无需多言, 这是一个经典的计数器例子.
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
)
然后在经过 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)
上面 <Provider /> 的作用, 无非是给 <App /> 及其后代组件提供了一个上下文, 这是不是很像 React.useContext().Provider?
另一方面, connect 起到的核心作用, 也就是把 redux 产生的中心状态的副本用某种不简洁的方式注入到了 中
react-redux 时就觉得 connect 这套动作实在是又臭又长: 你至少要定义 mapStateToProps, 如果想在改变状态的时候优雅点, 还得定义 mapDispatchToProps.react-redux 的核心能力是什么? 归根结底, 是在 React 应用顶层为你
我们再来看看 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' })
我们简单概括下, 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().Provider 的 value 属性, 不就可以在这个 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>
)
}
// 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'))
现在我们有了一个 FooContext 和 FooCtxProvider, 我们总是成对地使用它: 在应用上层让 <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 };
}
// 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'))
以上例子可以在这里 看到. 你可以看到, use-redux-store.js 真的没有 20 行:
其它的注意点
在以上示例代码中, React.createContext({ foo: 'bar' }) 创建了一个具有默认值的 Context 对象 FooContext. 有有默认值, 意味着在之后尝试从 FooContext 中获取 value 的时候, 即便在上下文中无 FooContext.Provider[value] 来提供值的情况下, 依然可以拿到默认值(即 { foo: 'bar' }). 对于某些对 Context 使用有严格要求场景, 这个特点能增强应用的健壮性.
总结
一言以蔽之, React.useContext(...).Provider 将 React.useReducer(...) 的结果提供给了其后代组件.
如果你懒得自己再实现一遍上述过程, 也可以使用这个小包 @richardo2016/use-redux-store, 参考这里 使用
