🕒

7GUIsで学ぶReact状態管理Jotai | Flight Booker 編 (3/7)

2022/03/22に公開

はじめに

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

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/

お題:Flight Booker

(以下DeepL翻訳)

挑戦: 制約
タスクは、「片道フライト」と「往復フライト」の2つのオプションを持つコンボボックスC、開始日と帰着日をそれぞれ表す2つのテキストフィールドT1およびT2、および選択したフライトを送信するためのボタンBを含むフレームを構築することである。Cの値が "return flight "の場合、T2が有効になる。Cの値が "return flight "で、T2の日付がT1の日付より厳密に前の場合、Bは無効である。無効になっていないテキストフィールドTに不正な書式の日付がある場合、Tは赤で表示され、Bは無効になる。Bをクリックすると、ユーザーが選択したことを知らせるメッセージが表示されます (例: "あなたは2014年04月04日に片道航空券を予約しました。")。初期状態では、Cは "one-way flight "という値を持ち、T1とT2は同じ(任意の)日付を持つ(T2は無効であることが示唆される)。
Flight Bookerの焦点は、一方ではウィジェット間の制約をモデル化し、他方ではウィジェット内の制約をモデル化することにある。このような制約は、GUIアプリケーションとの日常的なインタラクションにおいて非常に一般的です。Flight Bookerの優れたソリューションは、制約をソースコード上で明確、簡潔、明示的にし、多くの足場の後ろに隠さないようにします。
Flight Bookerは、SodiumのFlight Booking Java例から直接インスピレーションを受けていますが、Flight Bookerの焦点は特殊/カスタムウィジェットではないため、特殊な日付選択ウィジェットではなく、日付入力にテキストフィールドを使用するという簡略化が施されています。

回答コード

解説

Atoms

createDateAtom()でstartDateAtomとreturnDateAtomを定義

行きと帰りの日付を扱うatomを作る為の関数がcreateDateAtomです。

export const startDateAtom = createDateAtom();
export const returnDateAtom = createDateAtom();

関数はコチラ。
関数内で base atom を用意してその derived atom を返しています。関数からatomを作る際にはこのパターンが多いです。

const createDateAtom = () => {
  const INITIAL_DATE = new Date();
  const baseDateAtom = atom({
    date: INITIAL_DATE as ReturnType<typeof parseDate>,
    str: formatDate(INITIAL_DATE)
  });
  const dateAtom = atom(
    (get) => get(baseDateAtom),
    (_get, set, dateStr: string) => {
      const date = parseDate(dateStr);
      set(baseDateAtom, {
        date,
        str: date ? formatDate(date) : dateStr
      });
    }
  );
  return dateAtom;
};

詳しく見ていきましょう。

関数内で初期値の日付を用意する=atomがマウントされたタイミングの日付になりますね。

const INITIAL_DATE = new Date();

Base atom は以下のように定義されています。Date | nullstring の日付両方を持っています。

const baseDateAtom = atom({
  date: INITIAL_DATE as ReturnType<typeof parseDate>,
  str: formatDate(INITIAL_DATE)
});

Derived atom は以下のように定義されています。
Read関数はそのまま base atom の値を返しています。
Write関数では日付のstringを受け取り、base atom に set 出来るようにしています。

const dateAtom = atom(
  (get) => get(baseDateAtom),
  (_get, set, dateStr: string) => {
    const date = parseDate(dateStr);
    set(baseDateAtom, {
      date,
      str: date ? formatDate(date) : dateStr
    });
  }
);

flightOptionAtom

片道か往復かの状態を保持するatomを定義します。
TypeScriptの場合、atom<型>(...)と書くことが出来ます。

export const flightOptionAtom = atom<"one-way flight" | "return flight">(
  "one-way flight"
);

selectの状態を保持する役目がありますが、bookAtomのread関数内で参照されています。

bookAtom

bookAtomはこれまでとは少し毛色の違うatomです。第1引数、第2引数それぞれで見てみます。

Read関数から返される値は、チケットの予約ボタンをクリック可能かのboolean値です。
条件判定のために各atomをgetしています。

  (get) => {
    const flightOption = get(flightOptionAtom);
    const startDate = get(startDateAtom).date;
    const returnDate = get(returnDateAtom).date;
    const isValid =
      startDate !== null &&
      (flightOption === "one-way flight" ||
        (returnDate !== null && startDate <= returnDate));
    return isValid;
  },

Write関数では予約処理が定義されています。set関数が使われていません。予約処理の非同期関数として使われています。
実際にはwindow.alertの箇所がリクエストに置き換わることになります。バックエンド側での予約の成功/失敗を受け取り、その結果を表すatomにsetすることも可能ですね。

  (get, _set) => {
    const startDate = get(startDateAtom).date;
    if (startDate === null) {
      return;
    }
    if (get(flightOptionAtom) === "one-way flight") {
      window.alert(
        `You have booked a one-way flight on ${formatDate(startDate)}`
      );
      return;
    }
    const returnDate = get(returnDateAtom).date;
    if (returnDate !== null) {
      window.alert(
        `You have booked a return flight from ${formatDate(
          startDate
        )} to ${formatDate(returnDate)}`
      );
      return;
    }
  }

bookAtomは、アクションがメインでその実行可否を状態で持つatomでした。

Components

これまでの記事と比較しても特に変わった箇所は無いので手短に。
各コンポーネントで必要なatomを扱うだけですね。bookAtomはこの様に使われています。
propsが全く登場していません。

const BookButton = () => {
  const [isValid, book] = useAtom(bookAtom);
  return (
    <div>
      <button disabled={!isValid} onClick={book}>
        Book
      </button>
    </div>
  );
};

const App = () => (
  <div className="App">
    <FlightOptionSelect />
    <StartDateField />
    <ReturnDateField />
    <BookButton />
  </div>
);

おわりに

atomの作り方が勉強になったテーマだったのではないでしょうか。
これまでの7GUIsは以下です。

https://zenn.dev/tell_y/articles/6436b8afa724a5

https://zenn.dev/tell_y/articles/769d171804e059

Jotai Friendsとは

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

https://jotaifriends.dev/

Jotai Friends

Discussion