今年からReactを勉強してサイト制作やアプリ開発を行っているのですが、よくあるフォームの実装でミスした経験と対策を共有していきます。
「React フォーム」などとGoogleやYoutubeで検索すると日本語だけでも大変多くの情報を得ることができ、自分もそれらを参考にして作っていました。
結論それで問題ないのですが、思ってもないトラブルに遭遇しハマったことがありました。
よくよく自分なりに調べてみると自分のような初心者向けに公開されている情報には一部バグの要因になる要素があることが分かりました。
自戒の意味はもちろん、同じようなReact初心者の方に共有するため本記事を書いていきます。
ちなみに動画も作っているので良ければどうぞ。
よくあるReactによるフォーム実装の例
まず自分もハマった、よくある作り方から紹介していきます。
GoogleやYoutubeで公開されている作り方の8割近くがこちらの方法になっています。
import React, { useState } from "react";
const Form = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
console.log({
email,
password
});
};
return (
<div>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="email">メールアドレス</label>
<input id="email" type="email" value={email}
onChange={(e) => setEmail(e.target.value)} />
</div>
<div>
<label htmlFor="password">パスワード</label>
<input id="password" type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div>
<button type="submit">送信</button>
</div>
</form>
</div>
);
};
export default Form;
useStateを使ったメールアドレス、パスワードを入力するフォームになっています。
本記事では送信機能までは割愛するので、onSubmitの関数はconsole.logで入力内容を出力するだけにしています。
誤解のないように説明しておくと、上記の書き方で正常に動作しますしフォームに限った話としては全くもって問題ありません。
あくまで「問題はないけどバグ要因が潜んでいる」という形で以下の2点を説明していきます。
①入力されるたびにレンダリングが実行される
まず1点目のバグ要因としては、入力されるたびにレンダリングが実行されてしまう点です。
あえてレンダリングを起こさせるケースもあるのですが、同じ画面で「レンダリングされると困る」ようなコードがあったときにハマります。
とはいえ回避策はあります。
レンダリングされると困るコードは「useEffect」で事前に制御しておけば良いからです。
APIの呼び出しが代表例です。
本記事はあくまで自分と同じような初心者向けに解説していますので、玄人の方のようにバグの回避策の引き出しは多く持っていません。
②そもそも入力内容を新しく状態管理するべきか?
自分も何となくコピペでやっていたので気付くのに時間がかかりました。
フォームの入力内容をわざわざ状態管理しておく必要がないケースは意外と多いです。
認証機能はバックエンドで処理することが多いからです。
Reactのようにフロントエンドでメールアドレス、パスワードを保持し続ける必要があるかは今一度よく考える必要があります。
PHP、Rubyのようなバックエンド又はFirebaseのようなSaasツールがある以上、メールアドレスのような認証情報はデータベースから取得できます。
とはいえブラウザのLocalStorageを使ったログイン情報の保持をするケースもあるので全てではないです。
※ちなみにLocalStorageを使ったログイン情報の保持も近年ではセキュリティ的に問題視されています。
Reactでフォームを作るときはuseStateではなくuseRefを使う
「じゃあどうすれば良いんだ?」という話になるのですが、現時点の自分のナレッジとしては「useRef」を使っておけば良いと考えます。
先ほどのコードを以下のようにできます。
import React, { useRef, useState } from "react";
const Form = () => {
const emailRef = useRef();
const passwordRef = useRef();
const handleSubmit = (e) => {
e.preventDefault();
console.log({
email: emailRef.current.value,
password: passwordRef.current.value,
});
};
return (
<div>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="email">メールアドレス</label>
</div>
<div>
<label htmlFor="password">パスワード</label>
<input ref={passwordRef} id="password" type="password" />
</div>
<div>
<button type="submit">送信</button>
</div>
</form>
</div>
);
};
export default Form;
React初心者の方には「useRef」は馴染みがないかもしれませんが、簡単に言うと「値を取る」ことを実現してくれます。
文法が少しだけ違うのですが、上記でも同じようにメールアドレスとパスワードを取得することができます。
また本件に限った話で言うと、onChange関数が不要になるのも良い点です。
「useState」「useRef」はとても似ているフックですが、大きな違いが「レンダリングするか」です。
「useRef」は値を参照しますが、1文字入力するたびにレンダリングは起こしません。
そのため別のコードで「あえてレンダリングさせたい」というものがある時には、逆に「useState」にした方が都合が良いです。
つまりメリット、デメリットを考えて適材適所で使っていくのが大事ってことですね。
Reactで作成するフォームに必要なローディング機能を持ったボタンの作り方
ボタンについてもポイントがあり、送信にタイムラグがある時のローディング表示です。
まず何も考慮せずに作成すると以下のような形になります。
APIやDBにデータを送信するときにタイムラグがあることを今回はsetTimeoutを使った時間差プログラムで表現しました。
import React, { useRef, useState } from 'react';
const App = () => {
const [todos, setTodos] = useState([]);
const inputRef = useRef(null);
const onSubmit = (e) => {
e.preventDefault();
if (inputRef.current == null) return;
// 送信時にタイムラグがある想定
setTimeout(() => {
setTodos((prev) => [...prev, inputRef.current.value]);
}, 1000);
};
return (
<div>
<form onSubmit={onSubmit}>
<label>タスク</label>
<input type="text" ref={inputRef} />
<button>作成</button>
<ul>
{todos.map((todo, i) => (
<li key={i}>{todo}</li>
))}
</ul>
</form>
</div>
);
};
export default App;
上記のコードでも大きな問題はありませんが、送信中のタイムラグ時にも「作成」ボタンをクリックできてしまいます。
そのため不必要なAPIコールが起こってしまうわけです。
もともとbuttonタグにはdisabledという属性があり、1回の送信が完了するまでボタンをクリックさせないようにすることが可能です。
この特定を利用してローディングを持たせたボタンを簡単に作成することができます。
import React, { useRef, useState } from 'react';
const App = () => {
const [todos, setTodos] = useState([]);
const inputRef = useRef(null);
// ここを追加
const [isLoading, setIsLoading] = useState(false);
const onSubmit = (e) => {
e.preventDefault();
if (inputRef.current == null) return;
// ここを追加
setIsLoading(true);
// 送信時にタイムラグがある想定
setTimeout(() => {
setTodos((prev) => [...prev, inputRef.current.value]);
// ここを追加
setIsLoading(false);
}, 1000);
};
return (
<div>
<form onSubmit={onSubmit}>
<label>タスク</label>
<input type="text" ref={inputRef} />
{/* ここを変更 */}
<button disabled={isLoading}>{isLoading ? '作成中' : '作成'}</button>
<ul>
{todos.map((todo, i) => (
<li key={i}>{todo}</li>
))}
</ul>
</form>
</div>
);
};
export default App;
1回の送信が完了するまではボタンが「ローディング」表示になり、disabledに渡しているisLoadingがfalseに変わるまでクリックができない状態にすることができました。
このようなフォームならではの細かい実装が実務では求められますので初心者の方も練習しておきましょう。