阅读 2081

使用 Mobx + Hooks 管理 React 应用状态

时长:预计 15 min

面向:用烦了 Redux 的开发者

代码:仓库

版本:React 16.13 + TypeScript 3.7 + Mobx 5.15 + Mobx-React 6.1

原文:获得更好阅读体验


目标:

实现一个 Todo List 应用如下

image

Step 1️:创建一个 Store Model

//  ./src/stores/todo.ts
import { action, observable, computed } from 'mobx';

export interface ITodo {
  id: number;
  name: string;
  desc: string;
  done?: boolean;
}

let id = 0;

export class TodoStore {
  @observable todos: ITodo[] = [];

  // 利用计算属性计算完成个未完成个数
  @computed get doneCount() {
    return this.todos.filter(todo => todo.done).length;
  }

  @computed get undoneCount() {
    return this.todos.filter(todo => !todo.done).length;
  }

  // 添加一个 Todo
  @action.bound addNewTodo() {
    const i = id++;
    const todo = {
      name: 'new task' + i,
      desc: 'new task' + i,
      id: i,
      done: false,
    };
    this.todos = [...this.todos, todo];
  }

  // 删除一个 Todo
  @action.bound removeById(id: number) {
    this.todos = this.todos.filter(todo => todo.id !== id);
  }

  // 切换 done 状态
  @action.bound toggleStatusById(id: number) {
    this.todos = this.todos.map(todo => {
      if (todo.id === id) {
        todo.done = !todo.done;
      }
      return todo;
    });
  }
}

export const STORE_TODO = 'todoStore';
复制代码

Step 2️:导出 Store

// ./src/stores/index.ts
import { createContext, useContext } from 'react';
import { STORE_TODO, TodoStore } from './todo';

function createStores() {
  return {
    [STORE_TODO]: new TodoStore(),
  };
}

const stores = createStores();

const StoresContext = createContext(stores);

// hooks 使用笔记看这里 -> https://github.com/olivewind/blog/issues/1
const useStores = () => useContext(StoresContext);

function useTodoStore() {
  const { todoStore } = useStores();
  return todoStore;
}

export {
  stores,
  useTodoStore
};
复制代码

Step 3️:使用 mobx-react 将 Store 绑定到组件

// ./src/app.tsx
import React from 'react';
import { Provider } from 'mobx-react';
import Routers from './containers/routers';
import { stores, StoresContext } from './stores';

function App() {
  return (
    // 服务类组件
    <Provider {...stores}>
      {/* 服务函数组件 */}
      <StoresContext.Provider value={stores}>
        <Routers />
      </StoresContext.Provider>
    </Provider>
  );
}

export default App;
复制代码

Step 4️: 在函数组件中使用状态

// ./src/containers/todo-list-fn/index.tsx
import React from 'react';
import { observer } from 'mobx-react';
import { useTodoStore } from '../../stores';
import { Todo } from '../../components';

function TodoListFnPage() {
  const {
    todos,
    undoneCount,
    doneCount,
    addNewTodo,
    removeById,
    toggleStatusById
  } = useTodoStore();
  return (
    <div>
      <div>
        Done: {doneCount}
        Undone: {undoneCount}
      </div>
      <br />
      {
        todos.map((todo) => {
          return (
            <Todo
              key={todo.id}
              todo={todo}
              onRemove={removeById}
              switchStatus={toggleStatusById}
            />
          )
        })
      }
      <br />
      <button onClick={addNewTodo}>Add New</button>
    </div>
  );
}

// 注意这里的 observer
export default observer(TodoListFnPage);
复制代码

Step 5️: 在类组件中使用状态

//  ./src/containers/todo-list-class/index.tsx
import React from 'react';
import { inject, observer } from 'mobx-react';
import { STORE_TODO, TodoStore  } from '../../stores';
import { Todo  } from '../../components';

// 注意这两个装饰器
@inject(STORE_TODO)
@observer
class TodoListClassPage extends React.Component<{ [STORE_TODO]: TodoStore }> {
  addNewTodo = () => {
    this.props[STORE_TODO].addNewTodo();
  }
  render() {
    const {
      todos,
      undoneCount,
      doneCount
    } = this.props[STORE_TODO];
    return (
      <div>
        <div>
          Done: {doneCount}
          Undone: {undoneCount}
        </div>
        <br />
        {
          todos.map((todo) => {
            return (
              <Todo
                key={todo.id}
                todo={todo}
                onRemove={this.props[STORE_TODO].removeById}
                switchStatus={this.props[STORE_TODO].toggleStatusById}
              />
            )
          })
        }
        <br />
        <button onClick={this.addNewTodo}>Add New</button>
      </div>
    );
  }
}


export default TodoListClassPage;
复制代码

Step 6️: 下班🎉