空雲 Blog

Eye catchReact手抜きの為のテクニック集

publication: 2021/11/23
update:2024/02/20

formやinputを非制御で最も簡単に使う方法

useStateやuseRefを使わずに、最小限の記述でフォームに入力したデータをとることが出来ます

Reactの公式ではvalueプロパティを使って制御する形を推奨しています
しかしvalueに対応するsetStateが上位の層にいると、テキストを入力するごとに下位コンポーネントの再評価が行われバケツの水の重力地獄に落ちることがあるので、特段の理由が無ければ私は非制御を推奨します
重力地獄はmemo化で対処は出来るといえば出来るのですが、それなら最初から余計な動きをさせないのが一番です

また、リアルタイムなバリデーションチェックはsetStateに関係なく、onChangeイベントで入力内容のチェックが可能です

1const Form = ({ onSubmit }: { onSubmit: (value: string) => void }) => (
2 <form
3 onSubmit={(e) => {
4 e.preventDefault();
5 const a = e.target['a'].value;
6 const b = e.target['b'].value;
7 onSubmit(JSON.stringify({ a, b }));
8 }}
9 >
10 <input name="a" />
11 <input name="b" />
12 <button>実行</button>
13 </form>
14);
15
16const App = () => {
17 const [value, setValue] = useState<string>(null);
18 return (
19 <>
20 <Form onSubmit={(v) => setValue(v)} />
21 <div>{value}</div>
22 </>
23 );
24};

非制御だけどinputの内容を外から制御する

一番最後のinputをユーザ入力は自由に出来るようにしつつ、ラジオボタンが切り替わったときだけ内容が書き換えます

inputにdefaultValueを使った場合、通常だと後から内容を書き換えることは出来ません
ここで登場するのがkeyです

これを書き換えることによってinputが作り直しになるので、defaultValueが再評価されることになります
keyはループ以外にも用途が存在するわけです

1const App = () => {
2 const [value, setValue] = useState('default');
3 return (
4 <>
5 <label>
6 <input name="a" type="radio" onChange={() => setValue('あいうえお')} />
7 あいうえお
8 </label>
9 <label>
10 <input name="a" type="radio" onChange={() => setValue('かきくけこ')} />
11 かきくけこ
12 </label>
13 {/* ↓の内容を書き換える */}
14 <input key={value} defaultValue={value} />
15 </>
16 );
17};

プルダウンメニュー的なイベント処理

プルダウンメニュー的なコンポーネントを作成したとき、面倒になるのが他にフォーカスが移ったときの非表示処理です
これを最小限のコード量で記述してみます

対象ノードにフォーカスを与えてblurイベントで非表示にするのはよくある流れなのですが、useRefやuseEffectを使わずfocus()を実行しています
refはDOM作成時にインスタンスを渡してくれるので、それを直接利用します

1const App = () => {
2 const [visible, setVisible] = useState(false);
3 return (
4 <>
5 <button onClick={() => setVisible(true)}>表示</button>
6 {visible && (
7 <div
8 style={{ border: 'solid' }}
9 tabIndex={0}
10 ref={(e) => e?.focus()}
11 onBlur={() => setVisible(false)}
12 >
13 表示内容
14 </div>
15 )}
16 </>
17 );
18};

hooksとデータ保存領域

「カウント」を押すとサイレントに押した回数を計測し、「結果」を押すとコンソールに押した回数が出力されます

クラスコンポーネントと違って作業データを保存するにはhookを使うことになるのですが、コンポーネントの再評価が必要なもの以外にuseStateは使ってはいけません
データを入れ替えるごとにコンポーネントの処理が再実行されてしまいます

今回のように情報を記憶しておけば良いだけのデータはuseRefでデータ領域を作りましょう

1const App = () => {
2 const count = useRef(0);
3 return (
4 <>
5 <button onClick={() => count.current++}>カウント</button>
6 <button onClick={() => alert(count.current)}>結果</button>
7 </>
8 );
9};

Hooks側で仮想DOMとデータ両方の制御を行う

子コンポーネントのボタンを押すと親コンポーネントで表示しているカウントが増えます
この動作を普通に書くと、親にカウントの増加を通知するためにコンポーネントからイベントを作成する流れになります
親はそのイベントを受け取り、useStateで作ったdispatchにデータを設定しなければなりません

今回の書き方だと、それらのロジックをhook側に閉じ込めて、親コンポーネントは送られてきた値をそのまま使うだけとなり、記述は最小限です

1const useCount = () => {
2 const [count, setCount] = useState(0);
3 const Counter = useMemo(
4 () => () => <button onClick={() => setCount((v) => v + 1)}>ボタン</button>,
5 []
6 );
7 return { count, Counter };
8};
9
10const App = () => {
11 const { count, Counter } = useCount();
12 return (
13 <>
14 <Counter />
15 {count}
16 </>
17 );
18};

useStateで重要なのはデータ自体では無くDispatchの方

Hooksを利用するとき、useStateを使ったことが無いという人はまずいないと思います
しかし何も考えずに使っていると、本質が分からずになんとなくそう書いているという状態になります

useStateでstateを扱うためのhookなので、重要なのはでは無く、いつもはset何とかと名付けているDispatchの方なのです

値を格納するだけならuseRefで領域を作って格納しても良いのです
ただ、それだと値の変更が通知されず関数が再評価されないので、dispatchを呼び出す必要があるのです
ぶっちゃけてしまえば、データ領域を全てrefにして、useStateは一個だけという構成でもコンポーネントは成り立ちます

1const App = () => {
2 const [, dispatch] = useState<{}>();
3 const count = useRef(0);
4 return (
5 <div
6 onClick={() => {
7 count.current++;
8 dispatch({});
9 }}
10 >
11 {count.current}
12 </div>
13 );
14};

useStateのデータ取得の非同期化

特定のコールバック環境下ではuseStateの値が更新されず、正確な情報を取得するためにはdispatchを呼び出して値を取得する必要があります

利用する値が単体の時は単純に書けるので問題ないのですが、複数のデータを同時に利用したい場合にdispatchの中でdispatchを呼び出したりと、記述する処理が複雑になります

今回のコードはそれを回避するためにPromiseを使ってデータを取り出す処理になります

値を取り出す部分はPromise.allを使った方が適切ですが、今回は分かりやすく書くため使っていません

ちなみにtsxだと<T>がエラーとなるので<T,>と書かなければなりません

1const getStateValue = <T,>(dispatch: React.Dispatch<React.SetStateAction<T>>): Promise<T> => {
2 return new Promise<T>((resolve) => {
3 dispatch((v) => {
4 resolve(v);
5 return v;
6 });
7 });
8};
9
10const App = () => {
11 const [, setA] = useState('');
12 const [, setB] = useState('');
13 const [c, setC] = useState('');
14 const handleClick = useCallback(async () => {
15 setC((await getStateValue(setA)) + (await getStateValue(setB)));
16 }, [/*あえてここは使わず、関数の再構築はしない*/]);
17 return (
18 <>
19 <input onChange={(e) => setA(e.target.value)} />
20 <input onChange={(e) => setB(e.target.value)} />
21 <button onClick={handleClick}>設定</button>
22 <div>{c}</div>
23 </>
24 );
25};

tsxでコンポーネントにgenericsを渡す方法

なんか気持ち悪いですが、タグの中で<>を使います

1const Test = <T,>({ value }: { value: T }) => <div>{value}</div>;
2
3const App = () => {
4 return (
5 <>
6 <Test<string> value="abc" />
7 </>
8 );
9};