🕑

7GUIsで学ぶReact状態管理Jotai | Temperature Converter 編 (2/7)

2022/03/17に公開

はじめに

この記事は、「GUIプログラミングのベンチマークとして提案された7つの課題を題材に、React状態管理ライブラリのJotaiを学んでみよう」というテーマのJotai学習記事の第二回Temperature Converter編です。
完成したコードの解説がメインになります。もしご自身で実装してみたい場合はネタバレになってしまうのでご注意ください。

7GUIsとは

言語やライブラリ系のベンチマークといえば計算速度が評価軸にされることが一般的ですが、この7GUIsはいくつかの指標を軸に7つGUIアプリのお題を用意し、それをベンチマークとして提案しています。
2018年頃に話題となったようで、web系だとReact/MobXやSvelteが実装例として掲載されています。
詳しくはこちら。
https://eugenkiss.github.io/7guis/

7GUIs x Jotai の元ネタ

Jotai作者の@dai_shiさんが過去に取り組んでおり、CodeSandboxで既に実装済みなのでそちらを題材として使わせていただきます。
https://blog.axlight.com/posts/learning-react-state-manager-jotai-with-7guis-tasks/

お題:Temperature Converter

(以下DeepL翻訳)

課題:双方向のデータフロー、ユーザー提供のテキスト入力。
タスクは、摂氏と華氏でそれぞれ温度を表す2つのテキストフィールドTCとTFを含むフレームを構築することである。初期状態では、TC と TF の両方が空である。ユーザーがTCに数値を入力すると、TFの対応する値が自動的に更新され、その逆もまた然りである。TCに数値以外の文字列を入力するとTFの値は更新されず,逆にTCに数値以外の文字列を入力するとTFの値は更新される。摂氏温度Cを華氏温度Fに変換する式はC = (F - 32) * (5/9)、その逆はF = C * (9/5) + 32である。
温度コンバータは、摂氏と華氏の入力間で双方向のデータフローがあり、ユーザー入力の妥当性をチェックする必要があるため、Counterの複雑さを増加させます。良い解決策は、最小限の定型的なコードで双方向の依存関係を非常に明確にすることです。
Temperature Converter は、Programming in Scala の本に載っている摂氏/華氏変換器にインスパイアされています。これは非常に広く使われている例で,時には通貨換算器の形でも使われるので,1000の参考文献を挙げることができます.Counter タスクも同様です。

回答コード

解説

Atoms

Base Atom と その初期化

摂氏温度(Celsius)と華氏温度(Fahrenheit)のatomをそれぞれ定義しています。それぞれを変換する関数、c2fとf2cも定義しています。atomの値の型はStringです。これらはBase Atomとしています。
お題では入力欄の初期状態は空とありますが、ここでは摂氏温度の初期値を用意して初期化し、華氏温度には変換関数c2fを使って初期化しています。

const c2f = (x: number) => x * (9 / 5) + 32;
const f2c = (x: number) => (x - 32) * (5 / 9);

const INITIAL_CELSIUS = 5;
const baseCelsiusAtom = atom(INITIAL_CELSIUS.toFixed(0));
const baseFahrenheitAtom = atom(c2f(INITIAL_CELSIUS).toFixed(0));

お題に忠実になるならば、以下のように書くことが出来ます。

const baseCelsiusAtom = atom('');
const baseFahrenheitAtom = atom('');

Derived Atom

baseCelsiusAtom, baseFahrenheitAtom を元に derived atom の celsiusAtom, fahrenheitAtom を定義します。
2つともやっていることは同じなので代表でcelsiusAtomを見ます。

export const celsiusAtom = atom(
  (get) => get(baseCelsiusAtom),
  (_get, set, value: string) => {
    set(baseCelsiusAtom, value);
    const temp = Number(value);
    if (value && Number.isFinite(temp)) {
      set(baseFahrenheitAtom, c2f(temp).toFixed(0));
    }
  }
);

read関数はbase atomをget()して返すのみです。Counter編で登場したやり方と同じです。
write関数を詳しく見ます。

write関数の第3引数

write関数はget, set, valueの3つを受け取ることが出来ます。Componentから更新用に任意の値を渡すことが出来るのですが、その値が第3引数のvalueに渡ってきます。
以下で言うとe.target.valueがそれにあたります。

// in Celsius component
const [value, setValue] = useAtom(celsiusAtom);
...
setValue(e.target.value)

ロジック内容

中の処理は特に難しいことはしていないです。ここでもgetは未使用です。
以下の2つが実行されます。

  1. 入力値のvalueを自身のbase atomにset
  2. 変換先のbase atomに変換値をset
(_get, set, value: string) => {
  set(baseCelsiusAtom, value); // 1
  const temp = Number(value);
  if (value && Number.isFinite(temp)) {
    set(baseFahrenheitAtom, c2f(temp).toFixed(0)); // 2
  }
}

すべてのatomを更新可能

上記でお気付きかもしれませんが、write関数内ですべてのatomを更新(set)可能です。
もちろん読み込み(get)も例外ではありません。

https://zenn.dev/tell_y/articles/25ecda0b397a22

Components

親ComponentであるAppはこの様になっています。CelsiusとFahrenheit componentを使うだけです。

const App = () => (
  <div className="App">
    <Celsius /> = <Fahrenheit />
  </div>
);

それぞれの component もシンプルです。
定義したcelsiusAtom, fahrenheitAtomをuseAtomを使って読み書き出来るようにします。

const Celsius = () => {
  const [value, setValue] = useAtom(celsiusAtom);
  return (
    <>
      <input value={value} onChange={(e) => setValue(e.target.value)} /> Celsius
    </>
  );
};

const Fahrenheit = () => {
  const [value, setValue] = useAtom(fahrenheitAtom);
  return (
    <>
      <input value={value} onChange={(e) => setValue(e.target.value)} />{" "}
      Fahrenheit
    </>
  );
};

おわりに

複数のatomが双方向に影響し合う様子を体験できたのではないかと思います。扱いたいデータ毎にatomを定義すればよく、それぞれに依存がある場合はその関係性をwrite関数に表現してあげるとよいです。

後は使う側(component側)で目的のatomを扱えばよいだけですね。
propsで渡す必要が皆無で処理内容もatomに押し込める事ができるので、componentの記述もシンプルです。

Jotai Friendsとは

いちJotaiファンとして、エンジニアの皆さんにもっとJotaiを知ってもらって使ってもらいたい、そんな思いから立ち上げたのがJotai Friendsです。

https://jotaifriends.dev/

Jotai Friends

Discussion