つくって学ぶ、React x MobXの実装 第1回 MobXによるルーティング

シリーズ1回目はアプリを開発するための環境を整えます。そして、まずアプリの土台となるMobXによるルーティング部分を実装します。decorateなどMobXのv4から追加された機能も利用します。

発行

著者 杉浦 有右嗣 Jamstackエンジニア
つくって学ぶ、React x MobXの実装 シリーズの記事一覧

本シリーズ内で紹介している内容は利用できますが、現在のバージョンでは新しい書式などが追加されているので注意してください。(2020年7月現在)

はじめに

このシリーズでは、WebアプリケーションのView部分を実装するためのライブラリであるReact.jsと、状態管理のためのライブラリであるMobXを使って、簡単なSPAを実装していきます。

実装するSPAであるメモアプリケーションの仕様は、「これから始めるVue.js 2.0 | CodeGrid」シリーズの最終回で実装している、Vuexのサンプルアプリケーションと同様とします。

ReactやMobXにはじめて触れる方だけでなく、普段はVue/Vuexを使っている方も、コードと見比べながら読み進めることで新たな発見があるかもしれません。

ReactとMobX

今回はReactとMobXを組み合わせて使用していますが、どちらも元々はそれ単体でも利用できるライブラリです。そのためReactを使ったWebアプリケーションを実装する場合にMobXが必須というわけでも、またその逆もありません。

ただ、両者の設計思想には親和性が高い部分があります。

view = fn(state)

これはReactの「Viewは状態によって宣言的に表現されるもの」という設計思想です。

Anything that can be derived from the application state, should be derived. Automatically.

これはMobXの設計思想で「アプリの状態に関するすべての事柄は、その状態変更に呼応して自動的に成されるべき」というものです。

これらの思想は競合することなく、MobXによって管理された状態はそのままReactを使ったViewで表現されます。このように単体で独立して動作する部品が、互いを意識せずに使えるという点が、筆者がReactとMobXを選んで使っている最大の理由です。

それぞれのライブラリ単体の特徴については、次の2つの記事を先に読んでおくと、より理解が深まるはずです。

アプリの仕様と実装の進め方

最初に、成果物の仕様を簡単に整理しておきます。

  • URLによる4つのルーティングとページ
    • /: 最新のメモ3件を表示
    • /add: メモの追加
    • /items: メモの一覧
    • /items/:id: 一覧ページ内、選択したメモの編集
  • メモの構成要素は3つ
    • タイトル
    • 日付
    • タグ(空白区切りで複数指定)
  • メモはリスト表示から削除できる
  • メモはメモリ上でのみ管理
    • LocalStorageなどへは保存しない

いわゆるTodoAppと似たシンプルな要件です。

このシリーズの以降の記事は、次のステップに分けて実装・解説していく予定です。

  • 第1回:開発環境の構築とMobXによるルーティング
  • 第2回:Reactの組み込みと一覧の表示
  • 第3回:メモの追加・削除・編集(完成)
  • 第4回:先を見据えたリファクタリング

第1回は開発を進めていくための環境構築と、MobXを使ったルーティングを実装していきます。

利用するライブラリ

まずは依存関係の整理です。

シリーズ名のとおり、React.jsとMobXの関連ライブラリを利用します。

Vuexの記事では、ルーティングにVue.js公式のルーター実装であるvue-routerを使っていましたが、こちらではReactにもMobXにも関連がないシンプルなルーターライブラリであるkrasimir/navigoを使って、ルートも状態の1つとして管理していくことにします(なおアプリの実装によりフォーカスするため、URLの切り替えにはPush-Stateではなく簡易なハッシュ(/#/)を使用します)。

また、開発環境およびビルドツールとしては、babelwebpack(および必要な周辺ライブラリ)を利用します。

package.jsonに表される最終的な依存関係は次のようになりました。

package.json

{
  "dependencies": {
    "mobx": "^5.0.0",
    "mobx-react": "^5.1.2",
    "navigo": "^7.1.2",
    "react": "^16.4.0",
    "react-dom": "^16.4.0"
  },
  "devDependencies": {
    "babel-core": "^6.26.3",
    "babel-loader": "^7.1.4",
    "babel-preset-react": "^6.24.1",
    "webpack": "^4.10.2",
    "webpack-cli": "^3.0.3",
    "webpack-dev-server": "^3.1.4"
  }
}

今回は、メモアプリケーションの開発・ビルドに最低限必要なもののみを利用しています。

必要に応じてESLintPrettierなどのツールを追加してもかまいません。

開発環境の構築

続けて、アプリケーションをビルドする部分を準備します。

今回はwebpackでReactを使ったアプリケーションをビルドするための最低限の設定をしていきます。

まずはwebpack.config.jsです。

webpack.config.js

const path = require('path');

module.exports = (_env, argv) => {
  const stepDir = path.resolve(argv.context || './1');

  return {
    context: stepDir,
    entry: './src/main.jsx',
    output: {
      path: `${stepDir}/dist`,
      filename: '[name].bundle.js',
    },
    module: {
      rules: [
        {
          test: /\.jsx?$/,
          exclude: [/node_modules/],
          use: ['babel-loader'],
        },
      ],
    },
    resolve: {
      extensions: ['.js', '.jsx'],
    },
    devServer: {
      contentBase: `${stepDir}/dist`,
      watchContentBase: true,
      host: '0.0.0.0',
      port: 9876,
    },
  };
};

合わせて、次の内容を.babelrcに記述します。

.babelrc

{
  "presets": ["react"]
}

前述のとおり、ステップバイステップで実装を進めていくため、各記事それぞれの時点での成果物を、それぞれのディレクトリに分けて開発していきます。

そのために、1つの設定ファイルで各ディレクトリをビルドできるよう、変数stepDirを使って設定を記述しています。そのほかは、特別なことをしていません。

webpackの設定については、次のシリーズも参考にしてください。

あとはwebpackをnpm-scriptsから利用できるよう、package.jsonで指定します。

package.json

{
  "scripts": {
    "dev": "webpack-dev-server --mode development",
    "build": "NODE_ENV=production webpack --mode production"
  },
}

これで開発の準備は整いました。

次のコマンドで開発用のサーバーがlocalhost:9876に立ち上がります。

npm run dev

なお、記事中のソースコードは次のディレクトリ構造を前提としています。

├── 1 // 今回のファイルを格納するディレクトリ
│   └── src // アプリ本体のjsx、jsファイルを格納するディレクトリ
├── .babelrc
├── package.json
├── webpack.config.js

MobXでルーティング

さて、ここからが今回の記事の本題です。

記事タイトルのとおり、MobXを使ってアプリの土台となるルーティング部分を実装していきます。

エントリーポイントを用意する

全体の処理の流れを掴むため、完成したコードを先に紹介します。まずはエントリーポイントであるmain.jsxです。

main.jsx

import { autorun } from 'mobx';
import Store from './store';
import { initRouter } from './router';

const store = new Store();
initRouter((name, params) => store.updateRoute(name, params));

const $root = document.getElementById('app');
autorun(() => {
  const { route, currentRoute } = store;
  const { name, params } = route;

  $root.innerHTML = `
    <nav class="menu">
      <a href="#/" class="${currentRoute.root ? 'router-link-active' : ''}">最新</a>
      <a href="#/items" class="${currentRoute.items ? 'router-link-active' : ''}">一覧</a>
      <a href="#/add" class="${currentRoute.add ? 'router-link-active' : ''}">追加</a>
    </nav>
    <div class="contents">
      <p>現在のルート: ${name}</p>
    </div>
  `;

  console.log(`route changed to ${name} with ${JSON.stringify(params)}`);
});

上から順にざっくり読んでいきます。

  • まず状態を管理するストアを初期化
    • この中でMobXを使ってObservableな値を用意する
  • 合わせてルーターを初期化し、ルートの変更時のハンドラを紐付け
    • store.updateRoute()がハッシュの変更に合わせて呼ばれるように
  • storeを使って、Viewを描画する
    • Observableな値に変更があるたびに、DOMツリーをまるごと書き直す
    • 後にReactを使ったコードに置き換える部分

コード中に登場するautorun()は、MobXを使って定義した値に変更があった場合に、引数として渡したハンドラが自動で実行されるものです。

その状態の変更は、ルーターに紐付けられたURL(ハッシュ)の変更によってもたらされます。a要素のそれぞれをクリックすると、コンソールにログを表示すると同時に、現在のルートが変更されます。

ストアを用意する

状態を管理するストアは、store.jsとして用意しています。

前述のとおり状態が変化した場合は、autorun()を実行させる必要があるためMobXを使用してStoreを定義します。

store.js

import { decorate, observable, computed, action } from 'mobx';

class Store {
  constructor() {
    this.route = {
      name: '',
      params: {},
    };
  }

  get currentRoute() {
    return {
      root: this.route.name === '/',
      items: this.route.name.startsWith('/items'),
      add: this.route.name === '/add',
    };
  }

  updateRoute(name, params) {
    const { route } = this;

    route.name = name;
    route.params = params;
  }
}

decorate(Store, {
  route: observable,
  currentRoute: computed,
  updateRoute: action,
});

export default Store;

このStoreクラスでは、routeオブジェクトにルーティング用の値を保持するようにしています。

route.name/itemsなどのルート名、route.paramsには/items/:idのようなルートにおいて、可変する部分の値を取得するようにしています。

3種類あるルートのうち、現在地を表すためにcurrentRouteをGetterとして用意し、あとはルーターがハッシュの変化を検知した際にコールするupdateRoute()から構成します。

さて、MobXを使ったこのストアで特筆すべきは、次の部分です。

router.js(抜粋)

decorate(Store, {
  route: observable,
  currentRoute: computed,
  updateRoute: action,
});

decorate()は、MobXのv4から追加されたAPIで、そのクラスをどのように振る舞わせるかを決定するものです。仕様がまだ確定していないDecorators構文の代わりに、同じようなイメージで使用することができます。

ここではrouteオブジェクトをobservable(後述するMobXのハンドラでフックすることができる)にしつつ、GetterであるcurrentRoutecomputedupdateRoute()メソッドを、actionobservableな値を変更するメソッドであることを明示)として指定しています。

decorate()で何もしなかった場合は、プレーンなJavaScriptと何ら変わりありません。開発中に思ったように動作しないなどの場合は、記述漏れがないか確認するようにしてください。

ルーターを用意する

最後にルーターです。router.jsとして用意します。

router.js

import Router from 'navigo';

const routes = [
  '/',
  '/items',
  '/items/:id',
  '/add',
];

export function initRouter(callback) {
  const router = new Router(null, true);

  for (const route of routes) {
    router.on(
      route,
      params => callback(route, params || {})
    );
  }

  router.resolve();

  return router;
}

krasimir/navigoは、ルートとハンドラを指定しておくことで、ハッシュの変更時にマッチするものがあった場合にそのハンドラを実行するだけの機構です。

今回は要件どおり、次の4つのルートを指定しました。

router.js(抜粋)

const routes = [
  '/',
  '/items',
  '/items/:id',
  '/add',
];

それ以外のルートは無視するようになっています。

このinitRouter()に引数として渡しているcallbackは、上述のストアのrouteを更新するためのメソッドでした。つまりこれでハッシュが変更され、定義しておいたルートにマッチした場合は、併せてストアの状態が更新される仕組みが完成したわけです。

おわりに

ここまでで完成したコードは、デモリポジトリの/1ディレクトリ内で確認できます*。

*注:ファイル追加

コードがバンドルされて書き出されるdistディレクトリにindex.htmlstyle.cssを追加しています。

次回の記事では、Viewを描画する部分のコードをReactを使ったものに置き替えつつ、あらかじめ用意したメモのデータから一覧を表示できるようにしていきます。

杉浦 有右嗣
PixelGrid Inc.
Jamstackエンジニア

SIerとしてシステム開発の上流工程を経験した後、大手インターネット企業でモバイルブラウザ向けソーシャルゲーム開発を数多く経験した。2015年にピクセルグリッドへ入社し、フロントエンド・エンジニアとして数々のWebアプリ制作を手掛ける。2018年に大手通信会社に転職し、低遅延配信の技術やプロトコルを使ったプラットフォームの開発と運用に携わっていたが、2020年ピクセルグリッドに再び入社。プライベートでのOSS公開やコントリビュート経験を活かしながら、実務ではクライアントにとって、ちょうどいいエンジニアリングを日々探求している。

この記事についてのご意見・ご感想 この記事をTweet