背景
React 是一個十分優秀的UI庫, 最初的時候, React 只專一於UI層, 對全局狀態管理並無很好的解決方案, 也所以催生出相似Flux, Redux 等優秀的狀態管理工具。css
隨着時間的演變, 又催化了一批新的狀態管理工具。html
簡單整理了一些目前主流的狀態管理工具:前端
- Redux
- React Context & useReducer
- Mobx
- Recoil
- react-sweet-state
- hox
這幾個都是我接觸過的,Npm 上的現狀和趨勢對比:react
毫無疑問,React
和 Redux
的組合是目前的主流。面試
今天5月份, 一個名叫 Recoil.js
的新成員進入了個人視野,帶來了一些有趣的模型和概念,今天咱們就把它和 Redux 作一個簡單的對比, 但願能對你們有所啓發。npm
正文
先看 Redux:redux
Redux
React-Redux 架構圖:api
這個模型仍是比較簡單的, 你們也都很熟悉。架構
先用一個簡單的例子,回顧一下整個模型:app
actions.js
export const UPDATE_LIST_NAME = 'UPDATE_NAME';
reducers.js
export const reducer = (state = initialState, action) => { const { listName, tasks } = state; switch (action.type) { case 'UPDATE_NAME': { // ... } default: { return state; } } };
store.js
import reducers from '../reducers'; import { createStore } from 'redux'; const store = createStore(reducers); export const TasksProvider = ({ children }) => ( <Provider store={store}> {children} </Provider> );
App.js
import { TasksProvider } from './store'; import Tasks from './tasks'; const ReduxApp = () => ( <TasksProvider> <Tasks /> </TasksProvider> );
Component
// 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 還有有一些自然的缺陷
:
- 概念比較多,心智負擔大。
- 屬性要一個一個 pick,計算屬性要依賴 reselect。還有魔法字符串等一系列問題,用起來很麻煩容易出錯,開發效率低。
- 觸發更新的效率也比較差。對於connect到store的組件,必須一個一個遍歷,組件再去作比較,攔截沒必要要的更新, 這在注重性能或者在大型應用裏, 無疑是災難。
對於這個狀況, 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
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") );
Recoil 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.
Atoms
假設, 如今要實現一個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, 能夠是一個, 也能夠是多個,當 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: {...}, }));
Selectors
「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") );
Redux vs Recoil
模型對比:
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體驗一下。
但願這篇文章能幫到你。
才疏學淺,文中如有錯誤, 歡迎指正。
若是你以爲這篇內容對你挺有啓發,能夠:
- 點個「在看」,讓更多的人也能看到這篇內容。
- 關注公衆號「前端e進階」,掌握前端面試重難點,公衆號後臺回覆「加羣」和小夥伴們暢聊技術。
參考資料
- http://react.html.cn/docs/context.html#reactcreatecontext
- https://recoiljs.org/docs/basic-tutorial/atoms
- https://www.emgoto.com/react-state-management/
- https://medium.com/better-programming/recoil-a-new-state-management-library-moving-beyond-redux-and-the-context-api-63794c11b3a5
本文同步分享在 博客「皮小蛋」(SegmentFault)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。