DEV Community

VTeacher
VTeacher

Posted on

Wow! Server Actions are now Stable from Next.js 14! Dramatic changes in React and Next's form

I am Satoko, a member of VTeacher.

I am a front-end engineer and back-end engineer at the same time.
I love to select a combination of classic and slightly geeky technologies!

Next.js Conf

Today (midnight to morning Japan time), Next.js Conf was held!

https://www.youtube.com/live/8q2q_820Sx4?t=5485

Next.js 14 was announced at this time and Server Actions are now officially Stable!

https://x.com/nextjs/status/1717596665690091542

I see that Clerks are also being used as usual.

https://x.com/dan_abramov/status/1717652653570736469

What is Server Actions?

About half a year ago, Server Actions (alpha version) was announced on the 4th day of Vercel Ship.

https://youtu.be/M4vrwI5PDI0?t=880

The mystery of "use server" that had been circulating on SNS was solved, and I got the impression that the reaction was great, in both good and bad ways.

A discussion of the direction React/Next is heading

Server Actions, but there are some critical opinions.

"(Back to PHP?)"

https://x.com/levelsio/status/1654053489004417026

This is my theory that I have been preaching since the alpha version...,

In a way, I feel it is similar to after React Server Components was announced by Facebook (now Meta), and I think it is a re-heat because it has become more realistic with the appearance of Server Actions 🤔.

Reacet Server Components ... RSC for short.
RSC was announced by Meta (formerly Facebook) at the end of 2020 and has since been working on the project with Vercel.

https://beta.nextjs.org/docs/rendering/server-and-client-components

Image description

What is the RSC, after all?

First of all, but first, here is my impression of Meta's front-end team.
Perhaps because Facebook (https://facebook.com/) was originally built with PHP, I have the impression that even though they are in charge of the front-end, they also have the perspective of a back-end engineer and place a lot of emphasis on performance. I have the impression that they are very performance oriented.

https://www.youtube.com/watch?v=8pDqJVdNa44

Historically, the following history and challenges have been involved,
(in chronological order from 1 to 2 to 3)

  1. use PHP (server-side rendering) to build the client side
  2. let's make the client side in JavaScript (React)
  3. Let's make JavaScript (React) render on the server side like PHP

I had issues with all of them,
This time, we are trying to make it possible to select either server-side rendering or client-side rendering for each component. This is the challenge.
That is the understanding of Reacet Server Components (RSC).

I have the impression that Meta is consistently focusing on the following.

  • Usability (native apps vs. HTML5 as in the past)
  • Performance (especially in terms of display speed)
  • Productivity (ecosystem development)

None of these are special, and in a sense, my impression is that they are quietly doing "normal" things.
(Only the scale is different).

But this is where the discussion heats up, mainly because of the passion from the users.

  • Is it really cool?
  • Isn't it going against the times?
  • Is it not good enough as it is now?

This time, the heated discussion occurred in Server Actions.

The opening "(revert back to PHP?)" is,
I personally think it is hard to say 🤔.
However, it may result in the impression that "(back to PHP, right?)".

I think there is no doubt that this is a turning point.
(If it is a turning point, it may be long past 😅)

Server Actions Overview

Until now, it was necessary to prepare APIs for extracting, registering, updating, and deleting form data. However, with the advent of Server Actions, the hassles of the past have been eliminated, and form data can now be handled within the server component. The way to handle form data will change dramatically.

Image description

Point.

The 'use server' in the function is a key point when using Server Actions.

You can also import and use Server Actions functions.

// 'use server';

export async function create(formData: FormData) {
  'use server';
  console.log(formData);
}
Enter fullscreen mode Exit fullscreen mode

Server Actions can be used in both server and client components (see below).

Forgetting to include 'use server' will result in an error.

Since the server component is processed on the server side, DB operations (ORM) are also an option. I personally am a big fan of the fact that form data on the client side can be easily handled on the server side!

https://x.com/dan_abramov/status/1654132342313701378

For more specific Server Actions, please see below!

What we tried to make

This is what our engineers have tried to make.

  • Next 14.0.0
  • nrstate (state management)
  • Server Actions
  • Zod (Validation)

https://nrstate-demo.vercel.app/demo/

*If you are viewing on a smartphone, please remove the orientation lock 🔐 and view it in landscape mode!

Documents

Try it out

To try it out, install Next.js 14 (the latest version is now available on npm).

Here is the code our engineers tried.

# Install dependencies:
pnpm install

# Start the dev server:
pnpm dev
Enter fullscreen mode Exit fullscreen mode

Image description

Case 1: The simplest Server Actions

  1. Render a <form> in the server component.
  2. Write a function (=Server Actions) that is associated with the action of <form>.
  3. Write 'use server' in the function of Server Actions.

It may be true for SPA in general, but until now it has been difficult to write Mutate-type processes (registration, update, deletion, etc.) in a clean and simple manner.
We always had to prepare an API, handle onClick on the client, payload the form data, and send it to the server.

With the advent of Server Actions, this processing method has changed dramatically.
It enjoys the characteristics of a server component and can be written in an intuitive manner.

// ...

export default async function B() {

  // ...

  async function fServerAction(formData: FormData) {
    'use server';

    console.log(formData);
  }

  return (
    <ul>
      {examples.map(
        ({ id, name }: { id: string; name: string; }) => (
          <li key={id}>
            {/* @ts-expect-error Async Server Component */}
            <form action={fServerAction}>
              <input type="text" name="id" defaultValue={id} />
              <input type="text" name="name" defaultValue={name} />
              <button type="submit">
                Try Server Actions
              </button>
            </form>
          </li>
        ),
      )}
    </ul>
  );
}
Enter fullscreen mode Exit fullscreen mode

Case2: Server Actions using formAction

Additionally, the following elements allow you to use Server Actions using formAction.

  • <button>
  • <input type="submit" ...>
  • <input type="image" ...>

React did not support the formAction attribute, but it is now possible to pass it.

In this case, even if Server Actions are written in <form action= ...>, they will not be called.
This will take precedence.

export default async function B() {

// ... 

  async function fServerAction(formData: FormData) {
    'use server';

    console.log('--- fServerAction ---');
    console.log(formData);
  }

  async function fServerActionWithFormAction(formData: FormData) {
    'use server';

    console.log('--- fServerActionWithFormAction ---');
    console.log(formData);
  }

  return (
    <>
      {examples.map(
        ({ id, name, pos }: { id: string; name: string; pos: string }) => (
          <p key={id} className="m-5">
            {/* @ts-expect-error Async Server Component */}
            <form action={fServerAction}>
              <input type="text" name="id" defaultValue={id} />
              <input type="text" name="name" defaultValue={name} />
              <input type="text" name="pos" defaultValue={pos} />
              <button
                type="submit"
                className="w-1/12 rounded bg-blue-500 p-2 font-bold text-white hover:bg-blue-700 "
                formAction={fServerActionWithFormAction}
              >
                Mutate
              </button>
            </form>
          </p>
        ),
      )}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Case3: Call Server Actions from client component

I thought Server Actions was a function only for server components, but it turns out that it can also be called from client components.

This looks like it's going to change dramatically.
It seems like his client OR server can be realized in component units.

Image description

  • Server Actions
    • _action.tsx
'use server';

export async function fServerActionFromAnywhere(formData: FormData) {
  // 'use server'; 

  console.log('--- fServerAction ---');
  console.log(formData);
}

export async function fServerActionWithFormActionFromAnywhere(formData: FormData) {
  // 'use server'; 

  console.log('--- fServerActionWithFormAction ---');
  console.log(formData);
}
Enter fullscreen mode Exit fullscreen mode
  • Client component
    • B.client.tsx
'use client';

// ...

import {
  fServerActionFromAnywhere,
  fServerActionWithFormActionFromAnywhere,
} from './_action';

export default function B({ children }: { children: React.ReactNode }) {

  // ...

  const { a, d } = pageState;

  // ...

  return (
    <div>
      <form name="fServerAction" action={fServerActionFromAnywhere}>
        <input type="hidden" name="a" defaultValue={a} />
        <input type="hidden" name="d" defaultValue={d} />
        <button
          type="submit"
        >
          ServerAction
        </button>
      </form>
      <form name="fServerActionWithFormAction">
        <input type="hidden" name="a" defaultValue={a} />
        <input type="hidden" name="d" defaultValue={d} />
        <button
          type="submit"
          formAction={fServerActionWithFormActionFromAnywhere}
        >
          with formAction
        </button>
      </form>
      <div>{children}</div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

The following logs are generated on the server side.

--- fServerAction ---
FormData {
  [Symbol(state)]: [
    { name: 'a', value: '' },
    { name: 'd', value: 'asc' },
    { name: 'd', value: 'asc' }
  ]
}

--- fServerActionWithFormAction ---
FormData {
  [Symbol(state)]: [ { name: 'a', value: '' }, { name: 'd', value: 'asc' } ]
}
Enter fullscreen mode Exit fullscreen mode

Case4: Getting a value from Page({ params }).

Incidentally, the Server Actions seem to be performed on the server side again, with automatic implicit state transitions (reloading).
(For this reason, you may see some samples that use it in combination with router.refresh().)

The reason for this is a feature of RSC, as described in the following.

For now, you must re-render the entire React tree from the root server component
For now, you must re-render the entire React tree from the root server component.
https://www.plasmic.app/blog/how-react-server-components-work

With this relationship, Server Actions can get values from Page({ params }), as in the following example.

  • app/demo/[id]/like-button.tsx
'use client';

export default function LikeButton({ increment }: { increment: () => void }) {
  return (
    <button
      onClick={async () => {
        await increment();
      }}
    >
      Like
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode
  • app/demo/[id]/page.tsx
import LikeButton from './like-button';

import type { ReadonlyURLSearchParams } from 'next/navigation';
export type PageProps = {
  params: {};
  searchParams: ReadonlyURLSearchParams & {
    location: string;
  };
};

export default async function Page(pageProps: PageProps) {
  async function increment() { // Server Actions
    'use server';
    console.log(pageProps);
  }

  return (
    <>
      <LikeButton increment={increment} />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Access it at the following URL and click the Like button that appears on the screen,

http://localhost:3000/demo/123?location=456

You will see the following log on the server side

{ params: { id: '123' }, searchParams: { location: '456' }
Enter fullscreen mode Exit fullscreen mode

Validation: Checking form values

You may want to rely on the Validation libraries you have been using, such as react-hook-form or Zod.

The official documentation shows how to do the following (Zod example).

'use client'

import { experimental_useFormState as useFormState } from 'react-dom'
import { useFormStatus } from 'react-dom'
import { createTodo } from '@/app/actions'

const initialState = {
  message: null,
}

function SubmitButton() {
  const { pending } = useFormStatus()

  return (
    <button type="submit" aria-disabled={pending}>
      Add
    </button>
  )
}

export function AddForm() {
  const [state, formAction] = useFormState(createTodo, initialState)

  return (
    <form action={formAction}>
      <label htmlFor="todo">Enter Task</label>
      <input type="text" id="todo" name="todo" required />
      <SubmitButton />
      <p aria-live="polite" className="sr-only" role="status">
        {state?.message}
      </p>
    </form>
  )
}
Enter fullscreen mode Exit fullscreen mode
'use server'

import { revalidatePath } from 'next/cache'
import { sql } from '@vercel/postgres'
import { z } from 'zod'

// CREATE TABLE todos (
//   id SERIAL PRIMARY KEY,
//   text TEXT NOT NULL
// );

export async function createTodo(prevState: any, formData: FormData) {
  const schema = z.object({
    todo: z.string().min(1),
  })
  const data = schema.parse({
    todo: formData.get('todo'),
  })

  try {
    await sql`
    INSERT INTO todos (text)
    VALUES (${data.todo})
  `

    revalidatePath('/')
    return { message: `Added todo ${data.todo}` }
  } catch (e) {
    return { message: 'Failed to create todo' }
  }
}

export async function deleteTodo(prevState: any, formData: FormData) {
  const schema = z.object({
    id: z.string().min(1),
    todo: z.string().min(1),
  })
  const data = schema.parse({
    id: formData.get('id'),
    todo: formData.get('todo'),
  })

  try {
    await sql`
      DELETE FROM todos
      WHERE id = ${data.id};
    `

    revalidatePath('/')
    return { message: `Deleted todo ${data.todo}` }
  } catch (e) {
    return { message: 'Failed to delete todo' }
  }
}
Enter fullscreen mode Exit fullscreen mode

The method introduced in Case 3 can also be used to call Server Actions from the client component (Zod example).

  • Server Actions
    • app/demo/server-actions/serverActionG.tsx
'use server';

// ...

export async function serverActionDBA({
  id,
  name,
  pos,
}: {
  id: string;
  name: string;
  pos: string;
}) {

  // ...

  return '{status: 200}';
}

Enter fullscreen mode Exit fullscreen mode
  • Server component
    • app/demo/B.server.tsx
// ...

import G from './G';
import { serverActionDBA } from './server-actions/serverActionG';

export default async function B() {

  // ...

  return (
    <ul className="list-disc">
      {rows.map(({ id, name, pos }) => (
        <li key={id} className="m-5">
          <G id={id} name={name} pos={pos} serverActionDBA={serverActionDBA} />
          <Suspense fallback={<div></div>}>
            {/* @ts-expect-error Async Server Component */}
            <F_server id={id} name={name} pos={pos} />
          </Suspense>
        </li>
      ))}
    </ul>
  );
}
Enter fullscreen mode Exit fullscreen mode
  • Client component
    • app/demo/G.tsx
'use client';

import { useState } from 'react';
import { z } from 'zod';

export default function G({
  id,
  name,
  pos,
  serverActionDBA,
}: {
  id: string;
  name: string;
  pos: string;
  serverActionDBA: ({
    id,
    name,
    pos,
  }: {
    id: string;
    name: string;
    pos: string;
  }) => Promise<string>;
}) {
  const [error, setError] = useState('');

  return (
    <>
      <div>
        <>
          <form
            action={async (formData: FormData) => {
              const ValidationSchema = z.object({
                id: z.string().min(2),
                name: z.string().max(25),
                pos: z.string().max(10),
              });

              try {
                ValidationSchema.parse({
                  id: formData.get('id'),
                  name: formData.get('name'),
                  pos: formData.get('pos'),
                } as z.infer<typeof ValidationSchema>);

                const result = await serverActionDBA({
                  id: formData.get('id')?.toString() ?? '',
                  name: formData.get('name')?.toString() ?? '',
                  pos: formData.get('pos')?.toString() ?? '',
                });

                console.log(result);

                setError('');
              } catch (error) {
                setError('Error');
              }
            }}
          >
            <input
              name="id"
              type="text"
              className="w-1/5 rounded border-gray-200"
              defaultValue={id}
            />
            <input
              name="name"
              type="text"
              className="w-2/5 rounded border-gray-200"
              defaultValue={name}
            />
            <input
              name="pos"
              type="text"
              className="w-1/5 rounded border-gray-200"
              defaultValue={pos}
            />
            <div className="inline w-1/5">
              <button className="w-16 rounded bg-blue-500 p-2 font-bold text-white hover:bg-blue-700">
                Save
              </button>
            </div>
            <p className="text-red-600">{error}</p>
          </form>
        </>
      </div>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

The code for this area can be found here.

https://github.com/vteacher-online/nrstate-demo/tree/example/next13-boilerplate

Pattern without FormData

You can also use only Server Actions (empty the process associated with FormData).

'use server';

export async function serverActionEmpty() {}

export async function serverActionDBA({ id, name, pos }: { id: string; name: string; pos: string; }) {
  return `serverActionDBA: id=${id}, name=${name}, pos=${pos}`;
}
Enter fullscreen mode Exit fullscreen mode
{/* Server Components */}
<form action={serverActionEmpty}>
  <G serverActionDBA={serverActionDBA} id={id} name={name} pos={pos} />
</form>
Enter fullscreen mode Exit fullscreen mode
'use client';

<button onClick={ async () => {

    // Validation
    // ...

    const result = await serverActionDBA({
        id: _id, name: _name, pos: _pos
    });

    console.log(result);
}}
>
  G
</button>
Enter fullscreen mode Exit fullscreen mode

The code for this area can be found here.

https://github.com/vteacher-online/nrstate-demo/tree/example/server-actions-validation

In the end

You've been very busy at Next.js Conf.

Image description

(sleepy...)

Top comments (0)