React Hooks下的全局状态管理
React Hooks
React Hooks 是 React 16.7.0-alpha
版本推出的新特性,旨在解决组件间共享状态逻辑的问题。
- **useState: **允许在 function 组件中,声明和改变状态。在此之前,只有 class 组件可以。(useImmer)
- **useEffect: **允许在 function 组件中,抽象地使用 React 的生命周期函数。开发者可以使用更函数式的、更清晰的 hooks 的方式。
使用 hooks 对带有本地状态的 Avatar 组件进行重构说明:
import React, { useState, useEffect } from 'react';
const Avatar = () => {
// 创建 user 状态和修改状态的函数
const [user, setUser] = useState("马飞");
// 默认 componentDidMount/componentDidUpdate 时会触发回调
// 也可以使用第二个参数,指定触发时机
useEffect(() => {
document.title = 当前用户:${user};
});
// 使用 setUser 改变状态
return <div onClick={() => setUser("mafeifei")}>{user};
};
- useReducer
const [state, dispatch] = useReducer(reducer, initialArg, init);
useState
的替代方案。它接收一个形如 (state, action) => newState
的 reducer,并返回当前的 state 以及与其配套的 dispatch
方法,提供了组件内使用redux的方式管理状态。
以下是用 reducer 重写 useState
一节的计数器示例:
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: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</>
);
}
Redux
作为目前最流行的状态管理框架,Redux在7.1.0版本中也提供了对于Hooks的支持。
API
store和reducer部分和原先保持一致,外层也需要包装Provider
const store = createStore(rootReducer)
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
在获取和更新store数据上,Redux针对Hooks提供了新的API。
useSelector()
const result : any = useSelector(selector : Function, equalityFn? : Function)
// useSelector(state => state.count)
selector概念上和connect的mapStateToProps参数相似,将store中的state作为第一个参数,在每次component render时都会调用。在数据的更新上,useSelector会订阅store,当有action执行时会重新调用selector获取最新的数据。和mapStateToProps不同的是useSelector默认使用===比较更新前后的数据,所以提供了第二个参数equalityFn来定制比较函数。
useDispatch
const dispatch = useDispatch()
useDiapatch返回dispatch的引用,用来调用action。
useStore()
const store = useStore()
useStore会返回Redux中store的引用,可以使用store.getState()获取store中的state。
Demo
reducer可配合immer使用
codesandbox
import React from "react";
import ReactDOM from "react-dom";
import { createStore } from "redux";
import { Provider, useSelector, useDispatch } from "react-redux";
import {produce} from 'immer'
const initState = {
count: 0
};
const reducer = (state = initState, action) => produce(state, draft => {
switch (action.type) {
case "increase": {
draft.count += 1
break
}
case "decrease": {
draft.count += 1
break
}
default:
}
})
const store = createStore(reducer);
const Counter = () => {
const count = useSelector(state => state.count); // useSelector
const dispatch = useDispatch(); // useDispatch
return (
<div>
<h1>Counter: {count}</h1>
<button onClick={() => dispatch({ type: "increase" })}>Increase</button>
<button onClick={() => dispatch({ type: "decrease" })}>Decrease</button>
</div>
)
}
const App = () => {
return (
<Provider store={store}>
<Counter />
</Provider>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<App />,
rootElement
);
New Context API + Hooks
useContext
React Hooks提供了useContext来访问context,可以在使用 Consumer
的时候不必包裹 Children
import React, { useContext } from 'react';
function Display() {
const value = useContext(NumberContext);
return <div>The answer is {value}.</div>;
}
useReducer
useReducer是hooks提供的针对复杂场景替代useState管理状态的API,提供了action和reducer的方式来管理组件内状态,它接收一个形如 (state, action) => newState
的 reducer,并返回当前的 state 以及与其配套的 dispatch
方法。
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: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</>
);
}
文章(link)提供了一种context配合hooks实现全局状态管理的一种思路,相较于纯context,配合hooks的useReducer提供了更为强大的状态管理能力。
import React, {createContext, useContext, useReducer} from 'react';
export const StateContext = createContext();
export const StateProvider = ({reducer, initialState, children}) =>(
<StateContext.Provider value={useReducer(reducer, initialState)}>
{children}
</StateContext.Provider>
);
export const useStateValue = () => useContext(StateContext);
首先新建一个StateContext,然后再StateContext的Provider中将context的值设置为useReducer(reducer, initialState),这样当调用action执行reducer后,context的值也将发生变化,似的引用context值得组件更新。由useReducer也保证了只有在数据变化是才会更新组件。
在访问context数据方便也做了一层useStateValue hooks封装,不需要在每个组件里调用useContext。
demo
import React, { createContext, useContext, useReducer } from "react";
import { render } from "react-dom";
import { produce } from "immer";
const StateContext = createContext();
const StateProvider = ({ reducer, initialState, children }) => (
<StateContext.Provider value={useReducer(reducer, initialState)}>
{children}
</StateContext.Provider>
);
const useStateValue = () => useContext(StateContext);
function App() {
const initialState = {
count: 0
};
const reducer = (state = initialState, action) =>
produce(state, draft => {
switch (action.type) {
case "increase": {
draft.count += 1;
break;
}
case "decrease": {
draft.count += 1;
break;
}
default:
}
});
return (
<StateProvider initialState={initialState} reducer={reducer}>
<Count />
</StateProvider>
);
}
const Count = () => {
const [{ count }, dispatch] = useStateValue();
return (
<div>
<h1>Counter: {count}</h1>
<button onClick={() => dispatch({ type: "increase" })}>Increase</button>
<button onClick={() => dispatch({ type: "decrease" })}>Decrease</button>
</div>
);
};
const rootElement = document.getElementById("root");
render(<App />, rootElement);
unstated-next
unstated是react社区中比较流行的轻量级的状态管理工具,在api设计的时候沿用react的设计思想,能够快速的理解和上手。
Unstated抛出三个对象,分别是Container、Subscribe和Provider。
Unstated会使用React.createContext来创建一个StateContext对象,用来进行状态的传递。
unstated demo
import React from 'react';
import { render } from 'react-dom';
import { Provider, Subscribe, Container } from 'unstated';
type CounterState = {
count: number
};
class CounterContainer extends Container<CounterState> {
state = {
count: 0
};
increment() {
this.setState({ count: this.state.count + 1 });
}
decrement() {
this.setState({ count: this.state.count - 1 });
}
}
function Counter() {
return (
<Subscribe to={[CounterContainer]}>
{counter => (
<div>
<button onClick={() => counter.decrement()}>-</button>
<span>{counter.state.count}</span>
<button onClick={() => counter.increment()}>+</button>
</div>
)}
</Subscribe>
);
}
render(
<Provider>
<Counter />
</Provider>,
document.getElementById('root')
);
随着Hooks的推出,Unstated也推出了符合hooks api的新一代状态管理库unstated-next,在API设计上延续了Unstated贴近react操作的风格,使用自定义hooks来管理状态。
createContainer(useHook)
创建container,接受一个封装状态的自定义hook参数
import { createContainer } from "unstated-next"
function useCustomHook() {
let [value, setValue] = useState()
let onChange = e => setValue(e.currentTarget.value)
return { value, onChange }
}
let Container = createContainer(useCustomHook)
<Container.Provider initialState>
保留了unstated中的Provider
function ParentComponent() {
return (
<Container.Provider initialState={"value"}>
<ChildComponent />
</Container.Provider>
)
}
useContainer(Container)
在组件内通过useContainer获取自定义hook中的状态和操作
import { useContainer } from "unstated-next"
function ChildComponent() {
let input = useContainer(Container)
return <input value={input.value} onChange={input.onChange} />
}
Demo
import { render } from "react-dom";
import React, { useState } from "react";
import { createContainer } from "unstated-next";
function useCounter(initialState = 0) {
let [count, setCount] = useState(initialState);
let increase = () => setCount(count + 1);
let decrease = () => setCount(count - 1);
return { count, increase, decrease };
}
let Counter = createContainer(useCounter);
function App() {
let counter = Counter.useContainer();
return (
<div>
<h1>Counter: {counter.count}</h1>
<button onClick={counter.increase}>Increase</button>
<button onClick={counter.decrease}>Decrease</button>
</div>
);
}
render(
<Counter.Provider>
<App />
</Counter.Provider>,
document.getElementById("root")
);
mobx-react-lite
作为流行度仅此于Redux的状态管理组件也针对hooks进行了匹配,推出了新一代的状态管理库mobx-react-lite,不再使用inject将store注入component,而是配合react的新context api将store放入context管理。
demo
import { observable } from 'mobx'
import { Observer, useObserver, observer } from 'mobx-react' // 6.x or mobx-react-lite@1.4.0
import ReactDOM from 'react-dom'
const person = observable({
name: 'John',
})
// named function is optional (for debugging purposes)
const P1 = observer(function P1({ person }) {
return <h1>{person.name}</h1>
})
const P2 = ({ person }) => <Observer>{() => <h1>{person.name}</h1>}</Observer>
const P3 = ({ person }) => {
return useObserver(() => <h1>{person.name}</h1>)
}
ReactDOM.render(
<div>
<P1 person={person} />
<P2 person={person} />
<P3 person={person} />
</div>,
)
setTimeout(() => {
person.name = 'Jane'
}, 1000)
比较
- Redux是社区最流行的状态管理工具,生态较好,支持middleware,提供了可预测的状态管理,但需要写大量的模板代码,且包体积在状态管理工具中相对较大,适合大型项目
- context + hooks提供了基本的状态管理功能,配合hooks中的useReducer可以支持大部分redux提供的功能,且不需要引入其他库,适合全局状态较简单的项目
- unstated将状态和操作封装在container中,优点是在风格上比redux更贴近react的操作方式,也更容易上手,仅有200b,但是相比context+useReducer的方式没什么优势,除非原来就使用了unstated的项目,否则并不推荐。
- mobx-react-lite延续了mobx的优点,将状态维护在observable的对象中管理,采用代理的方式,支持直接操作对象,相比redux在代码写法上较为简洁,但对于大型项目,mobx较为灵活的方式不如reducer action->reducer->state的流程更清晰,比较适合中小型项目。