きっかけ
前回の記事を書いてて、改めてReactを掘り下げてみようかと。。
Hook APIをメインに広く浅〜くご紹介していこうと思います。
第一回は、useState
とuseRef
です。
前提
- 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です!
個人的に工夫の余地がたくさんあってかなり面白い機能だと思います。。