DEV Community

Cover image for React 19, handling forms using useOptimistic and useFormStatus along with React Hook Form and Zod … practical example ⚛️
Alaa Mohammad
Alaa Mohammad

Posted on • Updated on

React 19, handling forms using useOptimistic and useFormStatus along with React Hook Form and Zod … practical example ⚛️

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(),
});

Enter fullscreen mode Exit fullscreen mode
const {
    register,
    control,
    trigger,
    reset,
    formState: { errors },
  } = useForm<TaskSchemaType>({
    resolver: zodResolver(TaskSchema),
    defaultValues: { completed: false, details: "", editMode: false },
});
Enter fullscreen mode Exit fullscreen mode

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:&nbsp;</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>
Enter fullscreen mode Exit fullscreen mode

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 });
  };
Enter fullscreen mode Exit fullscreen mode

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;
    }
  );
Enter fullscreen mode Exit fullscreen mode

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))
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

🎉🎉[Project on GitHub]: React 19 Features (https://github.com/alaa-m1/react19-features)

Top comments (0)