はじめに
こんにちは、株式会社コズムでフロントエンドエンジニアをやっている山田です。
普段の業務ではNext.jsをゴリゴリ使っていますが、Reactにどうやらメジャーアップデートが到来するようなので、本リリース前にその新機能に触れてみることにしました。
新機能
React19で追加される機能はフォーム関連の機能が中心となっています。また、Sever ComponentsやServer Action機能が追加されたことは印象的です。今回は追加された機能群の中でも、”useOptimistic” hookを使って簡単な画面を実装してみようと思います。
useOptimisticとは
楽観的なUIの更新を行う新たなhookらしいです。つまり、非同期的な処理を行う際に、予測される処理後の結果をUIとして「楽観的に」表示しておいて、実際の処理結果が帰ってきた際に改めてその値に置き換える、という実装を簡潔に行うことのできるhookです。それでは実装を見てみましょう。
まず、formの状態を扱うstateを用意します。ここではApp配下にcommentというstateを宣言します。そしてコメントの送信とstateの変更を行うsendComment関数を宣言します。
export default function App() {
const [comment, setComment] = useState({
text: "Enter a comment",
sending: false,
})
async function sendComment(formData) {
const sentComment = await deliverComment(formData.get("comment"))
setComment({ text: sentComment, sending: false })
}
return <CommentForm comments={comment} sendComment={sendComment} />
}
次に、CommentFormコンポーネントを見てみましょう。このコンポーネントにおいて、新しいhookであるuseOptimisticを使っていきます。これは引数に扱いたいstateとその楽観的な更新関数をとります。
function CommentForm({ comments, sendComment }) {
const formRef = useRef()
async function formAction(formData) {
addOptimisticComment(formData.get("comment"))
formRef.current.reset()
await sendComment(formData)
}
const [optimisticComment, addOptimisticComment] = useOptimistic(
comments,
(state, newComment) => {
return {
text: newComment,
sending: true,
}
}
)
return (
<>
<div>
<p>{optimisticComment.text}</p>
{!!optimisticComment.sending && <small> (Sending...)</small>}
</div>
<form action={formAction} ref={formRef}>
<input type="text" name="comment" placeholder="your comment" />
<button type="submit">Send</button>
</form>
</>
)
}
続けて、formAction関数を宣言します。この関数は、addOptimisticComment関数に引数としてフォームに入力された文字列を渡しています。つまり、フォームに入力された値を、送信処理に先んじて暫定的にstateに書き換えを行なっていると言えます。実際のコメント内容を記録したstateの書き換えは、その後のsendComment関数内で行われています。
ここでUIをみてみると、div要素内にoptimisticCommentの内容を表示しています。そして、addOptimisticComment関数で返される楽観的なcommentのstateは、sendingプロパティにtrueの値を持つので、これを参照してUIに”sending…”と表示しています。送信処理が完了し、実際のcommentのstateが書き換わると、useOptimisticはこのstateを参照するのでsetComment関数によりsendingプロパティはfalsyな値となり、”sending…”のメッセージは表示されなくなります。
ソースコードの全容
App.js
import { useOptimistic, useState, useRef } from "react"
import { deliverComment } from "./actions.js"
function CommentForm({ comments, sendComment }) {
const formRef = useRef()
async function formAction(formData) {
addOptimisticComment(formData.get("comment"))
formRef.current.reset()
await sendComment(formData)
}
const [optimisticComment, addOptimisticComment] = useOptimistic(
comments,
(state, newComment) => {
return {
text: newComment,
sending: true,
}
}
)
return (
<>
<div>
<p>{optimisticComment.text}</p>
{!!optimisticComment.sending && <small> (Sending...)</small>}
</div>
<form action={formAction} ref={formRef}>
<input type="text" name="comment" placeholder="your comment" />
<button type="submit">Send</button>
</form>
</>
)
}
export default function App() {
const [comment, setComment] = useState({
text: "Enter a comment",
sending: false,
})
async function sendComment(formData) {
const sentComment = await deliverComment(formData.get("comment"))
setComment({ text: sentComment, sending: false })
}
return <CommentForm comments={comment} sendComment={sendComment} />
}
actions.js
export async function deliverComment(message) {
await new Promise((res) => setTimeout(res, 1000))
return message
}
終わりに
useOptimisticは、フォームの状態管理をする上で、複数のstateを使って複雑に管理する必要がなく、ソースコードを簡略化できる可能性があると感じました。今回は簡単な実装を行なったのみなので、本リリース後に、プロダクトへ組み込んだりと、実践的な使い方をしてみたいと思います。