本文也在個人博客平臺:betamee.github.io 可見,歡迎來交流~css
在日趨複雜的 Web 應用中,狀態管理一直是一個很重要的話題。React 技術棧中也衍生出了很多解決方案,如 Redux、Mobx,而且隨着 React Hooks 的發佈,帶來全新的代碼編寫模式,在狀態管理庫的使用上也出現了不少新變化。html
爲了指明 React 狀態管理,我寫了幾個 examples,並比較一下其中的使用差別。前端
開發同一個 Todo App,但使用方式分爲如下四種:node
所有代碼發佈在 Github 的這個倉庫 GitHub - BetaMee/react-usage-examples: React Usage Examples 上,可供參考。react
Redux 是很火的一個技術解決方案,甫一推出就備受關注。Redux 由 Flux 演變而來,但受 Elm 的啓發,避開了 Flux 的複雜性。它提供了一套簡單但有效的 API,提供可預測化的狀態管理。但在學習難度上很是陡峭,由於它帶來新的概念和約定,只有理解通了,才能夠更好的應用它。git
要理解 Redux,能夠分爲如下幾個點:github
- 全部的 state 都以一個對象樹的形式存儲在一個單一的 store 中
- UI 是這個 state 樹某一時刻狀態的映射
- 改變 state 樹的惟一方式是觸發一個 action,一個描述發生什麼的對象
- 至於對 state 樹的具體改變操做,是由叫 reducers 的純函數來實現
- 更新後的 state 樹又映射爲新的 UI
由上能夠總結爲Redux 三大原則:typescript
- 單一數據源
- state 是隻讀的
- 使用純函數來執行修改
理通上述概念基本上能夠入門 Redux 了。但只有上述概念沒法解決 Web 應用更復雜的場景,如異步加載數據更新界面。編程
這就須要涉及到 Redux 中的中間件概念。json
使用中間件,咱們能夠在 state 變化的時候對數據進行監控、截獲、更改,還能夠拓展異步操做。這就給了咱們很大的自由,充分發揮 Redux 的潛力。
Redux 的中間件實質上是一個函數,是對store.dispatch
方法進行了改造,在發出 Action 和執行 Reducer 這兩步之間,添加了特定功能。
Redux 社區已經發展了不少成熟的中間件,如 redux-thunk、redux-soga,只須要按需使用便可。
具體可參考:redux#ecosystem#middleware 和 中文版本的 cnredux#ecosystem
與 Redux 類似的,前端出現的另外一個狀態管理方案是 MobX。
MobX 的官網是這樣介紹的:
MobX 是一個通過戰火洗禮的庫,它經過透明的函數響應式編程 (transparently applying functional reactive programming - TFRP) 使得狀態管理變得簡單和可擴展。MobX背後的哲學很簡單: 任何源自應用狀態的東西都應該自動地得到。 其中包括UI、數據序列化、服務器通信,等等。
其實相比 Redux 的強規則約定,MobX 明顯更簡單靈活,MobX 的核心原理是經過 action 觸發 state 的變化,進而觸發 state 的衍生對象(Computed value & Reactions)。開發者只須要定義須要 Observe 的數據和由此衍生的數據(Computed value)或者操做 (Reactions),剩下的更新天然就交給 MobX 去作就能夠了。
MobX 和 Redux 比較大概體現出這些差別:
- Redux 是函數式而 MobX 是面向對象
- Redux 的 state 每次返回新的數據,理想狀態是 immutable 的,MobX 則至始至終保持一份引用
- Redux 可支持數據回朔,MobX 因爲只有一份引用,沒法支持回朔
- Redux 約定單一數據源,MobX 能夠將 state 樹拆分進多個 store 中
- Redux 須要中間件處理異步,MobX 能夠直接使用 async/await 來處理
總之,二者相比,MobX 確實比 Redux 上手更容易些,而且不須要寫不少樣板代碼,但 Redux 的這種「複雜」不必定是無用的,強約定的規則能夠爲大型項目,複雜數據狀態管理提供可靠的支持,而 MobX 在某些場景下確實能夠比 Redux 項目提供更高效的選擇。
但也不能簡單地就將 Redux 複雜化,MobX 簡單化,仍是要看適合的場景。咱們應該更關注它們解決什麼問題,它們解決問題的關注點,或者說實現方式是什麼,它們的優缺點還有什麼,哪個更適合當前項目,以及項目將來發展。
咱們作的這個 Todo 只涉及到簡單的增刪功能:
接下來我將具體介紹四個 example 的用法,並從中學習項目的組織模式、代碼的編寫模式,看一看 React 的狀態管理庫的應用發展。
第一個是 Classic React App with Redux,是使用「傳統」的類模式 + Redux 進行開發的。這個模式在 Redux 剛推出時就有了。
咱們經過 create-react-app 腳手架來搭建咱們的啓動應用,這將會節省咱們不少時間:
npx create-react-app classicreactwithredux --typescript
複製代碼
這裏使用的是 Typescript 模版,整個應用都將以 TS 來開發,因此這也是個學習 TS 的一個練手機會。
如下是整個應用的代碼文件:
├── README.md
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
├── src // 源碼文件,全部的代碼在這裏
│ ├── App.css
│ ├── App.test.tsx
│ ├── App.tsx // App 入口
│ ├── components // UI 組件
│ │ ├── TodoAdd.tsx
│ │ ├── TodoCounter.tsx
│ │ ├── TodoView.tsx
│ │ ├── TodoViewList.tsx
│ │ ├── assets
│ │ │ ├── delete.svg
│ │ │ ├── select.svg
│ │ │ └── select2.svg
│ │ └── styles
│ │ ├── TodoAdd.css
│ │ ├── TodoCounter.css
│ │ ├── TodoView.css
│ │ └── TodoViewList.css
│ ├── containers // 容器組件
│ │ ├── TodoAddContainer.ts
│ │ ├── TodoCounterContainer.ts
│ │ └── TodoViewListContainer.ts
│ ├── index.css
│ ├── index.tsx // 整個應用的入口文件
│ ├── interfaces // 定義的 ts 接口文件
│ │ ├── component.ts
│ │ └── index.ts
│ ├── react-app-env.d.ts
│ ├── serviceWorker.ts
│ └── store // 這裏放置 store 文件
│ ├── store.ts
│ └── todo // 按角色劃分 state 樹,一個文件夾一個角色
│ ├── actions.ts
│ ├── index.ts
│ ├── reducers.ts
│ └── types.ts
├── tsconfig.json
└── yarn.lock
複製代碼
index.tsx
做爲入口文件,承擔着全局初始化工做的任務,能夠將初始化 store 、render app 等任務放在這裏。
咱們經過 react-redux
提供的 Provider
包裹 App
組件,將 store 數據注入全局應用,下面的組件能夠經過必定方式獲取 redux 的數據。後續我會介紹如何在 container 組件中使用。
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import configureStore from './store/store'
// 初始化 redux store
const AppStore = configureStore()
ReactDOM.render(
<Provider store={AppStore}>
<App />
</Provider>,
document.getElementById('root')
);
serviceWorker.unregister();
複製代碼
在 configureStore
函數中,執行的是組合 root state、加載中間件任務,全部對 redux 的配置基本上都在這裏:
import {
createStore,
combineReducers
} from 'redux';
import { todoReducer } from './todo/';
// 根 rootReducer
const rootReducer = combineReducers({
todoReducer: todoReducer
})
// 導出store 類型
export type AppState = ReturnType<typeof rootReducer>
const configureStore = () => {
// 組合中間件,這個例子沒有用到
// const middlewares = [thunkMiddleware];
// const middleWareEnhancer = applyMiddleware(...middlewares);
// 建立 store
const store = createStore(rootReducer);
return store;
}
export default configureStore
export * from './todo'
複製代碼
這裏咱們對 Redux 的 state 按角色劃分,如這裏的狀態數據 todo,咱們在 todo 的文件夾下配置 action
、reducers
項,而後統一導出:
│ └── todo // 按角色劃分 state 樹,一個文件夾一個角色
│ ├── actions.ts
│ ├── index.ts
│ ├── reducers.ts
│ └── types.ts
複製代碼
這個的好處就是依賴清晰,後續擴展很方便,好比再增長一個新的狀態數據,只須要新增文件夾,把這個「角色」的邏輯寫清楚就能夠了。
固然,Redux 不僅是能夠按照角色劃分,也能夠按照功能劃分,如全部的 actions
統一放到 action 文件夾下, reducers
放到 renduers 文件夾下:
// 示例:按功能拆分文件夾
│ └── actions
│ ├── index.ts
│ └── renduers
│ ├── index.ts
│ └── types
│ ├── index.ts
複製代碼
接下來,咱們看一下 todo 文件下,是如何編寫 Reducer 和 Action 邏輯的:
reducers.ts:
import {
TodoReducerType,
TodoActionType
} from './types'
import {
ADD_TODO,
REMOVE_TODO,
SELECT_TODO
} from './types'
// 初始化狀態
const initalState: TodoReducerType = []
export const todoReducer = (state = initalState, action: TodoActionType): TodoReducerType => {
switch(action.type) {
case ADD_TODO:
return [...state, {
id: Math.random(),
name: action.name,
finished: false
}]
case REMOVE_TODO:
return state.filter(todo => todo.id !== action.id)
case SELECT_TODO:
return state.map(todo => {
if (todo.id === action.id) {
return {
...todo,
finished: !todo.finished
}
} else {
return todo
}
})
default:
return state;
}
}
複製代碼
actions.ts:
import {
ADD_TODO,
REMOVE_TODO,
SELECT_TODO
} from './types'
import {
TodoActionType
} from './types'
export const addTodo = (name: string): TodoActionType => ({
type: ADD_TODO,
name: name
})
export const removeTodo = (id: number): TodoActionType => ({
type: REMOVE_TODO,
id: id
})
export const selectTodo = (id: number): TodoActionType => ({
type: SELECT_TODO,
id: id
})
複製代碼
這兩個文件決定了每一個角色 state 如下兩個關鍵點,這是使用 Redux 的精髓:
- action 純函數:描述如何改變狀態(是什麼)
- reducer 純函數:描述具體改變狀態的邏輯(怎麼作)
其中 actions.ts
中的函數是咱們須要導入到 UI 組件中去的,才能發揮做用。
那麼咱們是如何將 actions
(操做) 和 state
(狀態)具體導入到組件中去呢?
這就是 container 文件夾下的 container 組件須要回答的問題:
│ ├── containers // 容器組件
│ │ ├── TodoAddContainer.ts
│ │ ├── TodoCounterContainer.ts
│ │ └── TodoViewListContainer.ts
複製代碼
其實不要被 Redux 中所謂的容器組件、UI 組件概念迷惑,這其實就是一個鏈接 Redux 和 React 組件的中間步驟而已。
這裏以 TodoViewListContainer.ts
爲例:
import { connect } from 'react-redux'
import {
Dispatch
} from 'redux'
import TodoViewList from '../components/TodoViewList'
import {
AppState,
TodoActionType,
removeTodo,
selectTodo
} from '../store/store'
const mapStateToProps = (state: AppState) => ({
todos: state.todoReducer
})
const mapDispatchToProps = (dispatch: Dispatch<TodoActionType>) => ({
removeTodoById: (id: number) => dispatch(removeTodo(id)),
selectTodoById: (id: number) => dispatch(selectTodo(id))
})
export default connect(mapStateToProps, mapDispatchToProps)(TodoViewList)
複製代碼
其中,TodoViewList
是具體的 React 組件,mapStateToProps
、mapDispatchToProps
如其名字所示,是將 action 和 state 做爲 React 的 props
往下導入。
而後在 TodoViewList
組件中,咱們這樣使用:
import React from 'react'
import TodoView from './TodoView'
import './styles/TodoViewList.css'
import {
IViewListProp
} from '../interfaces'
const TodoViewList: React.FC<IViewListProp> = ({
todos,
selectTodoById,
removeTodoById
}) => (
<div className="viewlist">
{todos.map((item, index) => (
<TodoView
todo={item}
selectTodoById={selectTodoById}
removeTodoById={removeTodoById}
key={index}
/>
))}
</div>
)
export default TodoViewList
複製代碼
這就是 React 和 Redux 的結合使用,到這一步基本完成全部的工做了,後續只要 state 有變化,組件就會發生更新,而要使 state 發生變化,惟一的途徑是傳入的 action 函數。這樣,整個應用依賴清晰,數據流變化清晰可調控,對於排查問題很是方便。
接下來咱們看看使用類模式 + MobX 架構的 Todo 應用。
先總體看一下這裏的文件結構:
├── README.md
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
├── src // 源碼
│ ├── App.css
│ ├── App.test.tsx
│ ├── App.tsx
│ ├── components // UI 組件
│ │ ├── TodoAdd.tsx
│ │ ├── TodoCounter.tsx
│ │ ├── TodoView.tsx
│ │ ├── TodoViewList.tsx
│ │ ├── assets
│ │ │ ├── delete.svg
│ │ │ ├── select.svg
│ │ │ └── select2.svg
│ │ └── styles
│ │ ├── TodoAdd.css
│ │ ├── TodoCounter.css
│ │ ├── TodoView.css
│ │ └── TodoViewList.css
│ ├── index.css
│ ├── index.tsx // 入口
│ ├── interfaces // 定義的 ts 接口
│ │ ├── base.ts
│ │ └── index.ts
│ ├── react-app-env.d.ts
│ ├── serviceWorker.ts
│ └── store // 存放 mobx store
│ ├── TodoListStore.ts
│ └── index.ts
├── tsconfig.json
└── yarn.lock
複製代碼
和 Redux 的最大區別在於,Redux 是面向函數式的思惟,而 MobX 是面向對象的思惟。咱們來看一下在 MobX 中是如何定義 store 的:
import {
observable,
computed,
action
} from 'mobx'
import {
ITodo
} from '../interfaces'
class Todo {
id = Math.random();
@observable name: string;
@observable finished = false;
constructor(name: string) {
this.name = name;
}
}
class TodoListStore {
@observable todos: Array<ITodo> = []
@computed get finishedTodoCount() {
return this.todos.filter(todo => todo.finished).length
}
@computed get totalCount() {
return this.todos.length
}
@action
public finishTodoById = (id: number) => {
this.todos.forEach(todo => {
if (todo.id === id) {
todo.finished = !todo.finished
}
})
}
@action
public addNewTodo = (name: string) => {
this.todos.push(new Todo(name))
}
@action
public removeTodoById = (id: number) => {
// 找到某一項的位置
const index = this.todos.findIndex(todo => todo.id === id)
this.todos.splice(index, 1)
}
}
export default TodoListStore
複製代碼
咱們定義一個類,裏面封裝狀態 + 函數,給狀態打上 @observable
裝飾器,給函數打上 @action
裝飾器。由於 MobX 在約束上很自由,只要觸動 @observable
的數據就會發生更新,但這樣的話就會發生濫用經過狀態直接更新的操做,不容易追蹤數據變化流程。好比在組件中隨手的一個:this.props.todos.push(...)
,這就會觸發整個應用數據更新。
因此 MobX 推薦使用嚴格模式,咱們只能經過被 @action
狀態裝飾過的函數來變動狀態,這個和 Redux 中經過 action 函數來變動狀態思路是一致,保證數據變化可追蹤調控。
與 Redux 另外一個不一樣點,是在 MobX 中咱們直接能夠編寫計算過的 @computed
數據邏輯,在 Redux 中,咱們只能經過獲取的 state 手動計算,而在 MobX 中,經過 @computed
裝飾的 get
函數能夠一直獲取最新的計算值。這也是 MobX 的理念:任何源自應用狀態的東西都應該自動地得到。
因爲 MobX 不像 Redux 約定單一數據源,因此咱們要拓展狀態也很簡單,繼續添加新的類,封裝好須要 @observable
的狀態、 @computed
的狀態以及對應於數據更新的 @action
函數。
在組件接入 MobX 的過程也比 Redux 要簡單不少。
首先是在入口組件中配置:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import {
TodoListStore
} from './store'
import {
Provider
} from 'mobx-react'
// 狀態
const rootStore = {
todoListStore: new TodoListStore()
}
// 經過 Provider/inject 來注入,react 16.8 + 可使用 hooks + context 來注入
ReactDOM.render(
<Provider {...rootStore} >
<App />
</Provider>,
document.getElementById('root') as HTMLElement
);
複製代碼
而後在須要使用 MobX 狀態的組件中使用 mobx-react
包中的 observer
和 inject
高階函數將數據注入:
App.tsx:
import React from 'react';
import {
observer,
inject
} from 'mobx-react'
import TodoAdd from './components/TodoAdd'
import TodoViewList from './components/TodoViewList'
import TodoCounter from './components/TodoCounter'
import './App.css';
import { IAppProp } from './interfaces'
// ! @inject 高階方法注入會和 Typescript 有衝突,緣由在於 TS 會檢查外部調用傳進來的 props 接口,
// ! 可是 inject 至關於中間注入,避開了檢查,這就致使 TS 報錯。
const App: React.FC<IAppProp> = inject('todoListStore')(observer(({ todoListStore }) => {
return (
<div className="app">
<div className="app-title">Classic React App ( mobx )</div>
<TodoAdd
addNewTodo={todoListStore!.addNewTodo}
/>
<TodoViewList
todos={todoListStore!.todos}
finishTodoById={todoListStore!.finishTodoById}
removeTodoById={todoListStore!.removeTodoById}
/>
<TodoCounter
finishedCount={todoListStore!.finishedTodoCount}
totalCount={todoListStore!.totalCount}
/>
</div>
);
}))
export default App;
複製代碼
TodoViewList.tsx:
import React from 'react'
import { observer } from 'mobx-react'
import TodoView from './TodoView'
import './styles/TodoViewList.css'
import {
IViewListProp
} from '../interfaces'
@observer
class TodoViewList extends React.Component<IViewListProp, {}> {
render() {
const {
removeTodoById,
finishTodoById,
todos
} = this.props
return (
<div className="viewlist">
{todos.map((item, index) => (
<TodoView
todo={item}
finishTodoById={finishTodoById}
removeTodoById={removeTodoById}
key={index}
/>
))}
</div>
)
}
}
export default TodoViewList
複製代碼
這裏相比 Redux 複雜的「容器組件」概念來講,簡單不少,只要在須要 MobX 狀態的組件中使用 @observer
裝飾器就能夠了。簡單明瞭。
React 16.8 的發佈帶來了全新的 Hooks API,給 React 生態圈帶來不少新思路。這裏咱們看看如何結合 Hooks + Redux 編寫同樣的應用,和類模式 + Redux 相比有何優點。
應用結構:
├── README.md
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
├── src // 源碼
│ ├── App.css
│ ├── App.test.tsx
│ ├── App.tsx
│ ├── components
│ │ ├── TodoAdd.tsx
│ │ ├── TodoCounter.tsx
│ │ ├── TodoView.tsx
│ │ ├── TodoViewList.tsx
│ │ ├── assets
│ │ │ ├── delete.svg
│ │ │ ├── select.svg
│ │ │ └── select2.svg
│ │ └── styles
│ │ ├── TodoAdd.css
│ │ ├── TodoCounter.css
│ │ ├── TodoView.css
│ │ └── TodoViewList.css
│ ├── index.css
│ ├── index.tsx
│ ├── interfaces
│ │ ├── base.ts
│ │ └── index.ts
│ ├── react-app-env.d.ts
│ ├── serviceWorker.ts
│ └── store // redux store
│ ├── store.tsx
│ └── todo
│ ├── actions.ts
│ ├── index.ts
│ ├── reducers.ts
│ └── types.ts
├── tsconfig.json
└── yarn.lock
複製代碼
咱們發現一個很大的區別,和 Classic React App with Redux 相比這裏沒有了 container 文件夾了!也就是說咱們不須要使用 container 接入 Redux 了。
是的,緣由在於這裏咱們使用的是 Context API + Hooks API。
Context API 來替代原有的 react-redux
包中 Provider
組件來全局注入數據。Provider
組件本質上也是用的 React Context API,只是之前不怎麼受官方推崇,React 16 出來後,Context API 才正式被承認,使用方式上也變得更簡單了。而另外一方面, Hooks 中直接內置了對 Redux 的支持,這樣就不用安裝額外的 Redux 包了。
事實上,這個應用只用到了 React 自身的特性,沒有安裝任何外部依賴庫:
"dependencies": {
"@types/jest": "24.0.15",
"@types/node": "12.6.8",
"@types/react": "16.8.23",
"@types/react-dom": "16.8.5",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-scripts": "3.0.1",
"typescript": "3.5.3"
}
複製代碼
Hooks 出現的意義在於能夠共享業務邏輯,因此這個應用中最核心的文件在於 store 文件夾下的 store.tsx
:
import React from 'react'
import {
TodoReducerType,
TodoActionType,
initalTodoState,
todoReducer
} from './todo'
type combineDispatchsType = React.Dispatch<TodoActionType>
// 將全部的 dispatch 組合
const combineDispatchs = (dispatchs: Array<combineDispatchsType>) => (obj: TodoActionType) => {
for (let i = 0; i < dispatchs.length; i++) {
dispatchs[i](obj)
}
}
// 根組件狀態
const AppState = {
todoState: [] as TodoReducerType,
dispatch: {} as ReturnType<typeof combineDispatchs>
}
// Context
export const ContextStore = React.createContext(AppState)
const HookContextProvider: React.FC = ({ children }) => {
const [todoState, todoDispatch] = React.useReducer(
todoReducer,
initalTodoState
)
return (
<ContextStore.Provider
value={{
todoState,
dispatch: combineDispatchs([
todoDispatch
])
}}
>
{children}
</ContextStore.Provider>
)
}
export default HookContextProvider
複製代碼
這個文件幹了這些事:
- 定義包含 state 和 action 的 AppState
- 使用 AppState 初始化 React.createContext,生成全局的 ContextStore
- 使用 React.useReducer 生成具體的 todoState、todoDispatch
- 經過 ContextStore.Provider 組件將 todoState、todoDispatch 注入整個應用
能夠看到,todoState
、todoDispatch
纔是整個代碼的重點,由於這裏包含着整個應用的 state 和 action,而生成 state 和 action 的 React.useReducer
中的參數:todoReducer
、initalTodoState
,就是正常的 Redux 模式中的 Reducer
函數,事實上,這裏的 todo 文件夾就是原封不動將類模式那個 todo 文件夾搬過來的。核心概念都是同樣的。
這也說明了,React 經過 Hooks API 將 Redux 概念整個地吸取進來了。
而在組件中,咱們使用也很方便:
TodoViewList.tsx:
import React, {
useContext
} from 'react'
import TodoView from './TodoView'
import {
ContextStore
} from '../store/store'
import {
SELECT_TODO,
REMOVE_TODO
} from '../store/todo'
import './styles/TodoViewList.css'
const TodoViewList: React.FC = () => {
const {
todoState,
dispatch
} = useContext(ContextStore)
// 發起 action 操做
const selectTodoById = (id: number) => dispatch({
type: SELECT_TODO,
id
})
const removeTodoById = (id: number) => dispatch({
type: REMOVE_TODO,
id
})
return (
<div className="viewlist">
{todoState.map((item, index) => (
<TodoView
todo={item}
selectTodoById={selectTodoById}
removeTodoById={removeTodoById}
key={index}
/>
))}
</div>
)
}
export default TodoViewList
複製代碼
只須要使用 const { todoState, dispatch } = useContext(ContextStore)
,很是方便。
相比起 Hooks 吸取了 Redux,MobX 並無直接被 Hooks 化,仍是須要外部的依賴。
"dependencies": {
"@types/jest": "24.0.15",
"@types/node": "12.6.8",
"@types/react": "16.8.23",
"@types/react-dom": "16.8.5",
"mobx": "^5.13.0",
"mobx-react-lite": "^1.4.1",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-scripts": "3.0.1",
"typescript": "3.5.3"
},
複製代碼
值得一提的是,mobx-react-lite
這個包是 mobx-react
的精簡,是爲了跟進 Hooks 而發佈的,也就是隻能在 React 16.8+ 中使用,不兼容類模式。根據官方說法,lite 包最終仍是會合並進 mobx-react
中的。
├── README.md
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
├── src // 源碼
│ ├── App.css
│ ├── App.test.tsx
│ ├── App.tsx
│ ├── components // UI 組件
│ │ ├── TodoAdd.tsx
│ │ ├── TodoCounter.tsx
│ │ ├── TodoView.tsx
│ │ ├── TodoViewList.tsx
│ │ ├── assets
│ │ │ ├── delete.svg
│ │ │ ├── select.svg
│ │ │ └── select2.svg
│ │ └── styles
│ │ ├── TodoAdd.css
│ │ ├── TodoCounter.css
│ │ ├── TodoView.css
│ │ └── TodoViewList.css
│ ├── index.css
│ ├── index.tsx
│ ├── interfaces
│ │ ├── base.ts
│ │ └── index.ts
│ ├── react-app-env.d.ts
│ ├── serviceWorker.ts
│ └── store // mobx store
│ ├── index.ts
│ └── useTodoStore.tsx
├── tsconfig.json
└── yarn.lock
複製代碼
這裏直接說重點,整個應用的重點在 store 文件夾下的 useTodoStore.tsx
:
import React, {
useContext
} from 'react'
import {
useLocalStore
} from 'mobx-react-lite'
import {
observable
} from 'mobx'
import {
ITodo
} from '../interfaces'
// 定義 store shape
const createStore = () => ({
todos: [] as ITodo[], // 或者 Array<ITodo>
get finishedTodoCount() {
return this.todos.filter(todo => todo.finished).length
},
get totalCount() {
return this.todos.length
},
finishTodoById(id: number) {
this.todos.forEach(todo => {
if (todo.id === id) {
todo.finished = !todo.finished
}
})
},
addNewTodo(name: string) {
// *新增 observable Todo 對象
this.todos.push(observable({
id: Math.random(),
name: name,
finished: false
}))
},
removeTodoById(id: number) {
// 找到某一項的位置
const index = this.todos.findIndex(todo => todo.id === id)
this.todos.splice(index, 1)
}
})
type TTodoStore = ReturnType<typeof createStore>
// 建立 context
const TodoStoreContext = React.createContext<TTodoStore | null>(null)
// 建立 Provider,經過 React.Context 來注入
const TodoStoreProvider: React.FC = ({ children }) => {
const store = useLocalStore(createStore)
return (
<TodoStoreContext.Provider value={store}>
{children}
</TodoStoreContext.Provider>
)
}
// 建立 Hook
const useTodoStore = () => {
const store = useContext(TodoStoreContext)
if (!store) {
throw new Error('You have forgot to use StoreProvider, shame on you.')
}
return store
}
export {
TodoStoreProvider,
useTodoStore
}
複製代碼
這裏,TodoStoreProvider
組件是 Context 的封裝,使用 useLocalStore
生成 MobX store,而 useTodoStore
Hook 是對於 useContext
的封裝,因此,在組件中使用也就變得很方便:
App.tsx:
import React from 'react';
import './App.css';
import TodoAdd from './components/TodoAdd'
import TodoViewList from './components/TodoViewList'
import TodoCounter from './components/TodoCounter'
import {
TodoStoreProvider
} from './store'
const App: React.FC = () => {
return (
<TodoStoreProvider>
<div className="app">
<div className="app-title">React Hooks App ( mobx )</div>
<TodoAdd />
<TodoViewList />
<TodoCounter />
</div>
</TodoStoreProvider>
);
}
export default App;
複製代碼
TodoViewList.tsx:
import React from 'react'
import {
observer
} from 'mobx-react-lite'
import TodoView from './TodoView'
import './styles/TodoViewList.css'
import {
useTodoStore
} from '../store'
const TodoViewList: React.FC = observer(() => {
const todoStore = useTodoStore()
return (
<div className="viewlist">
{todoStore.todos.map((item, index) => (
<TodoView
todo={item}
finishTodoById={todoStore.finishTodoById}
removeTodoById={todoStore.removeTodoById}
key={index}
/>
))}
</div>
)
})
export default TodoViewList
複製代碼
我的以爲,比起 React Hooks App with Redux 這個例子,使用方式要複雜一點,但比類模式要簡約很多,定義的 useTodoStore
能夠在任何組件中使用,若是後續要拓展的話,徹底能夠照着再寫一個 useXXXStore
。
四個 example 寫下來,個人我的感覺: