React 狀態管理使用指南

本文也在個人博客平臺:betamee.github.io 可見,歡迎來交流~css

目錄

1. 引言

在日趨複雜的 Web 應用中,狀態管理一直是一個很重要的話題。React 技術棧中也衍生出了很多解決方案,如 Redux、Mobx,而且隨着 React Hooks 的發佈,帶來全新的代碼編寫模式,在狀態管理庫的使用上也出現了不少新變化。html

爲了指明 React 狀態管理,我寫了幾個 examples,並比較一下其中的使用差別。前端

開發同一個 Todo App,但使用方式分爲如下四種:node

  • 使用類模式的 React + Redux
  • 使用類模式的 React + Mobx
  • 使用 Hooks 模式的 React + Redux
  • 使用 Hooks 模式的 React + Mobx

所有代碼發佈在 Github 的這個倉庫 GitHub - BetaMee/react-usage-examples: React Usage Examples 上,可供參考。react

2. 狀態管理之 Redux 解決方案

Redux 是很火的一個技術解決方案,甫一推出就備受關注。Redux 由 Flux 演變而來,但受 Elm 的啓發,避開了 Flux 的複雜性。它提供了一套簡單但有效的 API,提供可預測化的狀態管理。但在學習難度上很是陡峭,由於它帶來新的概念和約定,只有理解通了,才能夠更好的應用它。git

要理解 Redux,能夠分爲如下幾個點:github

  1. 全部的 state 都以一個對象樹的形式存儲在一個單一的 store 中
  2. UI 是這個 state 樹某一時刻狀態的映射
  3. 改變 state 樹的惟一方式是觸發一個 action,一個描述發生什麼的對象
  4. 至於對 state 樹的具體改變操做,是由叫 reducers 的純函數來實現
  5. 更新後的 state 樹又映射爲新的 UI

redux-flow.jpg

圖 2.1 Redux Flow

由上能夠總結爲Redux 三大原則:typescript

  • 單一數據源
  • state 是隻讀的
  • 使用純函數來執行修改

理通上述概念基本上能夠入門 Redux 了。但只有上述概念沒法解決 Web 應用更復雜的場景,如異步加載數據更新界面。編程

這就須要涉及到 Redux 中的中間件概念。json

使用中間件,咱們能夠在 state 變化的時候對數據進行監控、截獲、更改,還能夠拓展異步操做。這就給了咱們很大的自由,充分發揮 Redux 的潛力。

Redux 的中間件實質上是一個函數,是對store.dispatch方法進行了改造,在發出 Action 和執行 Reducer 這兩步之間,添加了特定功能。

redux-flow-middleware.jpg

圖 2.2 Redux Flow Middleware

Redux 社區已經發展了不少成熟的中間件,如 redux-thunk、redux-soga,只須要按需使用便可。

具體可參考:redux#ecosystem#middleware 和 中文版本的 cnredux#ecosystem

3. 狀態管理之 MobX 解決方案

與 Redux 類似的,前端出現的另外一個狀態管理方案是 MobX。

MobX 的官網是這樣介紹的:

MobX 是一個通過戰火洗禮的庫,它經過透明的函數響應式編程 (transparently applying functional reactive programming - TFRP) 使得狀態管理變得簡單和可擴展。MobX背後的哲學很簡單: 任何源自應用狀態的東西都應該自動地得到。 其中包括UI、數據序列化、服務器通信,等等。

mobx-flow.png

圖 3-1 MobX Flow

其實相比 Redux 的強規則約定,MobX 明顯更簡單靈活,MobX 的核心原理是經過 action 觸發 state 的變化,進而觸發 state 的衍生對象(Computed value & Reactions)。開發者只須要定義須要 Observe 的數據和由此衍生的數據(Computed value)或者操做 (Reactions),剩下的更新天然就交給 MobX 去作就能夠了。

MobX 和 Redux 比較大概體現出這些差別:

  1. Redux 是函數式而 MobX 是面向對象
  2. Redux 的 state 每次返回新的數據,理想狀態是 immutable 的,MobX 則至始至終保持一份引用
  3. Redux 可支持數據回朔,MobX 因爲只有一份引用,沒法支持回朔
  4. Redux 約定單一數據源,MobX 能夠將 state 樹拆分進多個 store 中
  5. Redux 須要中間件處理異步,MobX 能夠直接使用 async/await 來處理

總之,二者相比,MobX 確實比 Redux 上手更容易些,而且不須要寫不少樣板代碼,但 Redux 的這種「複雜」不必定是無用的,強約定的規則能夠爲大型項目,複雜數據狀態管理提供可靠的支持,而 MobX 在某些場景下確實能夠比 Redux 項目提供更高效的選擇。

但也不能簡單地就將 Redux 複雜化,MobX 簡單化,仍是要看適合的場景。咱們應該更關注它們解決什麼問題,它們解決問題的關注點,或者說實現方式是什麼,它們的優缺點還有什麼,哪個更適合當前項目,以及項目將來發展。

4. 這個 example 長啥樣

咱們作的這個 Todo 只涉及到簡單的增刪功能:

todoapp.gif

5. Classic React App with Redux

接下來我將具體介紹四個 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 的文件夾下配置 actionreducers 項,而後統一導出:

│       └── 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 的精髓:

  1. action 純函數:描述如何改變狀態(是什麼)
  2. 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 組件,mapStateToPropsmapDispatchToProps 如其名字所示,是將 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 函數。這樣,整個應用依賴清晰,數據流變化清晰可調控,對於排查問題很是方便。

6. Classic React App with Mobx

接下來咱們看看使用類模式 + 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 包中的 observerinject 高階函數將數據注入:

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 裝飾器就能夠了。簡單明瞭。

7. React Hooks App with Redux

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
複製代碼

這個文件幹了這些事:

  1. 定義包含 state 和 action 的 AppState
  2. 使用 AppState 初始化 React.createContext,生成全局的 ContextStore
  3. 使用 React.useReducer 生成具體的 todoState、todoDispatch
  4. 經過 ContextStore.Provider 組件將 todoState、todoDispatch 注入整個應用

能夠看到,todoStatetodoDispatch 纔是整個代碼的重點,由於這裏包含着整個應用的 state 和 action,而生成 state 和 action 的 React.useReducer 中的參數:todoReducerinitalTodoState,就是正常的 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),很是方便。

8. React Hooks App with Mobx

相比起 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

9. 總結

四個 example 寫下來,個人我的感覺:

  1. Hooks 確實比類模式簡化不少,是 React 技術棧的將來趨勢
  2. MobX 也不必定比 Redux 簡約,好比 Hooks 下 MobX 接入也稍顯麻煩,不像 Redux 那樣直接內化爲官方 API
  3. Redux 的 reducer 和 action 的功能能夠由類模式無痛遷移到 Hooks 中
  4. Typescript 是真的 power!

參考

相關文章
相關標籤/搜索