LoginSignup
0
0

More than 1 year has passed since last update.

きっかけ

前回の記事を書いてて、改めてReactを掘り下げてみようかと。。
Hook APIをメインに広く浅〜くご紹介していこうと思います。
第一回は、useStateuseRefです。

前提

  • React 16.8〜(HookAPI前提)
  • Reactの基本的な説明はしません
  • TypeScriptで書いてます

お題

色の名前とカラーコードを登録する簡単なフォームを作ります。
2パターン考えてみます。

./src/App.tsx

import { useState } from "react"
import colorData from "./color-data.json";
import UncontrolledForm from "./UncontrolledForm"
import ControlledForm from "./ControlledForm"

interface Color {
  title: string,
  color: string
}

export default function App() {
  const [colors, setColors] = useState<Color[]>(colorData);

  const addColor = (title: string, color: string) => {
    const newColors = [...colors, { title, color }]
    setColors(newColors)
  }
  return (
    <UncontrolledForm
      sendParent={(title, color) => addColor(title, color)}
    />
    <ControlledForm
      onNewColor={(title, color) => addColor(title, color)}
    />
  );
}

パターンA: ./src/UncontrolledForm.tsx

import { useRef } from "react"

interface Props {
  sendParent: (title: string, color: string) => void;
}

export default function UncontrolledForm({ sendParent }: Props) {
  const txtTitle = useRef<HTMLInputElement>(null);
  const hexColor = useRef<HTMLInputElement>(null);

  const submit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    /** validate */
    if (!txtTitle || !txtTitle.current) return;
    if (!hexColor || !hexColor.current) return;

    const title = txtTitle.current.value;
    const color = hexColor.current.value;
    /** notify parent component */
    sendParent(title, color);
    /** reset input values */
    txtTitle.current.value = "";
    hexColor.current.value = "";
  };

  return (
    <form onSubmit={submit}>
      <input ref={txtTitle} type="text" placeholder="color title..." required />
      <input ref={hexColor} type="color" required />
      <button>ADD</button>
    </form>
  );
}

パターンB: ./src/ControlledForm.tsx

import { useState } from "react"

interface Props {
  sendParent: (title: string, color: string) => void;
}

export default function ControlledForm({ sendParent }: Props) {
  const [title, setTitle] = useState("");
  const [color, setColor] = useState("#000000");

  const submit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    sendParent(title, color);
    setTitle("");
    setColor("");
  };

  return (
    <form onSubmit={submit}>
      <input
        value={title}
        onChange={(event) => setTitle(event.target.value)}
        type="text"
        placeholder="color title..."
        required
      />
      <input
        value={color}
        onChange={(event) => setColor(event.target.value)}
        type="color"
        required
      />
      <button>ADD</button>
    </form>
  );
}

一応説明する

  • useState
    • データが更新されると、自身がフックされたコンポーネントを、更新されたデータで再描画するフック
  • useRef
    • 描画されたコンポーネントのDOMノードへの参照を保持するフック

どちらで書くべき?

結果は同じです。違いはsubmit関数の中身です。

  • パターンA: UncontrolledForm
    • 処理手順を列挙していて命令型っぽい
    • 副作用がある(DOMノードのvalue属性を書き換えている)
    • 制御されていないコンポーネントである
  • パターンB: ControlledForm
    • 宣言型っぽい
    • 副作用がない(DOMノードに直接アクセスしていない)
    • 制御されたコンポーネントである

そうです、Reactは関数型プログラミングの影響を強く受けています。
関数型?と思った方は前回の記事をご覧くださいましw

React界隈では、

  • 制御されていないコンポーネント
    • DOMを介してデータにアクセスするコンポーネント
  • 制御されたコンポーネント
    • React(のステート)によってデータが管理されるコンポーネント

と区別され、制御されたコンポーネントのアプローチが推奨されているようです。。
(もちろんuseRefを使うケースもあります)

カスタムフック

さて、useStateを使ってデータを管理した方が良いことは分かりました。
もうちょっとカッコよくしてみます。

言葉の通り、既存のフックをラップしたり、独自のフックを作ったりできる機能です。
今回は<input>タグへのvalueの割り当てと更新(onChangeイベント)を抽象化してみます。

value={title}
onChange={(event) => setTitle(event.target.value)}

React書いている方は分かると思いますが、
この手のフォーム入力とステート管理のパターンは腐るほど出てきますよねw

./src/hook.tsx

import { useState } from "react";

type Hook = (
  initialValue: string
) => [
  {
    value: string;
    onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
  },
  () => void
];

export const useInput: Hook = (initialValue) => {
  const [value, setValue] = useState(initialValue);
  return [
    {
      value,
      onChange: (e) => setValue(e.target.value)
    },
    () => setValue(initialValue)
  ];
};

./src/FormWithCustomHook.tsx

import { useInput } from "./hook";

export default function FormWithCustomHook({ sendParent }: Props) {
  const [titleProps, resetTitle] = useInput("");
  const [colorProps, resetColor] = useInput("#000000");

  const submit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    sendParent(titleProps.value, colorProps.value);
    resetTitle();
    resetColor();
  };

  return (
    <form onSubmit={submit}>
      <input
        {...titleProps}
        type="text"
        placeholder="color title..."
        required
      />
      <input {...colorProps} type="color" required />
      <button>ADD</button>
    </form>
  );
}

いい感じですね。。

  • 重複するコードがなくなった
  • 関数がより抽象化され、何をしているかが明確
  • スプレッド構文で見た目もスッキリ

カスタムフックは使いこなすと可読性がかなり向上すると思います。

次回

親->子コンポーネントにpropsを経由して変数やら関数やらを渡すのって、、、

ぶっちゃけ面倒じゃないですかwww

(筆者はかなり早い段階でそう思いました)
しかもTypeScriptで書いていると、型定義やらなんやらでコード量が増えるのなんのって。。

いわゆるPropsバケツリレーというやつでして、今回の例では気になりませんが
コンポーネントのネストが深くなるとカオスなことになりますw
もっとグローバル(汚染せず)に扱えるステートはないのか、、、

ということで、次回はuseContextです!
個人的に工夫の余地がたくさんあってかなり面白い機能だと思います。。

参考文献

Reactハンズオンラーニング 第2版 ――Webアプリケーション開発のベストプラクティス

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0