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
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)
- use PHP (server-side rendering) to build the client side
- let's make the client side in JavaScript (React)
- 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.
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);
}
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
Release details
https://nextjs.org/blog/next-14#server-actions-stableDocuments
https://nextjs.org/docs/app/building-your-application/data-fetching/server-actionsRelated Pull Requests
https://github.com/facebook/react/pull/26774
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
Case 1: The simplest Server Actions
- Render a
<form>
in the server component. - Write a function (=Server Actions) that is associated with the action of
<form>
. - 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>
);
}
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>
),
)}
</>
);
}
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.
- 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);
}
- 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>
);
}
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' } ]
}
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>
);
}
- 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} />
</>
);
}
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' }
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).
- Forms and Mutations https://github.com/vercel/next.js/tree/canary/examples/next-forms
'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>
)
}
'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' }
}
}
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}';
}
- 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>
);
}
- 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>
</>
);
}
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}`;
}
{/* Server Components */}
<form action={serverActionEmpty}>
<G serverActionDBA={serverActionDBA} id={id} name={name} pos={pos} />
</form>
'use client';
<button onClick={ async () => {
// Validation
// ...
const result = await serverActionDBA({
id: _id, name: _name, pos: _pos
});
console.log(result);
}}
>
G
</button>
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.
(sleepy...)
Top comments (0)