🤧

へっぽこエンジニアが MobX を学ぶ

2022/09/14に公開約8,800字

業務で状態管理ライブラリを使用したアプリケーションを作ることになったので、
useStateuseEffect くらいしか使えない私が気合いで MobX を使ってみる。

MobX を選んだ理由

  • シンプルかつ簡単に状態管理ができるから。
  • 身近に得意な方がいたから。
  • 本当は Redux を使いたかったけど、難易度が高すぎるとのことで見送り(無念)

ドキュメントを読んでみる

まずは入門や概要を読んでノリを掴む。

概念イメージ

  1. まず第一に、アプリケーションの状態があります。オブジェクトのグラフ、配列、プリミティブ、アプリケーションのモデルを形成するリファレンス。これらの値は、アプリケーションの「データセル」である。
  2. 第二に、派生物があります。基本的には、アプリケーションの状態から自動的に計算されるすべての値です。派生値や計算値には、未完成の TODO の数のような単純なものから、TODO を HTML で視覚的に表現したような複雑なものまで、さまざまなものがあります。スプレッドシートで言えば、アプリケーションの数式やグラフのようなものです。
  3. リアクションは導出と非常によく似ています。主な違いは、これらの関数が値を生成しないことです。その代わり、何らかのタスクを実行するために自動的に実行されます。通常は、I/O に関連するタスクです。DOM を更新したり、ネットワークへのリクエストを適切なタイミングで自動実行したりします。
  4. 最後に、アクションがあります。アクションとは、ステートを変更するすべてのものです。MobX は、あなたのアクションによって引き起こされるアプリケーションの状態のすべての変更が、すべての派生とリアクションによって自動的に処理されることを確認します。同期的に、そしてグリッチフリーで。

https://mobx.js.org/getting-started

https://mobx.js.org/the-gist-of-mobx.html

ちなみに先生は公式サポート 🔥

https://www.npmjs.com/package/eslint-plugin-mobx

タスク管理アプリを作ってみる

準備

とりあえずお決まりの。

zsh
npm create vite@latest learn-mobx -- --template react
zsh
npm init @eslint/config@latest
zsh
npm i -D prettier eslint-config-prettier

可愛くなるやつも入れる。

zsh
npm i @chakra-ui/react @emotion/react @emotion/styled framer-motion

GitHub を参考にパッケージを入れてみる。
全部で5つあるみたい(参考
React と併用することが多いだけで、React の使用は必須ではないらしい。

  1. mobx
  2. mobx-react-lite
  3. mobx-react
  4. mobx-undecorate
  5. eslint-plugin-mobx

React 用は軽量版もあるようだが、とりあえず通常版を入れる。
React でしか使用しない場合でも mobx はいるっぽい。

zsh
npm i mobx mobx-react

折角用意してくれてるので、先生も入れる。
これで準備完了。

zsh
npm i -D eslint-plugin-mobx
eslintrc.cjs
module.exports = {
  env: {
    browser: true,
    es2021: true,
    node: true,
  },
  extends: ["plugin:react/recommended", "plugin:mobx/recommended", "standard", "prettier"],
  overrides: [],
  parserOptions: {
    ecmaVersion: "latest",
    sourceType: "module",
  },
  plugins: ["react", "mobx"],
  rules: {},
};

実装

入門を真似して作っていく。

https://mobx.js.org/getting-started

シンプルな todo store を作る

この段階ではまだ MobX は関与しない。

stores/todoStore.js
class TodoStore {
  todos = [];

  get completedTodosCount() {
    return this.todos.filter((todo) => todo.completed === true).length;
  }

  report() {
    const nextTodo = this.todos.find((todo) => todo.completed === false);
    return (
      `Next todo: "${nextTodo ? nextTodo.task : "none"}". ` +
      `Progress: ${this.completedTodosCount}/${this.todos.length}`
    );
  }

  addTodo(task) {
    this.todos.push({
      task: task,
      completed: false,
    });
  }
}

export const todoStore = new TodoStore();

動作確認用のコードを書く。

App.jsx
import React from "react";
import { ChakraProvider } from "@chakra-ui/react";
import "./tests/todoTest";

function App() {
  return <ChakraProvider />
};

export default App;
tests/todoTest.js
import { todoStore } from "../stores/todoStore";

todoStore.addTodo("read MobX tutorial");
console.log(todoStore.report());

todoStore.addTodo("try MobX");
console.log(todoStore.report());

todoStore.todos[0].completed = true;
console.log(todoStore.report());

todoStore.todos[1].task = "try MobX in own project";
console.log(todoStore.report());

todoStore.todos[0].task = "grok MobX tutorial";
console.log(todoStore.report());

おお、ちゃんと動いた。

Browser Console
Next todo: "read MobX tutorial". Progress: 0/1         todoTest.js:4
Next todo: "read MobX tutorial". Progress: 0/2         todoTest.js:7
Next todo: "try MobX". Progress: 1/2                  todoTest.js:10
Next todo: "try MobX in own project". Progress: 1/2   todoTest.js:13
Next todo: "try MobX in own project". Progress: 1/2   todoTest.js:16

MobX を導入する

実際に MobX を組み込んでみる。
手動で report() を呼ぶのをやめて、状態 が変化するたびに report() が自動的に呼び出されるように変更する。

具体的には TodoStore で行われている全ての変更を MobX に観測させる。

  1. constructor を追加し、makeObservable を使うことで観測できるようになる。
  2. 観測対象である todosobservable、派生先となるメソッドに computed を付与する。
  3. computedgetter にしか適用できないので、report()getter に変更する。
  4. autorun() で変更検知時に任意の処理を実行できるので、ここに report() を突っ込む。
  5. 厳格モードが有効な時は、actionobservable な値を更新しないと怒られるので action を定義する。
stores/todoStore.js
import { observable, autorun, computed, action, makeObservable } from "mobx";

class TodoStore {
  todos = [];

  constructor() {
    makeObservable(this, {
      todos: observable,
      completedTodosCount: computed,
      report: computed,
      addTodo: action,
      toggleCompleted: action,
      changeTask: action,
    });
    autorun(() => console.log(this.report));
  }

  get completedTodosCount() {
    return this.todos.filter((todo) => todo.completed === true).length;
  }

  get report() {
    const nextTodo = this.todos.find((todo) => todo.completed === false);
    return (
      `Next todo: "${nextTodo ? nextTodo.task : "none"}". ` +
      `Progress: ${this.completedTodosCount}/${this.todos.length}`
    );
  }

  addTodo(task) {
    this.todos.push({
      task,
      completed: false,
    });
  }

  toggleCompleted(todo) {
    todo.completed = !todo.completed;
  }

  changeTask(todo, task) {
    todo.task = task;
  }
}

export const todoStore = new TodoStore();
tests/todoTest.js
import { todoStore } from "../stores/todoStore";

todoStore.addTodo("read MobX tutorial");

todoStore.addTodo("try MobX");

todoStore.toggleCompleted(todoStore.todos[0]);

todoStore.changeTask(todoStore.todos[1], "try MobX in own project");

todoStore.changeTask(todoStore.todos[0], "grok MobX tutorial");

問題なく動いている。
前回と比べてログの5行目がなくなっているがこれは正しい挙動で、
データは変わっているが出力内容が同じため出力されなくなった。

Browser Console
Next todo: "read MobX tutorial". Progress: 0/1        todoTest.js:15
Next todo: "read MobX tutorial". Progress: 0/2        todoTest.js:15
Next todo: "try MobX". Progress: 1/2                  todoTest.js:15
Next todo: "try MobX in own project". Progress: 1/2   todoTest.js:15

React と統合する

React に組み込んでみる。
とはいえ実装はとても簡単で、なんとコンポーネントを observer で囲むだけ。
あとは onClick 等でよしなにするだけ。恐ろしい 😱

App.jsx
import React from "react";
import { ChakraProvider, Container } from "@chakra-ui/react";
import { observer } from "mobx-react";
import { TodoList } from "./components/TodoList";
// import "./tests/todoTest";

const App = observer(function () {
  return (
    <ChakraProvider>
      <Container my={8}>
        <TodoList />
      </Container>
    </ChakraProvider>
  );
});

export default App;
components/TodoList.jsx
import React from "react";
import { observer } from "mobx-react";
import { Button, Heading, List, ListItem, Text } from "@chakra-ui/react";
import { todoStore } from "../stores/TodoStore";
import { Todo } from "./Todo";

export const TodoList = observer(() => {
  const onAddTodo = () =>
    todoStore.addTodo(prompt("Enter a new todo:", "coffee plz"));

  return (
    <>
      <Heading size={"md"} mb={4}>
        {todoStore.report}
      </Heading>
      <List borderY={"2px"} borderColor={"gray.100"}>
        {todoStore.todos.map((todo, index) => (
          <ListItem key={index}>
            <Todo todo={todo} hasBorder={index > 0}></Todo>
          </ListItem>
        ))}
      </List>

      <Button onClick={onAddTodo} mt={4}>
        New Todo
      </Button>
      <Text mt={2} fontSize={"sm"}>
        (double-click a todo to edit)
      </Text>
    </>
  );
});

components/Todo.jsx
import React from "react";
import { observer } from "mobx-react";
import { Center, Checkbox, HStack, Text } from "@chakra-ui/react";
import { todoStore } from "../stores/TodoStore";

export const Todo = observer((props) => {
  const { todo, hasBorder } = props;
  const onChangeTask = () =>
    todoStore.changeTask(todo, prompt("Task name", todo.task) || todo.task);

  const onToggleComplete = () => todoStore.toggleCompleted(todo);

  return (
    <HStack
      onDoubleClick={onChangeTask}
      py={3}
      borderTop={hasBorder && "2px"}
      borderColor={"gray.100"}
      cursor={"pointer"}
    >
      <Center gap={3}>
        <Checkbox onChange={onToggleComplete} isChecked={todo.completed} />
        <Text>{todo.task}</Text>
      </Center>
    </HStack>
  );
});

完成

良い感じ ✨

https://github.com/taichi221228/learn-mobx

動作イメージ

まとめ

かなりシンプルかつ簡単に状態管理ができた 🙌🏻
少ないコードで簡単に導入できそうなので、機能がシンプルなアプリケーションを作成する際に重宝しそう。

参考にさせていただいた情報 🙏🏻

Discussion

ログインするとコメントできます