In today’s blog, we will examine an experimental hook that helps us display optimized results when we display data we fetch from the server. What can we do to optimize such a use case? Is there a way to show updated data as it is being fetched from the server? We will explore precisely that with the new useOptimistic hook.
This hook from React gives us a copy of our data, which we pass to it, and a function like how useState works. This function is then utilized to manipulate the copied data we show in our application until the new data is fetched from the server or API call.
Implementing the “useOptimistic” Hook in Your Project
Like useFormStatus and useFormState in earlier blogs, this is an experimental hook by react-dom; therefore, it will not be available with the usual npm install package command. To accomplish this, run the following command in your terminal to install the experimental version of react and react-dom:
npm install react@experimental react-dom@experimental
After installing the experimental packages, your package.json should contain the dependencies listed below:
After doing this, if you want to use typescript in your project, you should additionally create a file informing your project that you will be utilizing the experimental utilities:
Following that, you should be able to import useFormState into your files and use it appropriately. If you use TypeScript, you may need to add “//@ts-ignore” above the import. TypeScript will not recognize this experimental import but will continue functioning as intended.
//@ts-ignore
import { useFormState } from "react-dom";
Developing a Simple User Interface
Let’s now create a simple react component showing an input and a button. You can use a form here as well; I have simply used the onClick property of the button to recreate the API service call scenario.
return ( <div className="bg-dark text-white py-5"> <div className="row mx-0"> <h2 className="text-center">useOptimistic</h2> <div className="col-md-6 p-5"> <input ref={inputRef} type="text" className="form-control my-3" placeholder="Enter item name" /> <button disabled={isLoading} onClick={() => startTransition(() => onClick())} className="btn btn-warning form-control" > Add Item </button> </div> </div> </div> );
We now have a simple input and button component. The input component is passed with an inputRef created by using the useRef hook of react, and the button’s onClick function is wrapped in a startTransition function to simulate loading.
The Utilities Listed in Form
Next, we look at the useful hooks we need to import into our app and some other useful utilities we will use to demonstrate our new hook.
import { useState, useOptimistic, useTransition, useRef } from "react"; import "bootstrap/dist/css/bootstrap.css"; function App() { const createItem = (item) => { return new Promise((resolve) => setTimeout(() => resolve(item), 2000)); }; const inputRef = useRef(); const [isLoading, startTransition] = useTransition(); const [itemList, setItemList] = useState([]); const [optimisticItems, setOptimisticItems] = useOptimistic(itemList);
As shown in the code above, we import all our hooks from React. As I mentioned earlier, we are using the “startTransition” function to simulate loading here. We are taking this function as the second value provided to us from the useTransition hook in React, the first being the loading state for the function we use startTransition with.
I have created a function here to replicate the API service call behavior of fetching data. It is the createItem function. It basically returns me a value with a 2-second delay. Other than this, we just have the inputRef, which we will use to get the data from input and useState to put the data in an array.
The last line of the code above is what we are looking for. We use the useOptimistic hook to duplicate our itemList, storing it in the variable optimisticItems. The second item we get from this hook is a function that updates the same copied state. We will use this method to initially update our copied state until the service call is complete and we have the correct data in our itemList variable. So, let’s examine the function where we will make this call.
const onClick = async () => { const inputValue = inputRef.current.value; if (inputRef.current == null || !inputValue) return; setOptimisticItems((list) => [ ...list, { itemName: inputValue, key: crypto.randomUUID(), loading: true, }, ]); const newItem = await createItem(inputValue); setItemList((list) => [ ...list, { key: crypto.randomUUID(), itemName: newItem, }, ]); };
setOptimisticItems Function
Let’s go through this code one by one. First, we take the value of input from inputRef and check to see if it is empty. If it is empty, we will simply return from the function. Next, we have the setOptimisticItems function, which is basically the main purpose of this blog.
Here, we are keeping it as simple as it could get. We get the previous state as the first parameter of this function, which would be the existing list of items, so we will spread that array and add our new value to its end. This now creates our data array and puts the latest data with it before the service call starts in the following line. We are also giving it a loading value which we will use to indicate this is not the complete data, more on that later.
In the following line, we are making the service call (or mocking a service call in this case), getting actual data from the service, and updating our state with that exact data. Therefore, until we perform the await operation, we have already displayed the desired data using the optimistic value. Let’s see how we can implement using the loading indicator I mentioned earlier.
<div className="col-md-6 p-5 ms-4"> <div>Item's List:</div> <ul className="my-3"> {optimisticItems.map(({ itemName, loading, key }) => ( <li key={key} className={loading ? "opacity-50" : "opacity-100"}> {itemName} {loading ? "..." : null} </li> ))} </ul> </div>
In the above code, we show that the value that is not loaded yet will be displayed with half opacity and will have an ellipsis at the end to indicate it is loading or waiting for the service call to be completed.
Output
Initial form state:
The user enters a value in the input field:
The Add button can be clicked to start a service call:
The value appears less visible than standard text and has an ellipsis at the end to indicate the loading state. When the loading is complete or the API call successfully completes, it will appear in its normal state, as shown in the image below.
This is basically how we can show the data before it is ultimately part of the database and indicate that proper data is loading using this hook. Another screenshot to show the same.
One simple yet powerful way to optimize user interactions in Next.js apps is to include the ‘useOptimistic’ hook. Developers may guarantee improved user pleasure and better performance by implementing optimistic updates. ‘useOptimistic’ is a helpful utility for Next.js developers, offering better user experiences with less work thanks to its effect and simplicity.