upinetree's memo

Web系技術の話題とか。Qiitaも合わせてどうぞ (http://qiita.com/upinetree)

Ruby25の聴講メモ

昨日(2/24)、Ruby25に参加してきました。 最初は小さくお祝いするのかなと思ってたら、予想以上に大きなカンファレンスでびっくりしました。参加できてよかった。 いろいろあって最近仕事でRubyをあまりキメられていないのですが、やっぱ良い言語で良いコミュニティだなと再確認しました。 また、久々に会えた方々とお話できて良かったです。

以下、聞いた講演のメモです(まとめられてないです)。 聞き間違えや解釈の違い等あるかもしれません。

高橋さん「Rubyの1/4世紀」

  • Ruby」の頭文字が大文字なのは、連載「ちょーわかりやすい Perl & Ruby 入門」で「ruby」のように小文字だと「Perl」との並びが悪いので大文字になったらしい
  • かつて主要なパッケージ管理にはRPA, RubyGemsがあり互換性なかった。RailsがRubyGmesを採用したことで普及
  • rubygems.org は Gemcutter が前身
  • パッケージ管理やbundlerなど、Rubyのエコシステムの構築をRailsがリード

Matz「Ruby after 25 years」

  • ソフトウェアの誕生には物理的実体がない。概念的
    • Rubyという概念とは、名前?
    • Rubyという名前が生まれたときが、Rubyという概念の誕生
  • 未来の話してと言われたけどむずかしい。当たるも八卦当たらぬも八卦

この25年をふりかえる

  • プラットフォームとしてのOSはほぼOS。WindowsすらWSL
  • CPUもあんまりアーキテクチャが変わらない
  • プラットフォームは多様性が減少する一方
  • コンピュータの普及、性能の向上、モバイルの台頭
  • サーバサイドでなんでもやる時代ではなくなった。モバイルアプリ、SPA
  • トレンドの変化がある
  • 変化の傾向として、ものすごくスケーラブルに、分散処理

これを踏まえて未来のRubyは?

  • Rubyのコアは変わらないのでは
    • チューリング完全性という最低限は備えているので劇的な変化は必要ない
    • 文法が大きく変わったらRubyとはいえない
  • プログラミング言語のテーマは生産性の向上
    • より早くより簡潔に書ける、頭の中を直接コンピュータに伝えられる
    • 優れた抽象モデルが提供できる
    • 保守性がある
  • 現在のRubyは小さなチームではじめられる

近未来のRuby(Ruby3)

  • 高速、分散、解析
  • Ruby 3x3
  • やればできるゴールではなく、できるかもしれないゴール、わくわくさせるようなことを提示するのが私の役割
  • 言ってみるもんで、案外できそうなところまできた
  • 2020年目標(約束はしない)
  • 過去に学んで、断続的な変化はしない。Ruby3は単なるラベルにする

その先のRuby

  • インタラクティブプログラミング
    • 入力したら警告がポップアップで出てくるみたいな感じ
    • 静的解析技術、実行時プロファイリングなどの発展系
    • 賢いテディベア(テディベアプログラミング)がコンパイラについてくるなど
  • 分散技術の発展とともに、分散処理の抽象化、スケーラブルなアーキテクチャが必要になってきている
    • Webもいい線行っているが万能ではない
    • FaaSが現在では近いと予想
    • Rubyでは、Guildのその先ができるといい
  • 非均質的計算環境対応
    • BigLITTLE、GPGPU
    • FPGAもある種の非均質的環境
    • どう抽象化し、同じシステムとして扱えるようにする?

むすび

  • 人類は25年ではあまり変わらないが、文化は変化する
    • 「コンピュータは楽しい」があたりまえになってほしい
    • 思考ツールとしてのRuby、人間のためのRubyにしていきたい
  • 最大の目標は「プログラミング言語サバイバル」
    • 生き残るために価値、楽しいプログラミングを提供し続ける

松田さん「Ruby on Ruby on Rails

  • RailsRubyの良さを伝えるためのインフラ
  • 先週くらいから6.0を開発中
  • 5.2はコミッタの中では終わったRails
  • これからもRailsは変わり続け世の中を変え続けるので必死こいてバージョンアップして下さい

近藤さん「 RubyとInfrastructure as Code、そして大規模インフラ」

  • Rubyはどう Infrastructure as Code と関わっていくか
  • RubyDSLを作るのに向いている
    • なのでDSLを採用したプロダクトが多い(Cheff, Itamae, Serverspec...)
    • 特にConfigurationのツールが多い
  • 直近のインフラコードの潮流
    • マイクロサービス、コンテナ化、サーバレス
    • ウェブインフラの大規模化、アプリの複雑さが背景
  • 支援団体 Cloud Native Computing Foundation(代表プロジェクト: Kubernetes)
    • この団体の支援プロジェクトの中で、Ruby製は1つだけ(最多はGo)
    • まだクラウドインフラ分野に進出の余地がある
    • mrubyで実現する未来を作っていきたい

石井さん「 mruby、今IoT、組込界隈でこう使われています、最新事例紹介!」

  • 組み込み技術者不足
  • webの人にも組込してもらえたら良いよね、ということでmruby
    • ruby, mruby, mruby/c で世の中のなんでも作れる
    • 試作しやすさ、文字列処理のしやすさ
    • ただし弱点として何msec以内に〜をしなければ、というのを保証できない
    • それ以外のエンタメとかいろいろな面で利用価値がある
  • しかし、なかなか使ってもらえなかった
    • 組み込みは一回製品を出荷したら変更できないのですごく慎重
  • 軽量Ruby 普及・実用化促進ネットワークで技術情報を提供中
  • これからのmruby
    • Ruby2.0をキャッチアップ(現在1.9ベース)
    • グローバル会議やりたい(来年)

田籠さん「Data Processing and Ruby in the World」

データ処理のステップは collect => summarize => analyze => visualize

この世界でのRuby

  • collect
    • fluentd
    • logstash (JRuby)
    • embulk
  • summarize, analyze
  • Rubyを使ったサービス
    • TD
    • Repro
    • FlyData

という感じになっている

これからの機械学習系プロジェクト

他の言語ではすでに実装されているものもあるが、全体のエコシステムで不自由なく使えることがとても大事。

Matz & Miyagawa 未来を語る特別対談

  • MJITは最初はオプトインで提供
    • 今後の性能特性を見てデフォルトにするか判断する
    • (2.6-preview1 がさっきでた)
  • steepによる型推論、別ファイルとしての型情報
    • TypeScriptの型定義ファイルみたいな感じ
    • 型を指定したときの安全感は別の気持ちよさがあるのでは
  • 言語のシンタックスは互換性を保つことを考えると厳しい。もう限界が来ている
  • Ruby3には間に合わないが、パッケージ、ネームスペースの再考したい
    • JSみたいにrequireの返り値がモジュールとか(現在Rubyはtrue)
    • ちなみにrequireは複数の引数を受け取れるが、全部のrqeuireをトランザクションで保証するわけではない

RailsのRoutingの名前付けでハマった

Astrum などのスタイルガイドジェネレータを利用する際、Railsの外からコンパイル済みCSSを読み込みたいことがある。

このとき、アセットパイプラインを利用している場合はdigestを含めて参照する必要があるのだが、Railsの外の世界で、かつ変更に開いていない場合いい感じにdigestを埋め込むことができない。

そこで今回はRailsのコントローラ経由でCSSをリダイレクトしてしまうアイデアで実現したのだが、Routingのリソース名でハマった。

class AssetsController < ApplicationController
  def show
    redirect_to helpers.stylesheet_url('hogehoge')
  end
end
resource :asset, only: :show

こうするとなにがまずいかというと、 asset_url が上書きされてしまうのだ。

従って一部のアセットが正しく取得できなくなる。 SassのURLヘルパーもこのメソッドを利用しているので 1CSSに埋め込んだimageやfontが軒並み表示されてなくて謎だった。

かなりレアケースではあると思うが2Railsでは内部で使われていそうな名前のRoutingは避けようという教訓を得た…。

そして、こういうときにfinalistのような意図しないoverrideを防ぐ手段があればなあと思うのであった。ただ、Ruby, Rails的には受容されない気もするが。

Routingに限らず、動的生成されるメソッドには油断しないようにしたい。


  1. 軽く覗いたところ、 sass-rails が sprockets で定義されたやつを利用しているように見える

  2. 資産(Asset)を定義する際も同様の問題が起こったとの話を聞いた

Jbuilder の generator を無効にする

故あってサンプルコードをたくさん作る必要があり、JBuilderの生成するコードがじゃまになる(けど使うので消せない)ときがあった。 そんなときに行った対策を簡単にまとめてみる。

.json.jbuilder ファイルの生成を無効化

config/application.rb に以下を記述。

    config.generators do |g|
      g.jbuilder = false
    end

経験上、他に不要なものも併せて以下のように記述することが多い。

    config.generators do |g|
      g.system_tests = nil
      g.jbuilder = false
      g.assets = false
      g.helper = false
    end

コントローラのテンプレート上書き

コントローラでは例えば以下のように、content type による振り分けが生成される。

  # POST /users
  # POST /users.json
  def create
    @user = User.new(user_params)

    respond_to do |format|
      if @user.save
        format.html { redirect_to @user, notice: 'User was successfully created.' }
        format.json { render :show, status: :created, location: @user }
      else
        format.html { render :new }
        format.json { render json: @user.errors, status: :unprocessable_entity }
      end
    end
  end

これは以下でRails標準のテンプレートが上書きされるため。

https://github.com/rails/jbuilder/blob/master/lib/generators/rails/templates/controller.rb

無効にするには、以下の手段がある。

Rails 5.0.2 => 5.1.4 へのアップデートで地味に面倒くさかったこと

ActionController::Parameters がpermittedでないときに例外出すようになった

ちゃんと指定してpermitしたほうが良いんだろうけど、ログとかでpermitしなくても問題ない場面やpermit対象が動的に変わるものがある。 そういときに

params[:hoge].to_h

みたいなのはpermittedフラグが立ってないので例外が発生する。

params[:hoge].permit!.to_h
# もしくは
params[:hoge].to_unsafe_h

のようにする。

ActiveRecord::AttributeMethods::Dirty の一部仕様が変わった

save前後で呼び出す attribute methods が変わった。 https://github.com/rails/rails/pull/25337

使い分けは面倒だが、save前後での違いが明確になってスッキリした感じ。

リリースノートにないので注意。

save後

attribute_changed? とかをsave後に利用することがdeprecatedとなった。 after_save コールバック内で呼び出したりするのも同じ。

たとえば以下のような感じに対応する。

  • attribute_changed? => saved_change_to_attribute?
  • attribute_was? => attribute_before_last_save

※ attribute method なので attribute の部分をカラム名で置き換える。nameの場合は saved_change_to_name のようになる。

save前

今までと同じ感じで使える。

ただ、save前用の新しいメソッドも生えてる。 たとえば attribute_changed? に対しては will_save_change_to_attribute? が使える。 新しい方は内部的な扱い方がちょっと違うけどやってることは同じっぽい。

create_table のプライマリキーがデフォルトBIGINTになった

ActiveRecord::Migration[5.1] 以降を継承しているマイグレーションが対象。

ActiveRecord::Migration[5.0] 以前のバージョンのマイグレーションはBIGINTにならないよう互換性の配慮がされている。 具体的には schema.rb での create_table のオプションに id: :serial (pgの場合)が付くようになる。

5.0以前と以後のマイグレーションではプライマリキーの型で違いが出るようになってしまう。 これに対して2通りのアプローチがある。

  1. Migration[5.0] 以前で作ったプライマリキー(及びそれを参照する外部キー)も順次BIGINTにしていく
  2. Migration[5.1] 以降のマイグレーションではすべて id: :integer をつけるルールにする(デフォルトに従わない)

1 が順当そうで、このPR投げた人もおすすめしてる ( http://www.mccartie.com/2016/12/05/rails-5.1.html )。 BIGINTが必要にならない場合は 2 もありなんだけど、ルールを忘れられないようにするか、自動でなんかするかしたほうがいい。

BIGINTとINTが混ざった状態も許容するのも考えられるが、後々の負の遺産となりそうなのでどちらかに統一したほうがよさそう。

互換性の考慮漏れが一部ある(Rails 5.1.4)

ふつうに create_table していれば前述したような互換性対策が走るので問題ないのだけど、一部考慮が漏れているところがある。 具体的にはプライマリキーを add_column :articles, :id, :primary_key のようにして作ったとき、BIGINTになってしまう。 masterでは対応が入っている ようなので、Rails 5.1.5 では問題にならなさそうだけど、 一応それまでは db:migrate:reset によるschema全更新ができないことは注意したい。

Webpacker 3.0 でのWebpackの設定カスタマイズ

Webpacker 3.0 以降ではWebpackの設定を直接書かずに、npmモジュールに定義されるデフォルトを上書きする形になった。 いい感じにデフォルトがそのまま使えて楽になった反面、すでに設定を結構いじったりしている場合にバージョンアップをするのがちょっと面倒だった。

カスタマイズの基本的な方法はドキュメントに書いてある。

https://github.com/rails/webpacker/blob/v3.0.1/docs/webpack.md

が、書いてあること以外で考えたことを以下にメモ。

環境ごとにPlugin設定の一部を上書き

// config/webpack/production.js
const environment = require('./environment');

const UglifyJsPlugin = environment.plugins.get('UglifyJs');
UglifyJsPlugin.options.sourceMap = false;

module.exports = environment.toWebpackConfig();

プラグインのoptionsオブジェクトに入っているのを上書きする。 Webpackプラグインの作りは理解してないけど、おそらくこのoptionsがあるという構造は統一されているのかな。

なお、この例ではプロダクションでソースマップを無効にしている。 (ソースマップ生成って結構パワー使うので)

一度プラグインを消しちゃって追加し直す方法もある。ぜんぜん違う設定にするときは楽かも。

参考: https://github.com/rails/webpacker/issues/771

environment.plugins.delete('UglifyJs')

environment.plugins.set(
  'UglifyJs',
  new UglifyJsPlugin({
    sourceMap: false
  })
)

動作確認は NODE_ENV 環境変数を利用するとかんたん。

$ NODE_ENV=production bin/webpack

staging環境

npmパッケージからデフォルトで提供されるenvironmentsは、 development, test, production のみ。 それ以外の環境はまっさらなEnvironmentオブジェクトがそのまま渡ってくる。

https://github.com/rails/webpacker/blob/v3.0.1/package/index.js#L10

めんどうなのでproductionと同じにしたいのだけれども、デフォルトで参照するenvironmentは環境変数 NODE_ENV で切り分けるため、NODE_ENV=production で起動する以外なさそう。

とはいえstaging固有の設定もしたいわけで、ひとまず以下のようにproduction相当の設定をべた書きする形となった。

const environment = require('./environment');
const webpack = require('webpack');
const CompressionPlugin = require('compression-webpack-plugin');

environment.plugins.set('ModuleConcatenation', new webpack.optimize.ModuleConcatenationPlugin());

environment.plugins.set('UglifyJs', new webpack.optimize.UglifyJsPlugin({
  sourceMap: true,
  compress: {
    warnings: false,
  },
  output: {
    comments: false,
  },
}));

environment.plugins.set('Compression', new CompressionPlugin({
  asset: '[path].gz[query]',
  algorithm: 'gzip',
  test: /\.(js|css|html|json|ico|svg|eot|otf|ttf)$/,
}));

module.exports = environment.toWebpackConfig();

もうちょっとうまい感じの方法はないかな…。

redux-thunk で雑に getState するのは控えめにしたほうが良いかも

たとえばこういう Store があるとき。

const reducers = combineReducers({
  foo: fooReducer,
  bar: barReducer
});

const store = createStore(
  reducers,
  applyMiddleware(thunk)
);

こんな感じの雑さで getState() して現在の state を取っていたとする。

const fooAction = () => (dispatch, getState) => {
  const hoge = getState().foo.hoge;
  dispatch({ type: FOO, hoge });
};

ここでは combineReducers() している通り、 getState().foo.hogefoo キーを指定して書かねばならない。 なぜなら getState() は全体の state を返すので。

でもこうすると特定の reducre に依存した Action Creator になってしまう。 最初のうちは良いかもしれないけど、combineReducers を修正するとか、他の文脈でその Action Creator 使うってときに困る。 つまり Action Creator の使い回しが難しくなる。 (他の文脈で Action Creator を使いまわすというのは気をつけたほうが良いかも。必要に応じて中身のビジネスロジックだけ切り出すほうが適切なこともある。知見ほしい)

こうならないためには Action Creator の呼び出し時に引数で受け取るとよさそう。

const fooAction = hoge => (dispatch, getState) => {
  dispatch({ type: FOO, hoge })
};

// Component
const FooComponent = ({ hoge, fooAction }) => {
  const onClick = () => { fooAction(hoge) };
  return <a onClick={onClick}>Foo</a>;
);

export default connect(
  ({ foo }) => ({ hoge: foo.hoge }),
  dispatch => ({ fooAction: dispatch(fooAction) })
)(FooComponent);

getState を使うのは、どうしてもそうしないと state が取れないってときに割り切る?

どう Store を構成するかっていうのはなるべく Action Creator に入り込まないようにして、Container側で吸収したほうが良さそう。

React の Component 間通信について色々実験してみる

React さんのこといまいちよく理解していなかったので、コンポーネントのやり取りを題材に色々試してみました。 コード例は載っていますが思考実験的なのでかなり雑な点ご了承ください。

対象課題

たとえばモーダル表示切り替えを考える。 DOM構造は下記のようなイメージ。

main
  main-content
  modal-open-button
modal
  modal-content
  modal-close-button

modal は全体に覆いかぶさる要素なので、CSS的に main 要素と同列の構造で扱いたい。 (ルートスタックコンテキストを適用したい)

modal-open-button により modal が開き、 modal-close-button により modal が閉じる。 main コンポーネント(もしくは modal-open-button コンポーネント)と modal コンポーネントの間での相互作用が必要になるが、これらは親子関係にないので props の受け渡しでは単純に表現できない。

SPA (Single Page Application) ではなく、サーバサイドのテンプレートエンジンで render した結果に対してJS制御を付与する。 つまりサーバーサイドのテンプレートエンジンの実行と局所的な React コンポーネントが共存する環境を想定する。

コンポーネントを作成

大本の親(コンテナ)コンポーネントを作り、その子コンポーネントとして含めるアイデア

container
  main
    modal-open-button
  modal
    modal-close-button

container に modal の状態を持たせ、 main や modal コンポーネント内でその状態を変えるコールバックを実行し伝播する形になる。

おそらく最もポピュラーな方法だが、コンポーネントツリーが深くなってくると問題になる。 何をするにしてもルートコンポーネントを辿って反映しないといけない。

また今回の例では他の問題もある。

  • サーバサイドで render する予定だった main 内部まで React コンポーネントに含まないといけない
  • container という責務が曖昧で大きなコンポーネントが作られてしまう

これらは、SPAのような全体をReactコンポーネントを組み合わせたアプリケーションなら問題なさそう。

Flux パターン

有名なやつ。 実装としては Flux や Redux などがある。 公式ドキュメントやサンプルが異様にしっかりしているのと、情報もたくさんあるのでここでは省略。

Flux と Redux 両方試してみた感触としては、Redux を react-redux なしで使い始めるのが最初はわかりやすさの面で良いかも?と思った。 (react-redux はパフォーマンス最適化されてるので必要性が増えてきたら積極的に導入すると良さそう)

Observer パターン

Flux パターンに乗っておけばとりあえずは大丈夫そうだけど、シンプルな用途には大げさ感がある。 そういうときは最小限のエッセンスだけを真似してみても良い。

Flux は Observer パターンと、 PubSub パターンのようなメッセージパッシングを組み合わせて、単方向データフローとなるように整えた感じのやつと考えられそう。 状態を保持する Store とコンポーネントの View を分け、その間を Observer パターンで通信することだけを真似してみる。

下記がその実装例。 各 Component が Observer で、 Store が Observable (Subject) にあたる。

import React from 'react';
import ReactDOM from 'react-dom';
import classNames from 'classnames';

// Observable
class ModalStore {
  constructor() {
    this.state = { active: false };
    this.observers = [];
  }

  activate() {
    this.state.active = true;
    this.notify(this.state);
  }

  deactivate()  {
    this.state.active = false;
    this.notify(this.state);
  }

  registerObserver(observer) {
    this.observers.push(observer);
  }

  notify(state) {
    // TODO: register でコールバックを登録するように実装すると依存が減らせる
    this.observers.forEach(l => {
      l.setState({ active: state.active })
    })
  }
}

class Modal extends React.Component {
  constructor(props) {
    super(props);
    this.handleClose = this.handleClose.bind(this);
    this.state = { active: props.active }
  }

  componentDidMount() {
    this.props.store.registerObserver(this);
  }

  render() {
    const modalClass = classNames(
      'modal',
      { 'modal_active': this.state.active, }
    );

    return (
      <div>
        <div className={ modalClass }>
          <div className='modal__text'>
            <div>active: { `${this.state.active}` }</div>
          </div>
          <div className='modal__close' onClick={this.handleClose}>
            [x] close
          </div>
        </div>
      </div>
    )
  }

  handleClose(e) {
    this.props.store.deactivate();
  }

  static get defaultProps() {
    return { active: false }
  }
}

const ModalActivator = (props) => {
  const handleOpen = (e) => {
    props.store.activate();
  }

  return (
    <div className='modalFull__open' onClick={handleOpen}>
      [x] open
    </div>
  )
};

document.addEventListener('DOMContentLoaded', () => {
  const modalStore = new ModalStore();

  ReactDOM.render(
    <Modal store={modalStore} />,
    document.body.appendChild(document.createElement('div'))
  );

  ReactDOM.render(
    <ModalActivator store={modalStore} />,
    document.getElementById('main')
  );
});

EventEmitter を使った Observer パターン

Observer パターンを自前で用意するよりは EventEmitter みたいなのを使っても良い。

https://www.npmjs.com/package/wolfy87-eventemitter

import EventEmitter from 'wolfy87-eventemitter';

class ModalStore extends EventEmitter {
  constructor() {
    super();
    this.state = { active: false };
  }

  activate() {
    this.state.active = true;
    this.emitChange();
  }

  deactivate() {
    this.state.active = false;
    this.emitChange();
  }

  registerObserver(observer) {
    this.on('change', () => {
      observer.setState({ active: this.state.active });
    });
  }

  emitChange() {
    this.emit('change');
  }
}

表現力が上がったほか、手段が統制された。

なお、 registerObserver(observer) のかわりに addChangeListener(callback) のようにすると、呼び出し側への依存を減らせる。

Mediator パターン

これまでの例には Flux と違ってメッセージ配送を制御する Action や Dispatcher がないので、 Store - Component 間が複雑(多対多とか)になるとコードの治安が悪くなるかもしれない。 そのときは Mediator パターンを使ってもよさそう。というわけで実験。

class ModalStore {
  constructor() {
    this.state = { active: false };
  }

  activate() {
    this.state.active = true;
  }

  deactivate() {
    this.state.active = false;
  }
}

class ModalMediator {
  constructor(store) {
    this.stores = [store]; // 雑に多対多を想定してみる
    this.listeners = [];
  }

  activate() {
    this.stores.forEach(s => { s.activate(); });
    this.notify({ active: true });
  }

  deactivate() {
    this.stores.forEach(s => { s.deactivate(); });
    this.notify({ active: false });
  }

  register(listener) {
    this.listeners.push(listener);
  }

  notify(state) {
    this.listeners.forEach(l => {
      l.setState({ active: state.active })
    })
  }
}

class Modal extends React.Component {
  componentDidMount() {
    this.props.mediator.register(this);
  }

  handleClose(e) {
    this.props.mediator.deactivate();
  }
  
  // ...
}

const ModalActivator = (props) => {
  const handleOpen = (e) => {
    props.mediator.activate();
  }
  // ...
};

document.addEventListener('DOMContentLoaded', () => {
  const modalStore = new ModalStore();
  const modalMediator = new ModalMediator(modalStore);

  ReactDOM.render(
    <Modal mediator={modalMediator} />,
    document.body.appendChild(document.createElement('div'))
  );

  ReactDOM.render(
    <ModalActivator mediator={modalMediator} />,
    document.getElementById('main')
  );
});

うーん、この例ではあまり必要性が実感できない…。 非同期処理とか複数 Store の順序依存とかが出てくるとメリットがわかりやすそう。 そうなる前に Flux, Redux に移れという話になるのかな…。

感想

上記はいづれも粗い実装ではありますが、最低限PDSは守られているので Flux に乗せるのも簡単のはずです。 というか複雑性に対処しようとしていろいろやっていくと Flux に近づいていく気がします。

では最初から Flux でいいのかというとそうでもなくて、状況(複雑性、リソース、スケジュール、コードの寿命)に合わせて引き出しをいくつか持っておくと良さそうです。

そしていろいろ試した結果、こういう一部だけコンポーネントを使うようなケースには、 React じゃなくて Vue 等のほうが向いているな、と思ったのでした…。 次は Vuex を触ってみよう。

参考