DEV Community

Cover image for New React Updates: Using useOptimistic Hook in a NextJs app
Favour Onyeke
Favour Onyeke

Posted on

New React Updates: Using useOptimistic Hook in a NextJs app

In the dynamic landscape of web development, React has constantly evolved, ushering in innovative solutions to enhance user experience. The latest stride in this journey comes in the form of a groundbreaking new hook, useOptimistic.

Discover how this innovative hook simplifies the implementation of optimistic updates, making your React/Nextjs applications not only faster but also more user-friendly. We’ll explore how to implement optimistic updates using React.js’s new useOptimistic hook in combination with Next.js 14 server action.

Prerequisites

  • Knowledge of Reactjs and Next.js.
  • Node.js installed.
  • Typescript and Tailwind.

One of the benefits of useOptimistic is its ability to display the result of a change on the user interface before the changes are effected on the server. This creates a more optimized, responsive application.

A typical web application sends user-submitted information to the server to update the database before displaying the result. This process may result in a delay before the data is shown, leading to a poor user experience. useOptimistic makes the required changes or updates in the user interface for the user to see before the data is confirmed. In the event of any issue with the data submission to the server, it will revert the changes.

Creating a Job Profile

To demonstrate the functionality of useOptimistic, I developed a basic web app named Job Profile. This app retrieves and presents essential information about people's job profiles, specifically their names and job titles. Next.js was utilized for client-side rendering and a backend configuration was implemented to communicate with an external API for fetching and sending relevant data.

Using this setup, we can compare the outcomes when useOptimistic is enabled.

job-profile

To send data to the backend in a simple form, Server actions will be used. More details about server action can be found on Nextjs.

Note: This project relies on TypeScript and Tailwind CSS. When installing Next.js, be sure to include TypeScript and Tailwind CSS.

Since this is a simple project, we will create the initial form (without the useOptimistic hook) on the page.tsx file.

To render the form, here's the JSX code in the return section of the Home component, after removing unnecessary starter codes and styles.

export default async function Home() {
  return (
    <main>
      <h1 className="text-3xl font-bold text-center">Job profile</h1>
      <form className="flex flex-col gap-5 max-w-xl mx-auto p-5">
        <input
          name="name"
          type="text"
          className="border border-gray-300 p-2 rounded-md"
          placeholder="Enter your name..."
        />
        <input
          name="job"
          type="text"
          className="border border-gray-300 p-2 rounded-md"
          placeholder="Your job"
        />
        <button className="border bg-green-500 text-white p-2 rounded-md">
          Add Job
        </button>
      </form>
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

API Call for the Job Profile Application

For this project, we will use mock API data obtained from SandAPI. SandAPI assists in managing API endpoints, which is crucial for our application, and in generating dummy data so that we do not need to be concerned about the database of our application.

After creating an account on SandAPI, click on the "Create a New API" button at the top right and fill in the information as shown in the image.

api Endpoint

Click on the "Auto-generate" button below to produce fabricated data. This will prompt fields to appear, enabling the user to specify the type of data to be generated. Once the value displayed in the image below has been entered into the designated field, select "Create Endpoint".

Api-endpoint

Getting data from the API endpoint

Within the Page.tsx file, an API request must be made to display the dummy data that has been created. As TypeScript is being utilized, it is necessary to specify the type of data being requested, which has been designated as Job. The data can be retrieved by using the API endpoint from SandAPI.

export interface Job {
  sand_id?: number;
  name: string;
  job: string;
}
//in the Home component, insert this code before the return function in the Home component
("use server");
const res = await fetch("https://api.sandapi.com/user_wFnZRo/jobs", {
  cache: "no-cache",
  next: {
    tags: ["job"],
  },
});
const data: Job[] = await res.json();
Enter fullscreen mode Exit fullscreen mode

To display the data, we iterate through each object in the array and present it. We insert this code below the form.

<main>
  <h2 className="font-bold p-5">List of Jobs</h2>
  <div className="grid gap-x-8 gap-y-4 grid-cols-3">
    {data.map((jobs) => {
      const { sand_id, name, job } = jobs;
      return (
        <div key={sand_id} className="p-6 shadow">
          <p className="mt-6 text-2xl font-bold text-gray-900 sm:mt-10">
            {name}
          </p>
          <p className="mt-6 text-base font-bold text-gray-600">{job}</p>
        </div>
      );
    })}
  </div>
</main>;
Enter fullscreen mode Exit fullscreen mode

The result should be this in the user interface.

Image description

Adding New Jobs

The values entered in the form field need to be updated in our list by sending a POST request. To do this, an action attribute must be added to the form tag and a function called addJob needs to be passed.

Create a new function called addJob that will allow us to add more items to our dummy data by using the information submitted on the form. The initial step is to add the "use server" since this information is retrieved from the server side of our application.

To add the data from the form, we'll use the get() callback to retrieve the value of the input element with the name attribute name or job.

Next, we create an object named newJob using the values obtained from the form. This object will be passed as a POST request to the API endpoint. This will enable the addition of new data to our dummy data.

The page.tsx file should reflect the following structure.

import { revalidateTag } from "next/cache";

export interface Job {
  sand_id?: number;
  name: string;
  job: string;
}

export default async function Home() {
  ("use server");
  const res = await fetch("https://api.sandapi.com/user_wFnZRo/jobs", {
    cache: "no-cache",
    next: {
      tags: ["job"],
    },
  });
  const data: Job[] = await res.json();
  //addJob responsible for adding a new Job profile
  const addJob = async (e: FormData) => {
    "use server";
    const name = e.get("name")?.toString();
    const job = e.get("job")?.toString();
    if (!name || !job) return;
    const newJob: Job = {
      name: name,
      job,
    };
    await fetch("https://api.sandapi.com/user_wFnZRo/jobs", {
      method: "POST",
      body: JSON.stringify(newJob),
      headers: {
        "Content-Type": "application/json",
      },
    });
    revalidateTag("job");
  };
  return (
    <main>
      <h1 className="text-3xl font-bold text-center">Job profile</h1>
      <form
        action={addJob}
        className="flex flex-col gap-5 max-w-xl mx-auto p-5"
      >
        <input
          name="name"
          type="text"
          className="border border-gray-300 p-2 rounded-md"
          placeholder="Enter your name..."
        />
        <input
          name="job"
          type="text"
          className="border border-gray-300 p-2 rounded-md"
          placeholder="Your job"
        />
        <button className="border bg-green-500 text-white p-2 rounded-md">
          Add Job
        </button>
      </form>
      <main>
        <h2 className="font-bold p-5">List of Jobs</h2>
        <div className="grid gap-x-8 gap-y-4 grid-cols-3">
          {data.map((jobs) => {
            const { sand_id, name, job } = jobs;
            return (
              <div key={sand_id} className="p-6 shadow">
                <p className="mt-6 text-2xl font-bold text-gray-900 sm:mt-10">
                  {name}
                </p>
                <p className="mt-6 text-base font-bold text-gray-600">{job}</p>
              </div>
            );
          })}
        </div>
      </main>
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

The inserted values in the form will be displayed in the rendering section.

no useoptimistic

Without the useOptimistic hook integrated into the form, the Job Profile app would rely on the traditional method of updating the user interface only after the server has confirmed the changes. When a user creates a new job profile, the app must wait for the server to process the update and confirm it before displaying the revised information. As a result, there is a slight delay in adding form input to the list.

Optimizing the display using useOptimistic

To improve the user experience of the app, we can use theuseOptimistic hook. To implement this, it is necessary to create a file named form.tsx.

All the JSX elements from page.tsx can be transferred to form.tsx, and the essential data gotten from the API can be passed as props, denoted as job. Furthermore, the addJob function should be removed as it will be configured in the form.tsx file.

To cache the data transmitted to the server, a function named validateData needs to be created. This function will invoke the server action named revalidatePath, and we can pass this validateData as props in the form.tsx file.

Upon implementing these adjustments, thepage.tsx file should reflect the following structure.

import { revalidatePath } from "next/cache";
import Form from "./components/form";
import React from "react";

export interface Job {
  sand_id?: number;
  name: string;
  job: string;
}
//the addJob is moved to form.tsx
export default async function Home() {
  ("use server");
  const res = await fetch("https://api.sandapi.com/user_wFnZRo/jobs", {
    cache: "no-cache",
    next: {
      tags: ["job"],
    },
  });
  const data: Job[] = await res.json();

  //The validateData function calls revalidatePath to purge cached data for a specific path.
  const validateData = async () => {
    "use server";
    revalidatePath("/jobs");
  };
  //We add the Form component that houses the form section of the app in Form.tsx.
  // Next, we include the two props, "jobs" and validateData.
  return (
    <main>
      <Form jobs={data} validateData={validateData} />
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

Both the jobs props and validateData functions will be destructured in the form.tsx file.

Importing useOptmistic to the app

In the form.tsx, we first destructure the useOptimistic hook from React.

"use client";
import { useOptimistic } from "react";
Enter fullscreen mode Exit fullscreen mode

The useOptimistic hook has two values - the optimistic state (called optimisticJob) and a function (called addOptimisticJob). The jobs props from page.tsx are passed to the useOptimistic hook to track any changes in the server's data and update it accordingly. By passing the props to useOptimistic, we can now replace the old data with the new optimisticJob variable.

"use client";
import { useOptimistic } from "react";

type JobType = {
  sand_id?: number;
  name: string;
  job: string;
};
type JobProps = {
  jobs: JobType[];
  validateData: any;
};

export default function Form({ jobs, validateData }: JobProps) {
  const [optimisticJob, addOptimisticJob] = useOptimistic(
    jobs,
    (state, newJob: JobType) => {
      return [...state, newJob];
    },
  );

  return (
    <main>
      <h1 className="text-3xl font-bold text-center">Job profile</h1>
      <form className="flex flex-col gap-5 max-w-xl mx-auto p-5">
        <input
          name="name"
          type="text"
          className="border border-gray-300 p-2 rounded-md"
          placeholder="Enter your name..."
        />
        <input
          name="job"
          type="text"
          className="border border-gray-300 p-2 rounded-md"
          placeholder="Your job"
        />
        <button className="border bg-green-500 text-white p-2 rounded-md">
          Add Job
        </button>
      </form>

      <h2 className="font-bold p-5">List of Jobs</h2>
      <div className="grid gap-x-8 gap-y-4 grid-cols-3">
        {/* We then change data to optimisticJob */}
        {optimisticJob.map((singleJob) => {
          const { sand_id, name, job } = singleJob;
          return (
            <div key={sand_id} className="p-6 shadow">
              <p className="mt-6 text-2xl font-bold text-gray-900 sm:mt-10">
                {name}
              </p>
              <p className="mt-6 text-base font-bold text-gray-600">{job}</p>
            </div>
          );
        })}
      </div>
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

Updating the User Interface

For updating values, we create the addJob function. This is responsible for submitting our data to the database.

It's essential to call the validateData function inside the addJob function to clear cached data for a particular path. The form.tsx file should have this code structure to ensure everything works as expected.

"use client";
import { useOptimistic } from "react";

type JobType = {
  sand_id?: number;
  name: string;
  job: string;
};
type JobProps = {
  jobs: JobType[];
  validateData: any;
};

export default function Form({ jobs, validateData }: JobProps) {
  const [optimisticJob, addOptimisticJob] = useOptimistic(
    jobs,
    (state, newJob: JobType) => {
      return [...state, newJob];
    },
  );
  //the addJob function
  const addJob = async (e: FormData) => {
    const name = e.get("name")?.toString();
    const job = e.get("job")?.toString();
    if (!name || !job) return;

    const formInput = {
      name,
      job,
    };

    addOptimisticJob(formInput);

    await fetch("https://api.sandapi.com/user_wFnZRo/jobs", {
      method: "POST",
      body: JSON.stringify(formInput),
      headers: {
        "Content-Type": "application/json",
      },
    });
    await validateData();
  };

  return (
    <main>
      <h1 className="text-3xl font-bold text-center">Job profile</h1>
      <form
        action={addJob}
        className="flex flex-col gap-5 max-w-xl mx-auto p-5"
      >
        <input
          name="name"
          type="text"
          className="border border-gray-300 p-2 rounded-md"
          placeholder="Enter your name..."
        />
        <input
          name="job"
          type="text"
          className="border border-gray-300 p-2 rounded-md"
          placeholder="Your job"
        />
        <button className="border bg-green-500 text-white p-2 rounded-md">
          Add Job
        </button>
      </form>

      <h2 className="font-bold p-5">List of Jobs</h2>
      <div className="grid gap-x-8 gap-y-4 grid-cols-3">
        {/* We then change data to optimisticJob */}
        {optimisticJob.map((singleJob) => {
          const { sand_id, name, job } = singleJob;
          return (
            <div key={sand_id} className="p-6 shadow">
              <p className="mt-6 text-2xl font-bold text-gray-900 sm:mt-10">
                {name}
              </p>
              <p className="mt-6 text-base font-bold text-gray-600">{job}</p>
            </div>
          );
        })}
      </div>
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

By examining the code, it becomes evident that we invoked the addOptimisticJob function on the data received from the form before dispatching it to the server. This approach initially updates the value in the UI component and then sends it to the backend for further processing. In the event of a failed update, the item gets removed from the UI.

The result should be this:

useOptmise job

As a result of this process, the data is displayed to the user immediately before being saved to the database, as depicted in the image above.

Conclusion

By demonstrating the functionality of useOptimistic in the Job Profile app, it becomes clear that this hook significantly enhances the user experience by eliminating delays and creating a more fluid and responsive interface for users to interact with.

In the event of any issues with the data submission to the server, useOptimistic also ensures that the changes made on the user interface are reverted, maintaining data integrity and providing a safety net for potential errors.

Top comments (0)