Server Actions in Next.js: Enhancing User Experience with Optimistic Updates

Server Actions in Next.js: Enhancing User Experience with Optimistic Updates

Server Actions is an alpha feature in Next.js that allows for server-side data mutations and reduced client-side JavaScript. It’s built on top of React Actions, providing progressively enhanced forms that can significantly improve your app’s user experience and performance. In this article, we’ll dive into the details of how to use Server Actions in Next.js and showcase how to implement optimistic updates to further enhance your app’s UX.

Table of Contents

Progressive Enhancement

Progressive Enhancement is a fundamental concept in web development that ensures the proper functioning of web pages regardless of whether JavaScript is enabled or not. One important aspect of Progressive Enhancement is that it allows forms to function properly even if JavaScript is not loaded or fails to load. This ensures that users can interact with forms and submit data without facing any technical glitches.

For example, consider a simple login form. Without Progressive Enhancement, if the JavaScript for the form fails to load, users would be unable to log in. However, by using Progressive Enhancement, the form would still function properly even if the JavaScript fails to load. This is because the form would still be usable and would submit data to the server via the standard HTML form submission method.

Enabling Server Actions

To enable Server Actions in a Next.js project, the experimental serverActions flag must be set to true in the next.config.js file.

const nextConfig = {
	experimental: {
		serverActions: true,
	},
};

Creating Server Action

In Next.js, a Server Action is created by defining an asynchronous function that has the “use server” directive at the top of its body. This function should have arguments and a return value that follows the React Server Components protocol.

async function serverAction() {
	"use server";
	//...
}

If you have a single file that exports multiple server actions, you can add a top-level “use server” directive to the file instead of adding it to each function. This way, all exports in that file will be considered Server Actions.

Experimental: useOptimistic hook

The experimental useOptimistic hook is a powerful tool that allows you to implement optimistic updates in your application. Essentially, optimistic updates are a technique that makes your app feel more responsive and snappy by immediately updating the UI to reflect what the user expects to happen. This means that when a server action is triggered, the UI is updated right away to show what the expected outcome will be, rather than waiting for the server to respond. This can help improve the overall user experience and make your app feel more polished and professional.

Sounds pretty cool, right? Let’s see how to use the useOptimistic hook with server actions.

Server Action with useOptimistic hook

"use client";
import { experimental_useOptimistic as useOptimistic } from "react";
import { post } from "./_actions.js";

export function Comments({ comments }) {
	const [optimisticComments, addOptimisticComment] = useOptimistic(
		comments,
		(state, newComment) => [...state, { text: newComment, saving: true }]
	);
	const formRef = useRef();

	return (
		<div>
			{optimisticComments.map((c) => (
				<div>
					{c.text}
					{c.saving ? "Saving..." : ""}
				</div>
			))}
			<form
				action={async (formData) => {
					const comment = formData.get("comment");
					formRef.current.reset();
					addOptimisticComment(comment);
					await post(comment);
				}}
				ref={formRef}
			>
				<input type="text" name="comment" />
			</form>
		</div>
	);
}

In this example, the Comments component renders a list of comments and a form to submit a new comment. The useOptimistic hook is used to provide an optimistic UI update when saving the content to database. When the form is submitted, the new comment is added to the list of comments with a saving flag set to true, and the UI is updated immediately to reflect this. The comment is then submitted to the server, and if the submission is successful, the flag is set to false and the UI is updated again to show the final state of the comment. This helps to provide a very smooth user experience and reduces the perceived latency of saving a comment. Less code than before and much more readable.

Demo

Server Actions and useOptimistic Hook Demo
Server Actions and useOptimistic Hook Demo

Code: Github

To create the demo showcasing the use of server actions and useOptimistic hook in Next.js, I combined the examples discussed earlier in this article. When the user types in and submits a new comment in the form, the UI immediately renders it without waiting for a response from the server.

This demo uses the following:

  1. Latest version of NextJS (v13.4.1)
  2. SQLite as db (maybe Vercel KV would have been better here?)
  3. Prisma
  4. Tailwind
  5. TypeScript

Don’t forget to run the migration script before booting up the app. I had a great time building it. Please provide feedback and feel free to make a PR. :)

Conclusion

In conclusion, Next.js Server Actions and the experimental useOptimistic hook offer exciting possibilities for building more performant and responsive applications. By enabling server-side data mutations and enhancing the user experience with optimistic updates, developers can create web applications that are both faster and more enjoyable to use. So let’s stay optimistic about the future of web dev and continue to explore new tools and techniques for building great applications!

Resources