New React Updates: Using useOptimistic Hook in a NextJs app
Tags: webdev, news, nextjs
Published: 2024-01-31 10:11:34

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ma5dcdcgg9opk6f3c3hj.png)

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](https://nextjs.org/docs/app/api-reference/functions/server-actions).

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.

```jsx
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>
  );
}
```

### API Call for the Job Profile Application

For this project, we will use mock API data obtained from [SandAPI.](https://sandapi.com/) 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/76hjoorkhgm9fr02oe48.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o4oabuv25u0wex8pxw84.png)

### 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.

```typescript
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();
```

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

```jsx
<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>;
```

The result should be this in the user interface.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nj6zdu8rew0vrfcbwc1k.png)

### 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.

```typescript
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>
  );
}
```

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

![no useoptimistic](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/teh38zghqc5ggrwoovh4.gif)

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 the`useOptimistic` 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, the`page.tsx` file should reflect the following structure.

```typescript
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>
  );
}
```

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.

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

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.

```typescript
"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>
  );
}
```

### 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.

```javascript
"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>
  );
}
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rd24b6714ioxw4f9drpt.gif)


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.