React 是一个十分优秀的UI库, 最初的时候, React 只专一于UI层, 对全局状态管理并无很好的解决方案, 也所以催生出相似Flux, Redux 等优秀的状态管理工具。css
随着时间的演变, 又催化了一批新的状态管理工具。html
简单整理了一些目前主流的状态管理工具:react
这几个都是我接触过的,Npm 上的现状和趋势对比:redux
毫无疑问,React
和 Redux
的组合是目前的主流。api
今天5月份, 一个名叫 Recoil.js
的新成员进入了个人视野,带来了一些有趣的模型和概念,今天咱们就把它和 Redux 作一个简单的对比, 但愿能对你们有所启发。架构
先看 Redux:app
React-Redux 架构图:dom
这个模型仍是比较简单的, 你们也都很熟悉。异步
先用一个简单的例子,回顾一下整个模型:async
export const UPDATE_LIST_NAME = 'UPDATE_NAME';
export const reducer = (state = initialState, action) => { const { listName, tasks } = state; switch (action.type) { case 'UPDATE_NAME': { // ... } default: { return state; } } };
import reducers from '../reducers'; import { createStore } from 'redux'; const store = createStore(reducers); export const TasksProvider = ({ children }) => ( <Provider store={store}> {children} </Provider> );
import { TasksProvider } from './store'; import Tasks from './tasks'; const ReduxApp = () => ( <TasksProvider> <Tasks /> </TasksProvider> );
// components import React from 'react'; import { updateListName } from './actions'; import TasksView from './TasksView'; const Tasks = (props) => { const { tasks } = props; return ( <TasksView tasks={tasks} /> ); }; const mapStateToProps = (state) => ({ tasks: state.tasks }); const mapDispatchToProps = (dispatch) => ({ updateTasks: (tasks) => dispatch(updateTasks(tasks)) }); export default connect(mapStateToProps, mapDispatchToProps)(Tasks);
固然也能够不用connect, react-redux
提供了 useDispatch, useSelector
两个hook, 也很方便。
import { useDispatch, useSelector } from 'react-redux'; const Tasks = () => { const dispatch = useDispatch(); const name = useSelector(state => state.name); const setName = (name) => dispatch({ type: 'updateName', payload: { name } }); return ( <TasksView tasks={tasks} /> ); };
整个模型并不复杂,并且redux
还推出了工具集redux toolkit
,使用它提供的createSlice
方法去简化一些操做, 举个例子:
// Action export const UPDATE_LIST_NAME = 'UPDATE_LIST_NAME'; // Action creator export const updateListName = (name) => ({ type: UPDATE_LIST_NAME, payload: { name } }); // Reducer const reducer = (state = 'My to-do list', action) => { switch (action.type) { case UPDATE_LIST_NAME: { const { name } = action.payload; return name; } default: { return state; } } }; export default reducer;
使用 createSlice
:
// src/redux-toolkit/state/reducers/list-name import { createSlice } from '@reduxjs/toolkit'; const listNameSlice = createSlice({ name: 'listName', initialState: 'todo-list', reducers: { updateListName: (state, action) => { const { name } = action.payload; return name; } } }); export const { actions: { updateListName }, } = listNameSlice; export default listNameSlice.reducer;
经过createSlice
, 能够减小一些没必要要的代码, 提高开发体验。
尽管如此, Redux 还有有一些自然的缺陷
:
对于这个状况, React 自己也提供了解决方案, 就是咱们熟知的 Context.
<MyContext.Provider value={/* some value */}> <MyContext.Consumer> {value => /* render something based on the context value */} </MyContext.Consumer>
给父节点加 Provider 在子节点加 Consumer,不过每多加一个 item 就要多一层 Provider, 越加越多:
并且,使用Context
问题也很多。
对于使用 useContext
的组件,最突出的就是问题就是 re-render
.
不过也有对应的优化方案: React-tracked.
稍微举个例子:
// store.js import React, { useReducer } from 'react'; import { createContainer } from 'react-tracked'; import { reducers } from './reducers'; const useValue = ({ reducers, initialState }) => useReducer(reducer, initialState); const { Provider, useTracked, useTrackedState, useUpdate } = createContainer(useValue); export const TasksProvider = ({ children, initialState }) => ( <Provider reducer={reducer} initialState={initialState}> {children} </Provider> ); export { useTracked, useTrackedState, useUpdate };
对应的,也有 hooks
版本:
const [state, dispatch] = useTracked(); const dispatch = useUpdate(); const state = useTrackedState(); // ...
Recoil.js 提供了另一种思路, 它的模型是这样的:
在 React tree 上建立另外一个正交的 tree,把每片 item 的 state 抽出来。
每一个 component 都有对应单独的一片 state,当数据更新的时候对应的组件也会更新。
Recoil 把 这每一片的数据称为 Atom,Atom 是可订阅可变的 state 单元。
这么说可能有点抽象, 看个简单的例子吧:
// index.js
import React from "react"; import ReactDOM from "react-dom"; import { RecoilRoot } from "recoil"; import "./index.css"; import App from "./App"; import * as serviceWorker from "./serviceWorker"; ReactDOM.render( <React.StrictMode> <RecoilRoot> <App /> </RecoilRoot> </React.StrictMode>, document.getElementById("root") );
Provides the context in which atoms have values. Must be an ancestor of any component that uses any Recoil hooks. Multiple roots may co-exist; atoms will have distinct values within each root. If they are nested, the innermost root will completely mask any outer roots.
能够把 RecoilRoot 当作顶层的 Provider.
假设, 如今要实现一个counter:
先用 useState 实现:
import React, { useState } from "react"; const App = () => { const [count, setCount] = useState(0); return ( <div className="App"> <button onClick={() => setCount(count + 1)}>Increase</button> <button onClick={() => setCount(count - 1)}>Decrease</button> <div>Count is {count}</div> </div> ); }; export default App;
再用 atom 改写一下:
import React from "react"; import { atom, useRecoilState } from "recoil"; const countState = atom({ key: "counter", default: 0, }); const App = () => { const [count, setCount] = useRecoilState(countState); return ( <div className="app"> <button onClick={() => setCount(count + 1)}>Increase</button> <button onClick={() => setCount(count - 1)}>Decrease</button> <div>Count is {count}</div> </div> ); }; export default App;
看到这, 你可能对atom 有一个初步的认识了。
那 atom 具体是个什么概念呢?
简单理解一下,atom 是包含了一份数据的集合,这个集合是可共享,可修改的。
组件能够订阅atom, 能够是一个, 也能够是多个,当 atom 发生改变时,触发再次渲染。
const someState = atom({ key: 'uniqueString', default: [], });
每一个atom 有两个参数:
key
:用于内部识别atom的字符串。相对于整个应用程序
中的其余原子和选择器,该字符串应该是惟一的
。default
:atom的初始值。atom 是存储状态的最小单位, 一种合理的设计是, atom 尽可能小, 保持最大的灵活性。
Recoil 的做者, 在 ReactEurope video 中也介绍了之后一种封装定atom 的方法:
export const itemWithId = memoize(id => atom({ key: `item${id}`, default: {...}, }));
“A selector is a pure function that accepts atoms or other selectors as input. When these upstream atoms or selectors are updated, the selector function will be re-evaluated.”
selector 是以 atom 为参数的纯函数, 当atom 改变时, 会触发从新计算。
selector 有以下参数:
key
:用于内部识别 atom 的字符串。相对于整个应用程序
中的其余原子和选择器,该字符串应该是惟一的
.get
:做为对象传递的函数{ get }
,其中get
是从其余案atom或selector检索值的函数。传递给此函数的全部atom或selector都将隐式添加到selector的依赖项列表中。set?
:返回新的可写状态的可选函数。它做为一个对象{ get, set }
和一个新值传递。get
是从其余atom或selector检索值的函数。set
是设置原子值的函数,其中第一个参数是原子名称,第二个参数是新值。看个具体的例子:
import React from "react"; import { atom, selector, useRecoilState, useRecoilValue } from "recoil"; const countState = atom({ key: "myCount", default: 0, }); const doubleCountState = selector({ key: "myDoubleCount", get: ({ get }) => get(countState) * 2, }); const inputState = selector({ key: "inputCount", get: ({ get }) => get(doubleCountState), set: ({ set }, newValue) => set(countState, newValue), }); const App = () => { const [count, setCount] = useRecoilState(countState); const doubleCount = useRecoilValue(doubleCountState); const [input, setInput] = useRecoilState(inputState); return ( <div className="App"> <button onClick={() => setCount(count + 1)}>Increase</button> <button onClick={() => setCount(count - 1)}>Decrease</button> <input type="number" value={input} onChange={(e) => setInput(Number(e.target.value))} /> <div>Count is {count}</div> <div>Double count is {doubleCount}</div> </div> ); }; export default App;
比较好理解, useRecoilState
, useRecoilValue
这些基础概念能够参考官方文档。
另外, selector 还能够作异步, 好比:
get: async ({ get }) => { const countStateValue = get(countState); const response = await new Promise( (resolve) => setTimeout(() => resolve(countStateValue * 2)), 1000 ); return response; }
不过对于异步的selector, 须要在RecoilRoot
加一层Suspense
:
ReactDOM.render( <React.StrictMode> <RecoilRoot> <React.Suspense fallback={<div>Loading...</div>}> <App /> </React.Suspense> </RecoilRoot> </React.StrictMode>, document.getElementById("root") );
模型对比:
Recoil 推荐 atom 足够小, 这样每个叶子组件能够单独去订阅, 数据变化时, 能够达到 O(1)级别的更新.
Recoil 做者 Dave McCabe
在一个评论中提到:
Well, I know that on one tool we saw a 20x or so speedup compared to using Redux. This is because Redux is O(n) in that it has to ask each connected component whether it needs to re-render, whereas we can be O(1).
useReducer is equivalent to useState in that it works on a particular component and all of its descendants, rather than being orthogonal to the React tree.
Rocil 能够作到 O(1) 的更新是由于,当atom数据变化时,只有订阅了这个 atom 的组件须要re-render。
不过, 在Redux 中,咱们也能够用selector 实现一样的效果:
// selector const taskSelector = (id) => state.tasks[id]; // component code const task = useSelector(taskSelector(id));
不过这里的一个小问题是,state变化时,taskSelector 也会从新计算, 不过咱们能够用createSelector
去优化, 好比:
import { createSelector } from 'reselect'; const shopItemsSelector = state => state.shop.items; const subtotalSelector = createSelector( shopItemsSelector, items => items.reduce((acc, item) => acc + item.value, 0) )
写到这里, 是否是想说,就这? 扯了这么多, Rocoil 能作的, Redux 也能作, 那要你何用?
哈哈, 这个确实有点尴尬。
不过我认为,这是一种模式上的改变,recoil 鼓励把每个状态作的足够小, 任意组合,最小范围的更新。
而redux, 咱们的习惯是, 把容器组件链接到store上, 至于子组件,哪怕往下传一层,也没什么所谓。
我想,Recoil 这么设计,多是十分注重性能问题,优化超大应用的性能表现。
目前,recoil 还处于玩具阶段
, 还有大量的 issues 须要处理, 不过值得继续关注。
感兴趣的朋友能够看看, 作个todo-list体验一下。
但愿这篇文章能帮到你。
才疏学浅,文中如有错误, 欢迎指正。