はじめに
みなさん、Reactは好きですか?アプリ開発でReact使ってますか?
私はここ数年、ViewライブラリはReact一択でアプリ開発をしています。
さらにはReact Hooksが登場してからというもの、local stateやlifecycle methodを扱うために仕方なく書いていたclassコンポーネントを今ではほぼ書いていません。(classでないと不可能なAPIを扱う場合はしぶしぶ書いていますが)
Functionコンポーネント最高ですね!
さて、超小規模なSPAだとglobalなstateは不要だったりしますが、いざ導入しようかという時、途端にReduxが微笑みかけてきます。(私の場合)
最近はもっぱらReact開発といっても、React Native(Expo)でのモバイルアプリ開発をしており、Reduxを使う機会がなくなっていました。
本記事では、とある案件でglobal stateを扱わなければならず、Reduxではなくreact-trackedというライブラリを採用したので、その経緯や使い心地などを技術的知見すくなめ&個人的主張多めに述べていきます。
Redux採用が辛い場面
Reduxはstate管理ライブラリとしてすごく人気で素晴らしいライブラリです。しかし、導入する開発アプリとReduxの規模がアンバランスなことが多くあるのではないかと感じています。
個人プロジェクトから小規模プロダクションレベルのアプリ、開発スピードを求められるPoCなアプリに対してReduxを採用することは、導入メリットより導入・運用の労力の方が勝ってしまうことが多いのではないでしょうか。
また、いかに優秀なライブラリとはいえ、使い手が十分に設計思想を理解していないとメンテナンスのしにくいアプリへと簡単に変貌してしまいます。
Reduxの公式ドキュメントは手厚くサポートされていますが、初心者の学習ハードルは高く、身近にReduxマスターが居ないことには簡単に心が折れること必至でしょう。(最近は世の中の知見が溜まってきてそこまで苦しまないのかもしれませんが・・・)
中には頑張ったけど辛い経験しか無く、Reduxという単語を聞くだけで拒絶反応が出る人もいるのではないでしょうか。ちなみに自分はこの成分多めな人間です。憎しみがあるわけではありません。
よく見かけるContextの利用について
素のContext APIをglobal stateとして利用する話をよく見かけます。
確かにReduxの代替案としては記述量の少なさや理解のしやすさから魅力的に見えるのは理解できます。しかし、変更があった際はContextが適用されているすべてのコンポーネントツリーが再描画されるAPIなので、頻繁にstateが書き換わるとなると、一概には良い代替案とは言えないかなと思ってます。
もちろん、上記にあげたような特徴を理解しつつ、個人プロジェクトやPoC用など開発スピード重視で気持ちよく書きたいとなると問題はないと思います。
世の中のglobal state管理どうしようか話
「react global state library」 という検索キーワードでググると、こんな記事(Global state with React)が現れます。
素のContextの利用と注意点、使う場合の提案について詳しく解説がされてます。ページ内で動作が確認でき、Contextの挙動を体験できます。
この記事では、Contextをglobal stateに活用するには、がメイントピックですが唯一ライブラリが紹介されています。
それが本記事の本題であるreact-trackedです。
react-trackedとは
Githubのrepositoryはこちら。
アドベントカレンダーの一環で、開発者の@daishiさん自らQiitaでreact-trackedの紹介記事を書いてたりもします。
react-trackedは、雑に言うとHooksとContext APIをベースに作られた何やらすごいチューニングされてるglobal state管理用ライブラリです。
私の言葉で解説するよりご本人の解説を読んで頂くほうが正確なのと、そもそも解説しろと言われても出来ませんので書けませんが、今後Reactの新たなAPIとして正式リリースされるであろうconcurrent modeにも対応してるみたいです。https://github.com/dai-shi/will-this-react-global-state-work-in-concurrent-mode#result
使用例
せっかくなのでシンプルな使い方を載せておきます。
store.js
にglobal stateを準備します。exportしているProvider
はアプリ全体を包むためのコンポーネントです。Provider内のコンポーネント(ここでは、CounterとTextBox)でuseTracked()
を使うと、global stateと更新用の関数を使うことができます。(const [state, setState] = useTracked();
)これだけです。
まずは、用意した4ファイルを見てみましょう。
実際のコードと動作はこちらで確認できます。
// store.js
import { useState } from 'react';
import { createContainer } from 'react-tracked';
const initialState = {
count: 0,
text: 'hello',
};
const useMyState = () => useState(initialState);
export const { Provider, useTracked } = createContainer(useMyState);
// Counter.jsx
import React from 'react';
import { useTracked } from './store';
const Counter = () => {
const [state, setState] = useTracked();
const increment = () => {
setState(prev => ({ ...prev, count: prev.count + 1 }));
};
return (
<div>
<div>count: {state.count}</div>
<button onClick={increment}>+1</button>
<div>random: {Math.random()}</div>
</div>
);
};
export default Counter;
// TextBox.jsx
import React from 'react';
import { useTracked } from './store';
const TextBox = () => {
const [state, setState] = useTracked();
const setText = text => {
setState(prev => ({ ...prev, text }));
};
return (
<div>
<div>text: {state.text}</div>
<input value={state.text} onChange={e => setText(e.target.value)} />
{/* 以下のコメントアウト状態だとstate.counteが変化しようがレンダリングは走りません。
コメントアウトを消すとstate.counteが変化するとレンダリングが走ります。 */}
{/* <div>count: {state.count}</div> */}
<div>random: {Math.random()}</div>
</div>
);
};
export default TextBox;
// App.jsx
import React from 'react';
import { Provider } from './store';
import Counter from './Counter';
import TextBox from './TextBox';
const App = () => (
<Provider>
<Counter />{/* Counter A */}
<hr />
<Counter />{/* Counter B */}
<hr />
<TextBox />
</Provider>
);
export default App;
ポイント
この例で見ていただきたいポイントは、レンダリングの走る様子です。
App.jsxでCounterを2つ(A,Bと呼称)用意しています。AかBのどちらかのCounterでインクリメント用のボタンを押すと、state.countが+1されるので更新され、レンダリングが両方で走ります。
このとき、TextBoxでは全体のstateをuseTracked()から得ているものの、state.countにはアクセスしていません。
なので、state.countが更新されてもTextBoxではレンダリングがはしりません。
(試しにstate.countを書くとレンダリングが走るようになるか確認するためにコメントアウトで用意しています)
もちろん、TextBoxで扱っているstate.textはCounterで参照していないので、state.textが変更されてもCounterではレンダリングが走りません。
素晴らしいですね。ReactのAPIだけ(Contextなど)でこの様に実装すると、先に述べたように1つstateプロパティを変更するとすべてのコンポーネントでレンダリングが走ります。
使ってみた感想
冒頭で述べたように、自身が関わるプロジェクトにreact-trackedを採用してみましたが、非常にストレス少なく導入・利用できました。
ちょっとだけネガティブな表現をすると、少しマジック感が強めな印象を受けるかもしれません。1つのコンポーネントでstateプロパティを書き換えると、そのstateプロパティが使われているコンポーネントでレンダリングが走ります。簡単に全stateにアクセスできるので、扱いを丁寧にしないと、どのstateプロパティがどこで使われて再レンダリングが走っているかが分かりにくくなると思っています。
ただ、Reduxなどよりも学習コストは少なく、React+Reduxで書いていた記述量より圧倒的に少ない記述量で済みます。reducerスタイルでも書けますし、カスタムフックを書きやすくするためのAPIも用意されています。
また、作者が今後のReactを考えてライブラリ設計してくれており、推してくれているので息が長いだろうという安心感もあります。
おわりに
React Hooksの登場から、concurrent modeの登場など目まぐるしく世界が変わっていってます。
そんな中、安定してglobal state管理ライブラリの王座に君臨し続けているReduxは素晴らしく、簡単には支持率を失うことはないでしょう。
ただ、Reduxの設計思想にもあるように、大規模開発向けであるがゆえの仕様の重さがあり、採用するにはストレスを抱えている人が居ることも事実かと思います。(私を含めサンプル1以上)
ちょっと手軽にglobal stateを扱いたい、でももしRedux以外のライブラリを使うとしても安心して使いたい、など、別ライブラリの採用に前傾姿勢だけど不安もある方、react-trackedを使ってみてはどうでしょう。
では、よいReactライフを!