React 19, handling forms using useOptimistic and useFormStatus along with React Hook Form and Zod … practical example ⚛️
Alaa Mohammad
Posted on April 7, 2024
The challenge here to use useOptimistic to get more responsive UI with form actions along with React Hook Form and Zod schema (to apply client side validation) and at the same time to prevent clicking on the submit button multiple time until the current action is finished which we will handle using useFormStatus hook.
First of all, I will give a brief explanation about the used hooks.
useOptimistic: allows to optimistically update the UI during async actions (like a network request) by showing different state.
It accepts the current state which will be returned initially and whenever no action is pending and an update function that takes the current state and the input to the action.
It will returning the optimistic state to be used while waiting for the action and a dispatching function “addOptimistic” to call when we have an optimistic update.
So, when a user submits a form, instead of waiting for the server’s response to display the new changes, the interface is immediately updated with the expected outcome with a label to indicate that the action in pending “loading, adding …”and when that action in completed then this label will be removed.
useFormStatus: gives status information of the last form submission.
This Hook returns information like the pending property to tell if the form is actively submitting, which we will use for example to disable buttons while the form is submitting.
Practical example 💻
1.
Both useOptimistic and useFormStatus are useful hooks in the upcoming React 19, for now these hooks are only available in React’s Canary and experimental channels.
To review React 19 features, we will need to install the Canary version of React and react-dom by using the following:
npm i react@18.3.0-canary-6db7f4209-20231021 react-dom@18.3.0-canary-6db7f4209-20231021
2.
Add Zod schema and zodResolver to integrate react-hook-form with Zod schema validation library:
export const TaskSchema = z.object({
details: z
.string()
.min(5, "Task details must be at least 5 characters")
.max(100, "Task details must be less than 100 characters"),
completed: z.boolean(),
editMode: z.boolean(),
});
const {
register,
control,
trigger,
reset,
formState: { errors },
} = useForm<TaskSchemaType>({
resolver: zodResolver(TaskSchema),
defaultValues: { completed: false, details: "", editMode: false },
});
3.
For forms we will use action to benefit from useOptimistic hook and from server actions React 19 feature:
<form
action={handleFormAction}
style={{ margin: "10px 5px 20px" }}
>
<div
style={{
display: "flex",
justifyContent: "space-around",
gap: "1",
padding: "5px",
}}
>
<div>
<label htmlFor={`task-details-${id}`}>Task details: </label>
<input
id={`task-details-${id}`}
type="text"
autoComplete="off"
{...register("details")}
/>
{errors.details && (
<div style={{ color: "#DC3545" }}>{errors.details.message}</div>
)}
</div>
<div>
<label htmlFor={`task-status-${id}`} className="prevent-select">
Is completed:
</label>
<input
id={`task-status-${id}`}
type="checkbox"
{...register("completed")}
/>
</div>
</div>
<SubmitBtn label="Save task details" />
</form>
4.
In formAction we will add an optimistic update after using "trigger" method from React Hook Form to trigger client side validation:
const handleFormAction = async (data: any) => {
const res = await trigger(["details"]);
if (!res) return;
const updatedTask: Task = {
id: task.id,
details: data.get("details"),
completed: data.get("completed"),
editMode: false,
};
addOptimisticTask({ ...updatedTask, process: "edit", editMode: true });
await editTask(updatedTask);
reset({ details: "", completed: false });
};
5.
We will use useOptimistic to handle different optimistic actions:
const [optimisticTasks, addOptimisticTask] =
useOptimistic<Array<Task>>(
tasks,
(state: Array<Task>, updatedTask: Task) => {
if (updatedTask.process === "add")
return [...state, { ...updatedTask, loading: true }];
else if (updatedTask.process === "delete") {
return state.map((task) =>
task.id === updatedTask.id
? { ...task, loading: true, process: updatedTask.process }
: task
);
} else if (updatedTask.process === "setForEdit") {
return state;
} else if (updatedTask.process === "edit") {
return state.map((task) =>
task.id === updatedTask.id
? { ...updatedTask,loading: true }
: task
);
} else return state;
}
);
6.
To mimic slow fetching we will use the following async method:
export const sleep=async(delayInMS: number):Promise<any>=>{
return new Promise((resolve)=> setTimeout(resolve, delayInMS))
}
7.
Add SubmitBtm which will be rendered within form tag to get pending status by using useFormStatus hook:
import React from "react";
import { useFormStatus } from "react-dom";
const SubmitBtn = ({ label }: { label: string }) => {
const status = useFormStatus();
return (
<button
type="submit"
disabled={status.pending}
style={{ minWidth: "100px" }}
>
{label}
</button>
);
};
export default SubmitBtn;
🎉🎉[Project on GitHub]: React 19 Features (https://github.com/alaa-m1/react19-features)
Posted on April 7, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.