先週のブログReact19のActions, useActionState Hookを使ってみたの続きです。
useActionState Hookはサーバー側の関数(Server Action)実行だけでなく、クライアントのみのフォーム(<form>
)処理にも使えます。
Reactでのフォーム処理には従来は、
- Controlled Component(Input) : 入力中の値等をStateで管理する
- Uncontrolled Component(Input) : 入力中の値等はブラウザーに任せ、
useRef
を使ってその値を取得する
の2つの方法がありましたが、useActionState
も新たなフォーム処理の方法だと思われるので同じアプリを作って比較してみる事にしました(React Hook Form
等の便利ライブラリーを使う方法もありますが、今回はReactの標準機能のみ検討します)。
Bing Image Creatorが生成した画像を使っています
今回のアプリ
いつものジャンケンは<form>
がないので、ToDoアプリで比較してみる事にします。またReactの環境は前々回のブログ同様にVite + React 19 Betaです。
仕様
- New task: にタスクを書きUpdateボタンを押すとタスクが追加されます
- タスクの左のチェックボックスをチェックしUpdateボタンを押すとタスクが完了になります
- 完了したタスクはチェックボックスは無くなり、取り消し線が付きます
- Upadte前ならチェックボックスはチェック・アンチェックできる
- 複数のタスクをチェックでき、New taskも同時に追加できる
Updateボタンを押すことでToDoリストが変わる<form>
向きな仕様になっています。😃
コード
Uncontrolled
まずは、Uncontrolled Component(Input)のコードです。useRef
で複数のチェックボックスに対応するための少し苦労しました。
- ① ToDoリスト管理用State
- task: タスク
- done: タスク完了状態
- ② 新規タスク入力用inputのref
- ③ タスク毎のチェックボックス用のref
- ToDoリストの長さ分のref配列を準備しています
- ④ Updateボタンの処理
- ⑤ チェックボックスがチェックされていればタスク完了を設定
- ⑥ 新規タスク入力があれば、ToDoの追加
- ⑦ ToDoリスト管理用Stateの更新
- ⑧ Submit(Updateボタン)の処理ハンドラーの設定
- ⑨ タスク完了状態により、取り消し線が付きタスク表示、またはチェックボックス付きで表示
import { RefObject, createRef, useRef } from "react";
type TodoType = {
task: string,
done: boolean
}
function AppUnConrolled() {
const [todos, setTodos] = useState<TodoType[]>([]); // ← ①
const newTask = useRef<HTMLInputElement>(null); // ← ②
const checks = useRef<RefObject<HTMLInputElement>[]>([]); // ←↓ ③
todos.forEach((_, ix) => checks.current[ix] = createRef<HTMLInputElement>());
const updateTodo = (e: React.FormEvent) => { // ← ④
e.preventDefault();
const newTodos = [...todos];
todos.forEach((_, ix) => {
if (checks.current[ix].current?.checked) { // ← ⑤
newTodos[ix].done = true;
}
});
if (newTask.current?.value) {
newTodos.push({task: newTask.current.value, done: false}); // ← ⑥
newTask.current.value = "";
}
setTodos(newTodos); // ← ⑦
}
return (
<>
<h2>ToDo</h2>
<form onSubmit={updateTodo}> {/* ← ⑧ */}
<ul>
{todos.map((todo, ix) =>
<li key={ix}>
{todo.done ? {/* ← ⑨ */}
<s>{todo.task}</s> :
<label>
<input type="checkbox" ref={checks.current[ix]} />
{todo.task}
</label>}
</li>
)}
</ul>
<label>New task: <input type="text" ref={newTask} /></label>
<div><input type="submit" value="Update"/></div>
</form>
</>
);
}
useActionState
今回のメイン、useActionState Hookを使ったコードです。
- ① Updateボタンの処理(アクション)関数
- 引数は現在のStateと
<form>
からのデータ - ここでは非同期
async
関数にしていますが、同期関数でも良いようです
- 引数は現在のStateと
- ②
newTask
パラメーターがあれば、新規ToDoの追加 - ③ チェックされたチェックボックスがあればタスク完了を設定
- チェックボックスのname属性はToDo配列のインデックスを文字列にしたものです
- ④ この関数は新規Stateを戻します
- ⑤
useActionState
の呼出し- 戻り値はStateとフォーム・アクション
- ⑥ フォーム・アクションの設定
- ⑦ チェックボックスにはname属性を指定します、値はToDo配列のインデックスを文字列にしたものです
- ⑧ 新規タスク入力、ここにもname属性(
name="newTask"
)を指定しています
import { useActionState } from "react";
type TodoType = {
task: string,
done: boolean
}
function AppUseActionState() {
const updateTodo = async (prevState: TodoType[], formData: FormData): // ← ①
Promise<TodoType[]> => {
const newTodos = [...prevState];
const newTask = formData.get("newTask"); // ← ②
if (newTask) {
newTodos.push({task: newTask as string, done: false});
}
prevState.forEach((_, ix) => {
const checked = formData.get(String(ix)); // ← ③
if (checked) {
newTodos[ix].done = true;
}
});
return newTodos; // ← ④
}
const [todos, formAction] = useActionState<TodoType[], FormData>(
updateTodo, []); // ← ⑤
return (
<>
<h2>ToDo</h2>
<form action={formAction}> {/* ← ⑥ */}
<ul>
{todos.map((todo, ix) =>
<li key={ix}>
{todo.done ?
<s>{todo.task}</s> :
<label> {/* ↓ ⑦ */}
<input type="checkbox" name={String(ix)} value="checked"/>
{todo.task}
</label>}
</li>
)}
</ul> {/* ↓ ⑧ */}
<label>New task: <input type="text" name="newTask"/></label>
<div><input type="submit" value="Update"/></div>
</form>
</>
)
}
Controlled Component(Input)
もう説明は要らないかと思いますが、
- ① 新規タスク入力用State
- ② チェックボックスのチェック状態用State(配列)
- ③ タスクが追加されたら、チェック状態用Stateにもfalseを追加
- ④ チェックボックスのonChangeでは配列のwithメソッドを使っています
- ES2023で追加された、非破壊的な配列要素の変更メソッドは便利ですね!
import { useState } from "react";
type TodoType = {
task: string,
done: boolean
}
function AppConrolled() {
const [todos, setTodos] = useState<TodoType[]>([]);
const [newTask, setNewTask] = useState(""); // ← ①
const [checked, setChecked] = useState<boolean[]>([]); // ← ②
const updateTodo = (e: React.FormEvent) => {
e.preventDefault();
const newTodos = [...todos];
checked.forEach((_, ix) => {
if (checked[ix]) {
newTodos[ix].done = true;
}
});
if (newTask) {
newTodos.push({task: newTask, done: false});
setNewTask("");
setChecked([...checked, false]); // ← ③
}
setTodos(newTodos);
}
return (
<>
<h2>ToDo</h2>
<form onSubmit={updateTodo}>
<ul>
{todos.map((todo, ix) =>
<li key={ix}>
{todo.done ?
<s>{todo.task}</s> :
<label>
<input type="checkbox" checked={checked[ix]} {/* ↓ ④ */}
onChange={() => setChecked(checked.with(ix, !checked[ix]))}/>
{todo.task}
</label>}
</li>
)}
</ul>
<label>New task:
<input type="text" value={newTask}
onChange={e => setNewTask(e.target.value)}/>
</label>
<div><input type="submit" value="Update"/></div>
</form>
</>
)
}
まとめ
useActionStateを使ったコードはとてもシンプルだと思います、Controlled/Uncontrolledは入力値の取得や管理が必要ですが、それらが要らないのが素晴らしいですね。
ということで、submitボタンで更新するフォームならuseActionStateを積極的に使ってもよいかなと思います。
ただし、入力タグと処理(アクション)の対応はname属性になり、綴りが間違っていてもTypeScript等でチェックされないので注意が必要ですね。