Working with server components and server actions in Next.js

Alexander Galev

Alexander Galev

Author

Published: Aug 06, 2024

Last Edited: Aug 07, 2024

Recently, I’ve been jumping back and forth between mobile app development and web development. Every time I transition, I try to grasp a new technology piece and attempt to do something familiar but in a different way. We know the saying that ‘tech is moving fast’ and I’m experiencing it every time I come back to a mature library or framework like React or Next and I see the progress that has taken place in the span of a few months.

So What’s new?

Quite an extensive list actually, but I’ll try to keep it brief and focus on the most impactful additions - namely Server Components and Server Actions .

Let’s start with Server Components

React introduced this new type of component with the intention to take rendition out of the client and do the work ahead of time. What this actually means is a transition in the way data is fetched - in the past we had to implement a useEffect hook and place a fetch request inside. This resulted in our application doing a few re-renders:


  1. The client component renders the static content. The fetched data is missing and being loaded.
  2. The asynchronous function in useEffect hook resolves and triggers a re-render.
  3. We are finally presented with the complete and populated page.

This approach took a bit of effort to smooth out and developers reached for other tooling from the React world like Suspense or just implementing a “Loading State”. The React team realized this short-coming and hence we now have Server Components.

With this new paradigm, we can read the data and then render it in the component like so:

export default async function Page({blogId}) {
//Note: loads *during* render.
const blogPost = await db.posts.get(id)

	return (
	<main>
		<h1>Author: {blogPost.author}</h1>
		<p>{blogPost.content}</p>
	</main>
	)
}

Notice the async prefix on the React Component - this is the indicator that we are now dealing with a Server Component. It is required - as we are utilizing an async/await approach for fetching the data on the lines below. It’s a much cleaner way of accessing data!

Server Actions

One limitation with the new Server Components infrastructure is that we are unable to use the good ol’ reliable hooks that we’ve grown to love over the years. The main selling point of Server Action is that you can actually use asynchronous tasks in a Client Component with the help of a Server Action. Here is some syntax to put a Server Action in, well, action for credentials authentication:

// lib/actions.ts
'use server'

export async function authenticate(_currentState: unknown, formData: FormData) {
  await signIn('credentials', formData) //comes from Auth.js
}

And then we can import this server action and use it in our login form:

// components/LoginForm.tsx
'use client'
import { useFormState, useFormStatus } from 'react-dom'
import { authenticate } from '@/lib/actions'

export default function LoginForm({ session }: { session: any }) {
	const [errorMessage, dispatch] = useFormState(authenticate, undefined)
	
	// button that is disabled while our form is in submission state
	function LoginButton() {
    const { pending } = useFormStatus()

    return (
      <button disabled={pending} aria-disabled={pending} type='submit'>
        {pending ? 'Logging in...' : 'Login'}
      </button>
    )
  }
  
  return (
	  <form action={dispatch}>
	      <label htmlFor='email'>Your email address</Label>
	      <input name='email' type='email' placeholder='Email' required />
	      <label htmlFor='password'>Your password</label>
	      <input name='password' type='password' placeholder='Password' required />
	      <LoginButton />
	    </form>
	  )
}

Few things are happening here:


  1. We are importing a server action in a client component.
  2. We are using a new set of hooks useFormState and useFormStatus (will be referred to as useActionState in upcoming versions of React)
  3. We are plugging in our server action in the useFormState hook and prepare it for dispatch.
  4. The dispatch is happening through the action attribute of the form. You can think of it as onSubmit.
  5. When we dispatch the action, we enter a “pending” state while the form is being submitted. This state is being utilized in the LoginButton component that disables it and changes the text inside.

I gave the simplest example here, but I’m a big fan of this new approach of constructing forms. The server action can also validate the data before dispatching it and return errorMessage - I used it on a recent project with the zod library.

Main takeaways

Although Next.js is known to be a full stack framework, I was never comfortable writing APIs inside of it, until now. With the help of an ORM like Prisma/Drizzle/Supabase - I find it way more intuitive to fetch data from within a server component using async/await. Additionally, with the newly introduced server actions, we are witnessing a bridging of the gap between a client/server/database. All this tooling has the goal to boost developer productivity and I must say I am here to reap the benefits.

“Stay Hungry, Stay Foolish” - Steve Jobs

Recent Articles