原文 地址对于想要跳过文章直接看结果的人,我已经把我写的内容制做成了一个库:use-simple-state,无任何依赖(除了依赖 react ),只有3kb,至关轻量。javascript
const state = {}; export const getState = () => state; export const setState = nextState => { state = nextState; };
一个全局可用的用于展示咱们应用状态的值:state;读取状态的能力:getState;html
更新状态的能力:setState。java
context
API,它可让数据在整个 react 应用可用。context
有两部分:provider (生产者) 和 consumer (消费者),provider 就像它的名字所说的那样,给应用提供 context
(data 数据),消费者意指当咱们读取 context
时,咱们就是消费者。context
:若是说 props
是显式的传送数据,那么 context
就是隐式的传送数据。React.createContext
来建立上下文,它能够给咱们提供 provider 和 consumer。import { createContext } from 'react'; const { Provider, Consumer } = createContext();
import React, { Component, createContext } from 'react'; const { Provider, Consumer } = createContext(); export const StateConsumer = Consumer; export class StateProvider extends Component { static defaultProps = { state: {} }; state = this.props.state; render () { return ( <Provider value={this.state}> {this.props.children} </Provider> ); } }
StateProvider
是接收一个 state
来做为初始状态的组件,而且使组件树中当前组件下面的任何组件均可以访问到这个属性。若是没有提供 state
,默认会有一个空对象代替。StateProvider
包裹住根组件:import { StateProvider } from './stateContext'; import MyApp from './MyApp'; const initialState = { count: 0 }; export default function Root () { return ( <StateProvider state={initialState}> <MyApp /> </StateProvider> ); }
MyApp
的任何地方得到应用的状态。在这里咱们会初始化咱们的状态为一个有一个 count
属性的对象,因此不管何时咱们想要当即获取应用的状态,咱们就能够从这里得到。StateConsumer
的子组件的例子来查看。state
参数传递给函数用以展示咱们应用的当前状态,做为咱们的初始状态,state.count
为 0.import { StateConsumer } from './stateContext'; export default function SomeCount () { return ( <StateConsumer> {state => ( <p> Count: {state.count} </p> )} </StateConsumer> ); }
StateConsumer
咱们须要知道的很重要一点是在上下文中它会自动订阅状态的改变,所以当咱们的状态改变后会从新渲染以显示更新。这就是消费者的默认行为,咱们暂时还没作可以用到这个特性的功能。StateProvider
里面更新状态。StateProvider
传递了一个 state
属性,也就是以后会传递给组件的 state
属性。咱们将使用 react 内置的 this.setState
来更新:export class StateProvider extends Component { static defaultProps = { state: {} }; state = this.props.state; render () { return ( <Provider value={{ state: this.state, setState: this.setState.bind(this) }}> {this.props.children} </Provider> ); }
this.setState
,这意味着咱们须要稍微改变咱们的上下文传值,不仅是传递 this.state
,咱们如今同时传递 state
和 setState
。StateConsumer
时能够用解构赋值获取 state
和 setState
,而后咱们就能够读写咱们的状态对象了:export default function SomeCount () { return ( <StateConsumer> {({ state, setState }) => ( <> <p> Count: {state.count} </p> <button onClick={() => setState({ count: state.count + 1 })}>
</button> <button onClick={() => setState({ count: state.count - 1 })}> - 1 </button> </> )} </StateConsumer> );
}react
* 有一点要注意的是因为咱们传递了 react 内置的 `this.setState` 做为咱们的 `setState` 方法,新增的属性将会和已有的状态合并。这意味着若是咱们有 `count` 之外的第二个属性,它也会被自动保存。 * 如今咱们的做品已经能够用在真实项目中了(尽管还不是颇有效率)。对 react 开发者来讲应该会以为 API 很熟悉,因为使用了内置的工具所以咱们没有引用任何新的依赖项。假如以前以为状态管理有点神奇,但愿如今咱们多少可以了解它内部的结构。 ## 华丽的点缀 * 熟悉 redux 的人可能会注意到咱们的解决方案缺乏一些特性: >* 没有内置的处理反作用的方法,你须要经过 [redux 中间件](https://redux.js.org/advanced/middleware)来作这件事 >* 咱们的 `setState` 依赖 react 默认的 `this.setState` 来处理咱们的状态更新逻辑,当使用内联方式更新复杂状态时将可能引起混乱,同时也没有内置的方法来复用状态更新逻辑,也就是 [redux reducer](https://redux.js.org/basics/reducers) 提供的功能。 >* 也没有办法处理异步的操做,一般由 [redux thunk](https://github.com/reduxjs/redux-thunk) 或者 [redux saga](https://github.com/redux-saga/redux-saga)等库来提供解决办法。 >* 最关键的是,咱们没办法让消费者只订阅部分状态,这意味着只要状态的任何部分更新都会让每一个消费者更新。 * 为了解决这些问题,咱们模仿 redux 来应用咱们本身的 **actions**,**reducers**,和 **middleware**。咱们也会为异步 actions 增长内在支持。以后咱们将会让消费者只监听状态内的子状态的改变。最后咱们来看看如何重构咱们的代码以使用新的 [**hooks api**](https://reactjs.org/docs/hooks-intro.html)。 ## redux 简介 > 免责声明:接下来的内容只是为了让你更容易理解文章,我强烈推荐你阅读 [redux 官方](https://redux.js.org/introduction/motivation)完整的介绍。 > > 若是你已经很是了解 redux,那你能够跳过这部分。 * 下面是一个 redux 应用的数据流简化流程图:  * 如你所见,这就是单向数据流,从咱们的 reducers 接收到状态改变以后,触发 actions,数据不会回传,也不会在应用的不一样部分来回流动。 * 说的更详细一点: * 首先,咱们触发一个描述改变状态的 action,例如 `dispatch({ type: INCREMENT_BY_ONE })` 来加1,同咱们以前不一样,以前咱们是经过 `setState({ count: count + 1 })`来直接改变状态。 * action 随后进入咱们的中间件,redux 中间件是可选的,用于处理 action 反作用,并将结果返回给 action,例如,假如在 action 到达 reducer 以前触发一个 `SIGN_OUT` 的 action 用于从本地存储里删除全部用户数据。若是你熟悉的话,这有些相似于 [express](https://expressjs.com/) 中间件的概念。 * 最后,咱们的 action 到达了接收它的 reducer,伴随而来的还有数据,而后利用它和已有的状态合并生成一个新的状态。让咱们触发一个叫作 `ADD` 的 action,同时把咱们想发送过去增长到状态的值也发送过去(叫作 payload )。咱们的 reducer 会查找叫作 `ADD` 的 action,当它发现后就会将 payload 里面的值和咱们现有的状态里的值加到一块儿并返回新的状态。 * reducer 的函数以下所示: * ```javascript (state, action) => nextState
// Action types const ADD_ONE = 'ADD_ONE'; const ADD_N = 'ADD_N'; // Actions export const addOne = () => ({ type: ADD_ONE }); export const addN = amount => ({ type: ADD_N, payload: amount });
dispatch
的占位符函数,咱们的占位符只是一个空函数,将会被用于替换上下文中的 setState
函数,咱们一会再回到这儿,由于咱们还没作接收 action 的 reducer 呢。export class Provider extends React.PureComponent { static defaultProps = { state: {} }; state = this.props.state; _dispatch = action => {}; render () { return ( <StateContext.Provider value={{ state: this.state, dispatch: this._dispatch }}> {this.props.children} </StateContext.Provider> ); } }
(state, action) => nextState
Array.reduce
方法来迭代数组,最终生成咱们的新状态:export class Provider extends React.PureComponent { static defaultProps = { state: {}, reducers: [] }; state = this.props.state; _dispatch = action => { const { reducers } = this.props; const nextState = reducers.reduce((state, reducer) => { return reducer(state, action) || state; }, this.state); this.setState(nextState); }; render () { return ( <StateContext.Provider value={{ state: this.state, dispatch: this._dispatch }}> {this.props.children} </StateContext.Provider> ); } }
this.setState
来更新 StateProvider
组件的状态。function countReducer ({ count, ...state }, { type, payload }) { switch (type) { case ADD_N: return { ...state, count: count + payload }; case ADD_ONE: return { ...state, count: count + 1 }; } }
action.type
,而后假如匹配到以后将会更新相对应的状态,不然就会在通过 switch
判断语句以后返回函数默认的 undefined
。咱们的 reducer 和 redux 的 reducer 的一个重要的区别在当咱们不想更新状态时,通常状况下咱们会由于未匹配到 action type 而返回一个falsy 值,而 redux 则会返回未变化的状态。StateProvider
:export default function Root () { return ( <StateProvider state={initialState} reducers={[countReducer]}> <MyApp /> </StateProvider> ); }
export default function SomeCount () { return ( <StateConsumer> {({ state, dispatch }) => ( <> <p> Count: {state.count} </p> <button onClick={() => dispatch(addOne())}>
</button> <button onClick={() => dispatch(addN(5))}> + 5 </button> <button onClick={() => dispatch(addN(10))}> + 10 </button> </> )} </StateConsumer> );
## 中间件 * 如今咱们的做品已经跟 redux 比较像了,只须要再增长一个处理反作用的方法就能够。为了达到这个目的,咱们须要容许用户传递中间件函数,这样当 action 被触发时就会被调用了。 * 咱们也想让中间件函数帮助咱们处理状态更新,所以假如返回的 `null` 就不会被 action 传递给 reducer。redux 的处理稍微不一样,在 redux 中间件你须要手动传递 action 到下一个紧邻的中间件,假如没有使用 redux 的 `next` 函数来传递,action 将不会到达 reducer,并且状态也不会更新。 * 如今让咱们写一个简单的中间件,咱们想经过它来寻找 `ADD_N` action,若是它找到了那就应当把 `payload` 和当前状态里面的 `count` 加和并输出,可是阻止实际状态的更新。
function countMiddleware ({ type, payload }, { count }) {
if (type === ADD_N) {git
console.log(`${payload} + ${count} = ${payload + count}`); return null;
}
}github
* 跟咱们的 reducer 相似,咱们会将中间件用数组传进咱们的 `StateProvider`。 * ```javascript export default function Root () { return ( <StateProvider state={initialState} reducers={[countReducer]} middleware={[countMiddleware]} > <MyApp /> </StateProvider> ); }
Array.reduce
来得到咱们的值。跟 reducer 相似,咱们也会迭代数组依次调用每一个函数,而后将结果赋值给一个变量 continueUpdate
。StateProvider
里面找到 middleware
属性,咱们会将 continueUpdate
置为默认的 undefined
。咱们也会增长一个 middleware
数组来做默认属性,这样的话 middleware.reduce
就不会由于没传东西而抛出错误。export class StateProvider extends React.PureComponent { static defaultProps = { state: {}, reducers: [], middleware: [] }; state = this.props.state; _dispatch = action => { const { reducers, middleware } = this.props; const continueUpdate = middleware.reduce((result, middleware) => { return result !== null ? middleware(action, this.state) : result; }, undefined); if (continueUpdate !== null) { const nextState = reducers.reduce((state, reducer) => { return reducer(state, action) || state; }, this.state); this.setState(nextState); } }; render () { return ( <StateContext.Provider value={{ state: this.state, dispatch: this._dispatch }}> {this.props.children} </StateContext.Provider> ); } }
null
咱们就会跳过剩下的中间件函数,continueUpdate
将为 null
,意味着咱们会中断更新。dispatch
和 state
时调用函数,这样就能够给用户所写的异步 action 执行的机会。拿这个受权 action 做为例子来看下:const logIn = (email, password) => async dispatch => { dispatch({ type: 'LOG_IN_REQUEST' }); try { const user = api.authenticateUser(email, password); dispatch({ type: 'LOG_IN_SUCCESS', payload: user }); catch (error) { dispatch({ type: 'LOG_IN_ERROR', payload: error }); } };
logIn
的 action 建立器,不是返回一个对象,它返回一个接收 dispatch
的函数,这可让用户在一个异步 API 请求的前面和后面触发异步 action,根据 API 不一样的返回结果触发不一样的 action,这里咱们在发生错误时发送一个错误 action。StateProvider
里的 _dispatch
方法里检查 action
的类型是否是 function
:export class StateProvider extends React.PureComponent { static defaultProps = { state: {}, reducers: [], middleware: [] }; state = this.props.state; _dispatch = action => { if (typeof action === 'function') { return action(this._dispatch, this.state); } const { reducers, middleware } = this.props; const continueUpdate = middleware.reduce((result, middleware) => { return result !== null ? middleware(action, this.state) : result; }, undefined); if (continueUpdate !== null) { const nextState = reducers.reduce((state, reducer) => { return reducer(state, action) || state; }, this.state); this.setState(nextState); } }; render () { return ( <StateContext.Provider value={{ state: this.state, dispatch: this._dispatch }}> {this.props.children} </StateContext.Provider> ); } }
action
为函数,传入 this.state
,这样用户能够访问异步 action 中已有的状态,咱们也会返回函数调用的结果,容许开发者从他们的异步 action 中得到一个返回值从而开启更多的可能性,例如从 dispatch
触发的 promise 链。connect
高阶组件,它提供了一个映射函数 — mapStateToProps
— 仅仅只在关联的 mapStateToProps
的输出改变时(只映射从如今开始的状态)才会触发组件的从新渲染。若是不这样的话,那么每次状态更新都会让组件使用 connect 来订阅存储改变而后从新渲染。mapState
前面的输出,这样咱们就能够比较二者看看有没有差别来决定咱们是否须要继续向前和从新渲染咱们的组件。为了作到这一点咱们须要使用一种叫作记忆化的进程,跟咱们这行的许多事情同样,对于一个想到简单的进程来讲,这是一个重要的词,尤为是咱们可使用 React.Component
来存储咱们状态的子状态,而后仅在咱们检测到 mapState
的输出改变以后再更新。shouldComponentUpdate
可让咱们达到目的。它将接收到的 props 和 state 当作参数来让咱们同现有的 props 和 state 进行比较,若是咱们返回 true
那么更新继续,若是返回 false
那么 react 将会跳过渲染。class ConnectState extends React.Component { state = this.props.mapState(this.props.state); static getDerivedStateFromProps (nextProps, nextState) {} shouldComponentUpdate (nextProps) { if (!Object.keys(this.state).length) { this.setState(this.props.mapDispatch(this.props.state)); return true; } console.log({ s: this.state, nextState: nextProps.mapState(nextProps.state), state: this.props.mapState(this.state) }); return false; } render () { return this.props.children({ state: this.props.state, dispatch: this.props.dispatch }); } } export function StateConsumer ({ mapState, mapDispatch, children }) { return ( <StateContext.Consumer> {({ state, dispatch }) => ( <ConnectState state={state} dispatch={dispatch} mapState={mapState} mapDispatch={mapDispatch}> {children} </ConnectState> )} </StateContext.Consumer> ); }
getDerivedStateFromProps
和 shouldComponentUpdate
,它也接收一个 render 属性来做为子组件,就像默认的消费者同样。咱们也会经过使用传递的 mapState
函数来初始化咱们的消费者初始状态。shouldComponentUpdate
将只会在接收到第一次状态更新以后渲染一次。以后它会记录传进的状态和现有的状态,而后返回 false
,阻止任何更新。shouldComponentUpdate
内部也调用了 this.setState
,而咱们都知道 this.setState
老是会触发从新渲染。因为咱们也会从 shouldComponentUpdate
里返回 true
,这会产生一次额外的从新渲染,因此为了解决这个问题,咱们将使用生命周期 getDerivedStateFromProps
来获取咱们的状态,而后咱们再使用 shouldComponentUpdate
来决定基于咱们获取的状态是否继续更新进程。this.state
对象以致组件跳过更新:function shallowCompare (state, nextState) { if ((typeof state !== 'object' || state === null || typeof nextState !== 'object' || nextState === null)) return false; return Object.entries(nextState).reduce((shouldUpdate, [key, value]) => state[key] !== value ? true : shouldUpdate, false); }
shallowCompare
函数,实际上只是调用并检查结果。若是它返回 true
,咱们就返回 true
来容许组件从新渲染,不然咱们就返回 false
来跳过更新(而后咱们得到的状态被放弃掉)。咱们也想在 mapDispatch
存在的时候调用它。class ConnectState extends React.Component { state = {}; static getDerivedStateFromProps ({ state, mapState = s => s }) { return mapState(state); } shouldComponentUpdate (nextProps, nextState) { return shallowCompare(this.state, nextState); } render () { return this.props.children({ state: this.state, dispatch: this.props.mapDispatch ? this.props.mapDispatch(this.props.dispatch) : this.props.dispatch }); } }
mapState
函数让咱们消费者以只匹配咱们的部分状态,这样咱们就会将它做为一个属性传给咱们的 StateConsumer
:return ( <StateConsumer mapState={state => ({ greeting: state.greeting })} mapDispatch={dispatch => dispatch}> // ...
greeting
里面的改变,所以假如咱们更新了组件里的 count
将会被咱们的全局状态改变所忽略而且避免了一次从新渲染。若是你还没听过它,它正在快速变成 react 的下一个大特性。这里有一段来自官方描述的简单解释:web
Hooks 是一个让你没必要写 class 就可使用 state 和 react 其余特性的新功能。
咱们来看一个使用基本的 useState
钩子的例子来看看它们是如何工做的:express
import React, { useState } from 'react'; function Counter () { const [count, setCount] = useState(0); return ( <> <span> Count: {count} </span> <button onClick={() => setCount(count + 1)}> Increment </button> </> ); }
useState
传递一个 0 来初始化新状态,它会返回咱们的状态:count
,以及一个更新函数 setCount
。若是你之前没见过的话,可能会奇怪 useState
是如何不在每次渲染时初始化,这是由于 react 在内部处理了,所以咱们无需担忧这一点。useReducer
钩子来从新应用咱们的 provider,就像咱们正在作的同样,除了将 action 触发到一个得到新状态的 reducer,它就像 useState
同样工做。知道了这个,咱们只须要将 reducer 的逻辑从老的 StateProvider
拷贝到新的函数组件 StateProvider
里就能够了:redux
export function StateProvider ({ state: initialState, reducers, middleware, children }) { const [state, dispatch] = useReducer((state, action) => { return reducers.reduce((state, reducer) => reducer(state, action) || state, state); }, initialState); return ( <StateContext.Provider value={{ state, dispatch }}> {children} </StateContext.Provider> ); }
能够如此的简单,可是当咱们想要保持简单时,咱们仍然没有彻底掌握 hooks 的全部能力。咱们也可使用 hooks 来把咱们的 StateConsumer
换为咱们本身的钩子,咱们能够经过包裹 useContext
钩子来作到:api
const StateContent = createContext(); export function useStore () { const { state, dispatch } = useContext(StateContext); return [state, dispatch]; }
Provider
和 Consumer
,可是此次咱们会将它存到咱们传递到 useContext
单个变量从而让咱们可不用 Consumer
就能够接入上下文。咱们也将它命名为咱们本身的 useStore
钩子,由于 useState
是一个默认的钩子。接下来咱们来简单地重构下咱们消费上下文数据的方法:
export default function SomeCount () { const [state, dispatch] = useStore(); return ( <> <p> Count: {state.count} </p> <button onClick={() => dispatch(addOne())}>
</button> <button onClick={() => dispatch(addN(5))}> + 5 </button> <button onClick={() => dispatch(addN(10))}> + 10 </button> </> );
}
* 但愿这些例子能让你感觉到 hooks 是如何的直观、简单和有效。咱们减小了所需的代码数量,而且给了咱们一个友好、简单的 API 供使用。 * 咱们也想让咱们的中间件和内置的异步 action 从新开始工做。为了作到这一点,咱们将咱们的 `useReducer` 包裹进一个自定义钩子,在咱们的 `StateProvider` 中被特殊的使用,而后简单地重用咱们老的状态组件的逻辑就行了。 * ```javascript export function useStateProvider ({ initialState, reducers, middleware = [] }) { const [state, _dispatch] = useReducer((state, action) => { return reducers.reduce((state, reducer) => reducer(state, action) || state, state); }, initialState); function dispatch (action) { if (typeof action === 'function') { return action(dispatch, state); } const continueUpdate = middleware.reduce((result, middleware) => { return result !== null ? middleware(action, state) : result; }, undefined); if (continueUpdate !== null) { _dispatch(action); } } return { state, dispatch }; }
continueUpdate !== null
咱们就继续更新状态。咱们也不会改变处理异步 action 的方式。useStateProvider
的结果和它的参数到咱们的 provider,这咱们以前没怎么考虑:export function StateProvider ({ state: initialState, reducers, middleware, children }) { return ( <StateContext.Provider value={useStateProvider({ initialState, reducers, middleware })}> {children} </StateContext.Provider> ); }
在开始的时候提到过,我写了一个库, Use Simple State,看完这篇文章以后,你能够在个人 github页面看到,我已经使用 hooks 最终实现了,包括几个新增的功能。