🤮

【大長編】地獄のリファクタリング〜第三章 メシマズフォームはレシピに習え〜【フロント編】

2025/02/18に公開

なんでお前が書いてんの?(半ギレ)

https://zenn.dev/lxdesign_blog/articles/c40708cdd87878
ここで詳細記事は神々が書くと言ったな。
あれは嘘だ。
ウワァァァァァ

まぁここに至るまでにもいろいろあったわけですが、端的に説明します。

あるプランニング時、個別記事を待ち望む下人のもとに、フロントエンドを得意とする先生がいらっしゃいました。
子曰く、「僕にとっては当たり前の内容なんで、知らんかった人に書いてもらえばええんとちゃいます?」


……

ぐぅの音も出ない。

というわけで、本記事では私が生み出してしまったおどろおどろしいクソフォームをどうやって浄化したかについて、淡々とお話していければと思います。
ちなみに、ご指導くださった-というか、ほぼ書き直してくれました師は、大阪出身の優しいイケメンです。

初期のフォームがひどかった話

では、ここからが本題です。

諸君、メシマズの話をしよう。

私は料理が出来ないメシマズなわけですが、なぜ私のメシはマズくなってしまうのでしょうか?
答えは簡単。

「レシピを知らない」からです。

レシピを知らないと、味のしないパスタで飢えを凌ぐ羽目になるわけですね。
その上、何も知らずに余計な味を足したりするから、まぁたちが悪い。

では、フォームについてはどうでしょうか?
これも例に漏れません。なんのレシピも知りません。
保守性の高いものに、なるはずもありません。


制作・著作
━━━━━
 ⓃⒽⓀ

おっと、これで記事が終わったら意味ないな。
まずは呪いのレシピから解説するか。

自らの首を締めた呪いのレシピ

あらかじめ申し上げておきますが、この章に掲載されているのは「呪いのレシピ」です。
数年−いや、数ヶ月も経たずに、自らのクソ実装をボロカスに叩かれたいという、特殊な嗜好をお持ちの方にしかおすすめできません。

さて、覚悟はいいですね?

全☆略

レシピって言っても、ゴミカスだからね。いらないよね。
完成したものがこちらになります。

映す価値なし
import React, { useState } from "react"
import { useAtom, atom } from "jotai"

// 値管理用のstore
const valueAAtom = atom("")
const valueBAtom = atom("")
const valueCAtom = atom("")
const valueDAtom = atom("")
const valueEAtom = atom("")
// 以下延々と続く

// ときにはderived atomでバリデーション
const valueCErrorAtom = atom((get) => {
  const valueC = get(valueCAtom)
  return valueC === "" ? "必須項目です" : ""
})
const valueDErrorAtom = atom((get) => {
  const valueD = get(valueDAtom)
  return valueD === "" ? "必須項目です" : ""
})

// 各種値入力用のfield
const CursedFieldForValueA: React.FC = () => {
  const [valueAError, setValueAError] = useAtom("");

  const handleChangeA = (event: React.ChangeEvent<HTMLInputElement>) => {
    setValueA(event.target.value);
  }

  // ときにはuseEffectでバリデーション
  useEffect (() => {
    if (valueA === "") {
      setValueAError("必須項目です")
    } else {
      setValueAError("")
    }
  }, [valueA])

  return (
    <input
      value={valueA}
      onChange={handleChangeA}
    />
  );
};
// こんな感じのフィールドが以下延々と続く

// 最終的なまとめフォーム
const CursedForm: React.FC = () => {
  const [valueA, ] = useAtom("");
  const [valueB, ] = useAtom("");
  const [valueC, ] = useAtom("");
  const [valueD, ] = useAtom("");
  const [valueE, ] = useAtom("");

  const handleSubmit = () => {
    let formData = new FormData()
    formData.append("valueA", valueA)
    formData.append("valueB", valueB)
    formData.append("valueC", valueC)
    formData.append("valueD", valueD)
    formData.append("valueE", valueE)

    fetch("https://example.com", {
      method: "POST",
      body: formData
    })
  }

  return (
    <>
      <div>
        <CursedFieldForValueA />
        <CursedFieldForValueB />
        <CursedFieldForValueC />
        <CursedFieldForValueD />
        <CursedFieldForValueE />
      </div>
      <button onClick={handleSubmit}>送信</button>
    </>
  );
};

export default CursedForm;

??「全くちがうものですからね。簡単でしたよ。」
??「...大丈夫だよな?」
??「絶対アカンフォームは、投稿者がかつて作ったB(バカ)のフォームです!
??「ポンコツじゃねぇか!」
ボウン

すでに吐きそうになっている皆様、申し訳ございません。
エチケット袋をご用意の上、読み進めていただけますと幸いです。

このフォームの絶対アカンポイント

まずは絶対アカンポイント。
最初のフォームをちゃんと見てみると、それはもうクソポイントの雨あられ。
すでに食欲も夏バテ並、季節外れの減退を見せているとは思いますが、とりあえず見ていきましょう。

formタグがない

コードを見てくださった方はすぐにこう思われたでしょう。
「あれ?誤植かな?」と。
残念、仕様です

想定されたフォームの仕様として、フォームの送信ボタンがフィールドの隣の別要素に置かれる、というものがありました。
フィールドたちのスクロールに合わせ、ボタンを追従させる、あれ。
それを実現するために、何を思ったか、formタグを使わずにボタンを独立させ、onClickでフォームの要素を送信しようとしたんですよね。

聡明な皆様はもうお気づきでしょう。
丸ごとフォームタグでくくれば、formのonSubmitを活用できますよね。
何よりReactであれば、そのへんをよしなに対応してくれるイカしたライブラリが、いっぱいあるではありませんか。(呆れ)

すみません、馬鹿な内容で。
ですが、実装当時の私はライブラリなど利用せず、あまつさえ調べることさえできていませんでした。
もっとも、その必要性も全く理解してなかったので論外なんですけどね。

ともかく、私同様の浅瀬チャプチャプ勢の皆様は、

  • HTMLの基礎的な構造
  • どんな巨人が肩を貸してくれるのか

など、きちんと調べましょう。
プロダイバーの皆様には、ぜひ浅瀬の民にこのあたりまでご指導いただければ…
ヒィッ!チョウシノッテスミマセン!!

にわかなAtomの使い方

今回のシステム実装では、Store管理にjotaiを利用しています。
https://jotai.org/
このライブラリはとても直感的で取り回しが良く、素晴らしいStore管理システムです。
それでいてこのコード…
下手くそ(モンスター)なのは、ライブラリじゃなく、、、ユーザーなのか?

下手くそポイントはままありますが、一番の問題は、利用の動機です。
実装が進むにつれ、フィールドが増えていく。
そんな中で私はこう思いました。

「バケツリレーめんどくさくね?」
確かにバケツリレーはコードの見通しを悪化させることもあるでしょう。
が、バケツリレーそのものは悪では無い、見通しが悪化することこそが問題なのです
Storeの利用動機で言えば、 「複数コンポーネントにまたがる情報をスマートに管理したい」 が理由で有るべきです。
あれ?ってバケツリレー回避はそう言えなくも無いのか…??
そんなことも知らずに、フィールドの値を無理にAtomで管理した結果、次のようなゴミカスポイントが堆積していきました。

再利用できないField

今回の開発では、保守性を高めるため、Atomic Designを採用しました

皆様「保w守w性w m9(^д^)プギャー」

そうだよねそうだよね。
なんでこうなっちゃったんだろうね。
なにがおかしかったんだろうね。

本項こそ件のイケメンが個別記事を書いています!
こんなクソ記事は光の速さで離脱し、まずはぜひ、こちらを読んでください!
https://zenn.dev/lxdesign_blog/articles/18022bf4fd45bb

で、フォームレベルで言えば、FieldとAtomが密結合になることで、再利用できないFieldが量産されていた、という課題がありました。
そうなると、どのフォームでなんの要素が扱われているか、だんだんわからなくなってくるわけです。
そして、重要なのが 「なんの要素が」 ってとこ。
そう、バリデーション・エラーハンドリングでも地獄が引き起こされていたんです。

統一感の無いバリデーション

サンプルゴミコードを見てくださった方は解ると思います。
なんかバリデーション、キモいよなぁ?
最初は何もわからず、useEffect でひたすら入力値を監視していました。
まぁ、要件によっては当然このルートもあるんですけどね。
何もわからずってのがまずい。
だからderived atomも活用してみよう、みたいな話も出てきて、状況が混乱するわけです。

採用したことが無い方向けに軽く解説しておくと、jotaiにはderived atomと呼ばれる神システムがあります。
atomAが変更されると、それをトリガーにderivedAtomAが更新される、的な。
詳しくはこちら。
https://jotai.org/docs/guides/composing-atoms

この機能自体はめっちゃ偉いんです。
コンポーネントからロジックを切り離し、それをstoreと結合する()ルートを取れるので。

問題は、 じゃあどっちにするの? って話。
ときにはコンポーネントにロジックが書かれ、かたやどこかではstoreにバリデーションが押し込まれ。
あるときは別のコンポーネントで…もう辞めるか(白目)

とまぁこんな具合に、なんのルールもルーツもすらもなく、様々なリソースに紐づくstoreやフィールドが爆増。
そこの見えないヘドロの中で、ゴミコードが百鬼夜行することと相成ったわけでございます。

さて、ここまでの3項目、「にわかなAtomの使い方」「再利用できないField」「統一感の無いバリデーション」は、ゴリゴリの密結合となっております。
こちらでも循環参照上等のスパゲッティ記事が生成されていますね。
まるで成長していない…

だが、ここからようやく主人公が登場しますよ!
しばし遅れを取りましたが、今や巻き返しのときです。

え、今からでも入れる保険があるんですか!?

一見するとすでに致命傷。
汚泥はうず高く、強烈な異臭を放つ。
その苛烈な状況に、開発メンバーのストレスは当の昔に氾濫危険水位を超えていました。
誰もがこのそびえ立つウ○コに辟易し、今日の夕食はカレーかな、などと考えていたその時、ついに救世主が降臨したのです!
今すぐメシアをフォローしよう!
↓↓↓↓↓
https://zenn.dev/kei4429

呪いのフォームを美味しい料理に変えた手法

預言者(以下Yさん)は、当初のコードをほぼほぼ置き換える形でのリファクタリングを敢行。
今では、リファクタで作ったコンポーネントを基にフォームを構築し、明るい開発ライフが実現されました。
ここでは美味しいカレーを作るまでの流れを、先生に代わりご紹介いたします。

なお、当時のリファクタの進め方とは大きく異なります。
実際のリファクタは、様々な要素が絡み合い、長年積み重ねられた遊戯王のプロダクトの歴史[1]を感じさせるものでした。
本稿ではフォームのリファクタに絞り、端的な表現を目指します。
リファクタは一見複雑そうだけど、やれば複雑だぜ!!

ライブラリの導入

お料理をするにあたり、まずもって必要なものはなんでしょう?
食材?愛情?

そうだね調理器具だね
当初の開発では、ろくにライブラリも使ってなかったのでね。
いわば「泥だらけの野菜を丸かじり」と言える状況でした。
まずはお鍋とか包丁とか買わなきゃ。

ということで、今回使っていく調理器具はこちらになります。

  • react-hook-form
  • yup
  • storybook
  • aspida/axios
  • openapi2aspida

あれ?なんかプロっぽいぞ??
とりあえず、いっこずつみてくか。

react-hook-form
みんな知っている超有名ライブラリ(知らんかった)。
Reactのformにおける値管理をよしなにやってくれるやつだね。
競合どころだとformikとかがあるのかな?
この記事なんかが比較してくれてる(人任せ定期)!
fieldをControllerなるコンポーネントでくくってあげる(?)と、stateとかを使わずとも値を管理してくれる。っぽい!
で、特に偉いと思ったのが useFormContextでformに関するデータをuseContextライクに引っ張ってこれる点。
既存のhooksみたいに扱えて直感的だし、このあとのコンポーネントづくりでも大活躍でした。

yup
react-hook-formと組み合わせて、バリデーションをよしなにやってくれるやつだね。
似たようなツールだとzodとか??
誰か違いを教えてくれる人…いた!助かる!
今回はYさんより、「まぁ、僕も使ったことありますし。結構柔軟で、いろんなバリデーションも組みやすいんで、yupでええんとちゃいます?」という神託を賜りましたので、yup1択でした。
で、なるほど感あったのがカスタムルールの設定。
かな入力オンリーとか、日付の選択範囲を絞るとか、使用頻度の高いバリデーションありますよね?
これらをあらかじめ別ファイルで定義して、直接バリデーションをかけているファイルから参照することで、コードの見通しがクリアになるとともに、バリデーションのズレが一挙になくなり彼女もできて、収入もUPしました!
なんかちょいちょいハマった記憶もありますが、それを補ってあまりある柔軟性と透明性です!

storybook
地味に強力だったのがこれ。
千切りキャベツ作りたいときだけ使う燕三条産のスライサー、みたいなライブラリです。
この子のおかげで、作ったコンポーネントをatom,molecule単位でレビューに出せるようになりました。
今回の開発はいわば、これまでに作られた腐りきったカレーを鍋ごと捨てて、すべて作り直すようなものでした。
その点、食材単位での細かな確認ができたこと、なにより視覚的・チケット単位での単一責任化ができたことが、この後の開発にも非常に良い影響を与えてくれたことに、疑いの余地はありません。

aspida/axios
フリーダムガンダム
機動戦士ガンダムSEED FREEDOM公式より布教[2]

一番強力な兵器−兵器群−でした。
実質フリーダムガンダムです
aspidaについて詳しいことを知りたい人は、公式とかこの記事とか見ておけばいいんじゃないかな?
要は 「型安全にRESTAPIにリクエスト投げてくれる」 ってスグレモノです。
で、何が偉いかって話なんだけど、やっぱりアホみたいなミスが減ったってところに尽きるかなぁ…。
導入前は、「URL間違ってた…」とか「APIと入力値の整合性が取れない…」とか、フロントの半分外側で半端なトラブルが乱舞していました。
でも、このaspidaを導入してからあら不思議。
メソッドチェーンよろしく、.を打っているだけで、エンドポイントのリンクをどんどん推測してくれる。
そしてなにより、APIへのリクエストで使うaspida用の型指定をyupに取り込んであげれば、フィールドのバリデーションがエンドポイントの要求とずれるなんてことはなくなります!
いやぁ素晴らしい、これで一気に楽になりましたね!
??「これが人の夢、人の望み、人の業!」

…えー、テンションを上げている私を尻目に、皆様はこう思ったことでしょう。
「で、その型指定は誰が書くんだい?」と。
大丈夫です、安心してください。
当然私のような無能が書くわけがありません。
これこそが、aspidaがガンダムと言える所以なんですね。

openapi2aspida
ガンダムが宇宙空間でお料理をするためには、必要な装備がありますね。
そうだねミーティアユニットだね(錯乱)
ことaspidaに関しては、このopenapi2aspidaが強化兵装に当たります。
こいつの威力がまたえげつない。
つまり、API側で用意したOpenAPIのファイルを使い、フロントにおける大量の型定義ファイルを一撃で殲滅生成してくれるのだ!!!

先に、浅瀬の民向けのOpenAPIの解説()をば。
かんたんに言ってしまうと、「APIの仕様書の規格」です。それも世界標準の。
それをswaggerとかいうイカしたツールとかで、色々ごにゃごにゃ書くと、なんかどのエンドポイントにどうやってアクセスすると、何が返ってくるかとかわかるんだって。

あ、この辺とか見ると触れるかも。
ふむふむ…へー(わかってない)

で、今回はこのOpenapiファイルをAPI側でガッと作って、フロント側でコマンド一閃!!
無能投稿者は、この仕様に従った、ぬるぬる開発ライフを送るだけ、って寸法よ♪

「結局APIが大変になっただけでは?」
そう思われたプロの皆様。ご安心ください。
なんともう一柱の神たる弊社のCTOが、ゴリゴリ開発が進行しているAPIにOpenAPIをさらっと導入!
ファイルを一括生成してくれたのです!

もしまだお読みで無い方は、こんなクソ記事を光の速さで(ry
詳しくはこちらの記事をどうぞ!
https://zenn.dev/lxdesign_blog/articles/e36126cc2b6b05

いやぁ、この辺の導入とか解説とか、もっと詳しいのあったほうがいいよね。
需要あるなら、このAspida系統の導入だけで1記事書いて「ほしい」なぁ…

共通コンポーネントの準備

いや〜、実に感激。
素人にはもったいない高級機器の数々ではありませんか。
ここからはいよいよ、食材の準備に移っていくやで!
なんやかんや種類も多かったので、代表的なコンポーネントをいくつか紹介します。

テキストフィールド
一番代表的なフィールドですね。
カレーで言えば、豚肉みたいなものです。
コードはこんな感じ。

長過ぎる(記事が)ので折りたたみ
// 今回のプロジェクトはMuiを採用
import TextField, { TextFieldProps } from "@mui/material/TextField";
import { useFormContext, Controller } from "react-hook-form";

type FormTextFieldProps = {
  name: string;
  placeholder?: string;
  fullWidth?: boolean;
  multiline?: boolean;
  disabled?: boolean;
  readOnly?: boolean;
  type?: string;
} & TextFieldProps;

const FormTextField: React.FC<FormTextFieldProps> = (props) => {
  const {
    control,
    formState: { errors }
  } = useFormContext();
  // ホントはもっと明記する変数減らせる
  const {
    name,
    placeholder,
    fullWidth,
    multiline,
    disabled = false,
    readOnly = false,
    type = "text"
  } = props;

  return (
    <Controller
      name={name}
      control={control}
      render={({ field }) => (
        <TextField
          {...field}
          {...props}
          placeholder={placeholder}
          error={!!errors[name]}
          helperText={errors[name]?.message?.toString()}
          fullWidth={fullWidth}
          multiline={multiline}
          disabled={disabled}
          InputProps={{
            readOnly,
            ...props.InputProps
          }}
          label=""
          type={type}
        />
      )}
    />
  );
};

export default FormTextField;

はい、あとはこれをフィールドから呼んでやれば、入力された値がnameに紐づく形で保存されます。
以上。

…え?これだけ??
ふつくしい…

日付選択
ちょっと特殊なフィールド、日付選択。
玉ねぎですね。
これも相当きれいにかけます。

長過ぎる(記事が)ので折りたたみ
// 日付を扱うためにdayjsを採用
import { Dayjs } from "dayjs";
import dayjs from "libs/dayjs";

import DatePicker from "@mui/x-date-pickers/DatePicker";

import { useFormContext, Controller } from "react-hook-form";

type FormDatePickerProps = {
  name: string;
  minDate?: Dayjs;
  monitoredTargets?: string[];
};

const FormDatePicker: React.FC<FormDatePickerProps> = (props) => {
  const {
    control,
    trigger,
    formState: { errors }
  } = useFormContext();
  const {
    name,
    minDate,
    monitoredTargets
  } = props;

  return (
    <Controller
      name={name}
      control={control}
      render={({ field }) => (
        <DatePicker
          {...field}
          onChange={async (e) => {
            field.onChange(e);
            // 他のフィールドに応じてバリデーションをかけたいときに差し込み
            if (monitoredTargets) {
              await monitoredTargets.forEach((target) => {
                trigger(target);
              });
            }
          }}
          error={!!errors[name]}
          helperText={errors[name]?.message?.toString()}
          width="100%"
          minDate={dayjs(minDate)}
        />
      )}
    />
  );
};

export default FormDatePicker;

…必要な要素が全部引数or Context経由で渡ってきている…。
これなら一回作ったら、何も考えずに使い回せるじゃん。ばなな。
ほぼ話すことなさそうだけど、一応もう一個ぐらいみとく??

チェックボックス(複数選択)
なんだ?にんじん、とかか??
まぁ複数選択、ってのがちょっと変わってますね。

長過ぎる(記事が)ので折りたたみ
import React from "react";
import {
  Checkbox,
  FormControl,
  FormHelperText,
  FormGroup
} from "@mui/material";
import { useFormContext, Controller } from "react-hook-form";

type FormCheckboxGroupProps = {
  name: string;
  // 選択肢とラベルを引数として渡す
  options: {
    label: string | React.ReactNode;
    value: string | number;
  }[];
  disabledValues?: Array<number | string>;
};

const FormCheckboxGroup: React.FC<FormCheckboxGroupProps> = ({
  name,
  options,
  disabledValues
}) => {
  const {
    control,
    formState: { errors },
    setValue,
    getValues
  } = useFormContext();

  // チェックボックスの変更を処理
  const handleChange = (
    value: string | number,
    event: React.ChangeEvent<HTMLInputElement> 
  ) => {
    // event.target を HTMLInputElement にキャストする
    const { checked } = event.target;
    const currentValues = getValues(name) || [];
    const newValues = checked
      ? [...currentValues, value]
      : currentValues.filter((v: string) => v !== value);
    setValue(name, newValues, { shouldDirty: true });
  };

  return (
    <FormControl error={!!errors[name]} component="fieldset">
      <Controller
        name={name}
        control={control}
        render={({ field }) => (
          <FormGroup
            sx={{
              display: "flex",
              flexDirection: "column",
              width: "100%",
              gap: ".5rem"
            }}
          >
            {options.map((option) => (
              <Checkbox
                key={option.value}
                label={option.label}
                checked={
                  field.value ? field.value.includes(option.value) : false
                }
                // イベントハンドラーを適切に修正
                onChange={(e) =>
                  handleChange(
                    option.value,
                    e as React.ChangeEvent<HTMLInputElement>
                  )
                }
                disabled={
                  disabledValues && disabledValues.includes(option.value)
                }
              />
            ))}
          </FormGroup>
        )}
      />

      <FormHelperText>{errors[name]?.message?.toString()}</FormHelperText>
    </FormControl>
  );
};

export default FormCheckboxGroup;

react-hook-formのsetValueを使って値を反映させている分、若干コードが膨らんでいるようには見えますが。
やはりuseContextを使うことで、処理がコンポーネントに閉じ込められているのが偉い。
nameとoptionsを渡すだけで、fieldの値管理ができます。

あれれ~おかしいぞ~
完成度が高すぎて、書くことがない(節穴並みの感想)
実運用上は、このコンポーネントはmoleculeにあたり、atom単位で色々調整している部分もあります。
また、フィールド用のラベルを入れたり、その他必要なコードはもう少し足されています。
が、おそらくmolecules内部におけるfieldsの総数は、リファクタリング前の1/5程度にはなっているはずです。
そして何より、要件レベルの調整が入らない限り、このコンポーネントそのものには手を入れる必要がない―つまり、考える必要すらないのが本当に偉い!
この実装以降、フォームを組むときはこの共通コンポーネントを拾ってくれば良くなったので、考えることも実質1/3程度になっているんじゃないでしょうか?(バカの計算)

いよいよフォームの作成

さて、長らくお待たせいたしました。
ようやくこれまで準備してきた食材を、お鍋に投入するときです!
では…

全☆略

お鍋に食材をいれてしばらく煮込み、途中でカレールーを(中略)配膳したものがこちらになります!
…あれ?略しすぎたか??

長過ぎる(記事が)ので折りたたみ
import type React from "react";
// 内部で送るデータを整形したいなんだりしてる
import aspida from "libs/aspida";

import FormTextField from "components/molecules/FormTextField";
import FormSubmitButton from "components/molecules/FormSubmitButton";

import { FormProvider, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
// 自動生成した型定義
import { Methods } from "api/api/v1/clean";

const CleanForm: React.FC = () => {
  const cleanFormSchema: yup.ObjectSchema<Methods["post"]["reqBody"]> = 
  yup.object().shape({
    valueA: yup.string().required("valueAは必須です"),
    valueB: yup.string().required("valueBは必須です"),
    valueC: yup.string().required("valueCは必須です"),
    valueD: yup.string().required("valueDは必須です"),
    valueE: yup.string().required("valueEは必須です")
  });

  const cleanForm = useForm<Methods["post"]["reqBody"]>({
    resolver: yupResolver(cleanFormSchema),
    mode: "all"
  });

  const handleSubmit = async (data: Methods["post"]["reqBody"]) => {
    await aspida.client.v1.clean.post({ body: data });
  };

  return (
    <FormProvider {...cleanForm}>
      <form onSubmit={cleanForm.handleSubmit(handleSubmit)}>
        <FormTextField
          name="valueA"
          placeholder="valueA"
        />
        <FormTextField
          name="valueB"
          placeholder="valueB"
        />
        <FormTextField
          name="valueC"
          placeholder="valueC"
        />
        <FormTextField
          name="valueD"
          placeholder="valueD"
        />
        <FormTextField
          name="valueE"
          placeholder="valueE"
        />
        <FormSubmitButton
          label="送信"
        />
      </form>
    </FormProvider>
  );
};

export default CleanForm;

…ちょっと見覚えありませんか?
つまり、このコードは序盤で出ていた呪いのフォームのリファクタ版だったんだよ!
な、なんだってー!?

もともとは、再利用不可能なコンポーネントが煩雑に折り重なり、コードを省略しても悪臭が隠しきれていませんでした。
しかし、「moleculesは再利用可能なものだけ」「moleculesではstoreを使わない」など、シンプルなルールを設定することで、filedの数だけあったmoleculesが一気に減少。
同時に、値管理の役割をmoleculesに閉じ込めることで、機能実装の際に変更するポイントも大幅に削減されました

外構部分に目を向ければ、今まで一切手が入っていなかったAPIとの通信部分。
urlやparamsの入力を毎回要求され、外壁はボロボロ。
それも今回、匠たちの手によってaspidaという強力なツールが導入されたことで、フロントとAPIの境界はガッチリ補強されました。
API側からもOpenAPIを導入することで、基礎から打ち直し。
そこにopenapi2aspidaを使って頑強な橋をかけることで、フロントがフロントの役割に集中できるようになりました

また大きな問題として残っていたのが、フィールドに対するバリデーションです。
コンポーネントに密結合なものもあれば、derived atomで実装したもの、全く別の場所で管理されたものまで、もうめちゃくちゃ。
一見すると、もうどうしようも無いかと思われましたが…。
なんということでしょう!
yup + openapi2aspida の組み合わせで、バリデーションの書き方を一本化できたのみならず、APIとの型の整合性を、仕組みレベルで取ることができるようになったではありませんか!

ほとんどゴミ屋敷の立て直しに近い今回のリファクタ。
お味はいかがでしょうか…?

リファクタのポイント〜つまり、食レポ〜

足掛け数ヶ月の時間と、数万行に及ぶリファクタの果て。
気がつくと私は、澄み渡る清流のほとりに立っていました。
川の向こう側では、預言者Yさんが手を振っていらっしゃいます。
ここが三途の川かと思いつつ、美味しそうなカレーの匂いにつられ、両の足で、静かに川を、向こう岸へ。
そのままPCの前に座ると、眼の前には最後の晩餐が。
これまでの苦難と苦労と黒歴史を振り返りつつ、スプーンで一口…

……
………

うまぁい!!!

共通コンポーネントの威力

まず目を引くのが、食材の質の高さです。
詳しい事例は先に示したとおりですが、やはり「フォームのリソースに依存していない」というのが素晴らしい!
あえてもうすこし言語化するなら、「フィールドの内容は普遍的」「フォームの作りは個別的」が、実装のルールとしても明確化されたみたいな感じでしょうか?
具体的には、

  • フィールドでは値管理に責任を持つ→普遍的
  • yupでバリデーション用のSchemaを用意し、それに責任を持つ→個別的
  • 通信はフォーム(or別ファイル)に置き、より大きなレイヤーで責任を持つ→個別的

と言った具合です。
もちろん、先に述べた考える必要性の部分やコンポーネントを作る総数、パフォーマンスのお話もあります。
ですが、個人的にはこの思想レベルの統一ができたこと、それが設計・コードレベルで実行できたことは非常に大きかったと思うところです。

強すぎるライブラリ

??「びゃあ゛ぁ゛゛ぁうまひぃ゛ぃぃ゛やっぱり機械で割った卵は、一味違いますよ〜!」
↑ネタにされていますが、マジで冗談じゃありません

今回のリファクタリングで、これだけ質の高い食材を用意できた理由。
それは明らかに使った調理器具―優秀なライブラリたちのおかげです。

基本はreact-hook-formやyup、強烈なところだとaspida系の兵装まで、多くのライブラリがその得意とする範囲を、きちんと棲み分けてカバーしてくれました
ライブラリそれぞれも素晴らしかったのですが、同時に組み合わせも完璧でしたね。
特にaspida2openapiとyupのかみ合わせには目を見張るものがありました。
他の開発環境でも、積極的に採用していこうと思います。
また、当たり前すぎて忘れがちだとは思うのですが、すべてのベースはtypescriptによる型指定・型推論でしょう。
こんな開発をさせてもらっちゃうと、もう生のjavascriptには戻れないなぁ…。

Yさんからの預言

最後に。
ここまで立派な調理器具を揃え、食材を準備できたのはなぜでしょうか?
明々白々。Yさんのおかげですね
およそ技術とは離れたお話にはなりますが、今回の実装を通して自分より遥かに書ける方からのご指導を受けることが、いかに世界を広げてくれるかを知ることができました。

そもそも序盤で申し上げたとおり、私はreact-hook-formなどの必要性なども、まるで理解していませんでした。
実際こうやって書いてみるとわかるんですけどね、痛いほど。
まっとうな人間であらせられたYさんはこう言っていたわけです。
「僕にとっては当たり前のことなんで」
でも、野菜を洗ったことも無い人間にとっては、お鍋の存在だけでも革命レベルです
今回のリファクタリングにおいて、個人的に一番大きな変化は多分ここだったと思います。
一人独学で書いていた頃を思えば、コードの質は雲泥の差。
「知らんかった人」にこのような機会をくださったことと合わせて、深く感謝申し上げます。

なお、世界が広がったことと技術力の向上は別のお話です。
くれぐれも同一視しないように(すっとぼけ)

まとめ

さて、ここまでお付き合いいただき、本当にありがとうございました。
だいたい内容だけで1万字程度、それがいらんことをもりもり書いたおかげで2万字程度に膨れ上がっています。
もうこの記事がなんの話だったのか、忘れてしまっている人も多いと思うので、主題を確認しておきましょう。

そうだねメシマズの話だね!

確かに私は、昔も今もメシマズです。
ですが、実は今回のリファクタ、大規模修正を加えた場所の多くは私の担当でした
当然、基本となる食材の多くを用意してくださったのはYさんです。
が、それを加味しても、メシマズが作ったとは思えない美味しさだとは思いませんか?

なぜ、私のメシはまずくなってしまったのか。
ここまでの一連の事例から、きれいに()証明されましたね。

答えは簡単。レシピを知らないから。

今回はYさんに、ガッツリレシピを教えていただいたので、なんとかなりました。
一応まとめなんで、レシピの要点だけおさらいしておきましょう。

  1. イケてるライブラリは、噛み合う形でどんどん導入しよう
  2. それぞれのコンポーネントの責務を、思想レベル・設計レベル・コードレベルで分割しよう
  3. できる人に教えを乞おう

まず、今回のライブラリとそのかみ合わせは本当に素晴らしかった。
先に述べたとおりですが、これだけでコードの総量が大きく減ったのみならず、見通しが一気に向上しました。
そして、それらに基づくコンポーネントの再設計。
コードレベルで完結になったのはもちろん、思想レベルに意識が向きました。
ここでの思想・実装ルールによって、このあとの負債生成速度が大きく減ったことは、言うまでもありません。

で、何よりも、だ。
できる人に教えてもらうのが、質を高めるためには、いっちゃん大事や!
まずは知るところから始めないとね。
こんなのにご指導いただき、本当にありがとうございました!

というわけで、「第3章メシマズフォームはレシピに習え」これにて完結でございます。
自己流で実装せず、まずはレシピを調べましょう。
今回のレシピも、どこかで皆さんの参考になったらいいなぁ、なんて思ったりして。

ということで、私も早速レシピを見ながら「たらこクリームパスタ」を実装。
盛大に事故りました。
なんでや。

脚注
  1. ※無駄な脚注です。
    リファクタ開始当時の歴史はわずか2年と少しです。遊戯王の僅か1/12だね(呆れ) ↩︎

  2. ※無駄な脚注です。
    機動戦士ガンダムSEED FREEDOM 4Kブルーレイ、好評発売中!https://www.gundam-seed.net/ ↩︎

LX DESIGN テックブログ

Discussion