React has introduced new form Actions and related hooks to enhance native forms and streamline client-server communication. These features enable developers to handle form submissions more effectively, improving both user experience and code maintainability. For an in-depth exploration of React form Actions, you can refer to my detailed post on my post about React Form Actions.
With React 18, the Server Components feature was introduced. Server components are not Server-Side Rendering (SSR), Server Components are executed exclusively on the server during both runtime and build time. These components can access server-side resources, such as databases and the file system, but they are not capable of performing client-side actions like event listeners or hooks.
To demonstrate the capabilities of Server Components and Server Actions, we'll use Next.js and Prisma.
Next.js is a React framework for building full-stack web applications. You use React Components to build user interfaces, and Next.js for additional features and optimizations. Under the hood, Next.js also abstracts and automatically configures tooling needed for React, like bundling, compiling, and more. This allows you to focus on building your application instead of spending time with configuration. learn more
Prisma is an ORM that simplifies database access and operations, allowing you to query and manipulate data without writing SQL. Learn more
Initial Setup
Start by creating a new Next.js application:
yarn create next-app server-example
Your initial folder structure will look like this:
Upgrade to the Canary Release to access React 19 features, including Server Actions:
yarn add next@rc react@rc react-dom@rc
install Prisma
yarn add prisma
Prisma Configuration
Create a Prisma schema file at src/lib/prisma/schema.prisma:
generator client { provider = "prisma-client-js" } datasource db { provider = "sqlite" url = "file:./dev.db" } model User { id Int @id @default(autoincrement()) email String @unique name String? age Int }
For demonstration purposes, we are using SQLite. For production, you should use a more robust database.
Next, add a Prisma client file at src/lib/prisma/prisma.ts
// ts-ignore 7017 is used to ignore the error that the global object is not // defined in the global scope. This is because the global object is only // defined in the global scope in Node.js and not in the browser. import { PrismaClient } from '@prisma/client' // PrismaClient is attached to the `global` object in development to prevent // exhausting your database connection limit. // // Learn more: // https://pris.ly/d/help/next-js-best-practices const globalForPrisma = global as unknown as { prisma: PrismaClient } export const prisma = globalForPrisma.prisma || new PrismaClient() if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma export default prisma
Configure Prisma in package.json:
{ //other settings "prisma": { "schema": "src/lib/prisma/schema.prisma", "seed": "ts-node src/lib/prisma/seed.ts" } }
And update TypeScript settings in tsconfig.json:
{ //Other settings here... "ts-node": { // these options are overrides used only by ts-node // same as the --compilerOptions flag and the // TS_NODE_COMPILER_OPTIONS environment variable "compilerOptions": { "module": "commonjs" } } }
Install ts-node globally:
yarn global add ts-node
Seeding Initial Data
Add a seed file at src/lib/prisma/seed.ts to populate initial data:
import { PrismaClient } from "@prisma/client"; const prisma = new PrismaClient(); async function main() { await prisma.user.create({ email: "[email protected]", name: "Anto", age: 35, }); await prisma.user.create({ email: "[email protected]", name: "Vinish", age: 32, }); } main() .then(async () => { await prisma.$disconnect(); }) .catch(async (e) => { console.error(e); await prisma.$disconnect(); process.exit(1); });
Install Prisma client
yarn add @prisma/client
Run the migration command:
yarn prisma migrate dev --name init
If the seed data is not reflected, add it manually:
yarn prisma db seed
Great! Since the installations are ready, you can create an actions file that performs database operations.
Creating Server Actions
Server Actions are a powerful feature that enables seamless client-server intercommunication. Let's create a file for database operations at src/actions/user.ts:
"use server"; import prisma from '@/lib/prisma/prisma' import { revalidatePath } from "next/cache"; // export type for user export type User = { id: number; name: string | null; email: string; age: number; }; export async function createUser(user: any) { const resp = await prisma.user.create({ data: user }); console.log("server Response"); revalidatePath("/"); return resp; } export async function getUsers() { return await prisma.user.findMany(); } export async function deleteUser(id: number) { await prisma.user.delete({ where: { id: id, }, }); revalidatePath("/"); }
Let's create a React server component to read and render data from the database. Create src/app/serverexample/page.tsx:
import UserList from "./Users"; import "./App.css" export default async function ServerPage() { return (); }
Add some styling in src/app/serverexample/App.css
.App { text-align: center; } .App-logo { height: 40vmin; pointer-events: none; } .App-header { background-color: #282c34; min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: calc(10px 2vmin); color: white; } input { color: #000; } .App-link { color: #61dafb; }
Create components to fetch and render the user list:
src/app/serverexample/UserList.tsx
import { getUsers } from "@/actions/user"; import { UserDetail } from "./UserDetail"; export default async function UserList() { //Api call to fetch User details const users = await getUsers(); return ({users.length ? ( users.map((user) =>); }) ) : ( No User found)}
src/app/serverexample/UserDetail.tsx
export function UserDetail({ user }) { return (); }{user.name}{user.email}
Run the development server:
yarn dev
Navigate to http://localhost:3000/serverexample to see the rendered user list:
By default, components in Next.js are server components unless you specify the "use client" directive. Notice two important points:
Server Actions enable seamless client-server intercommunication. Let's add a form to create new users.
Create a new file at src/app/serverexample/AddUser.tsx:
"use client"; import "./app.css"; import { useActionState } from "react"; import { createUser } from "../../actions/user"; const initialState = { error: undefined, }; export default function AddUser() { const submitHandler = async (_previousState: object, formData: FormData) => { try { // This is the Server Action method that transfers the control // Back to the server to do DB operations and get back the result. const response = await createUser({ name: formData.get("name") as string, email: formData.get("email") as string, age: parseInt(formData.get("age") as string), }); return { response }; } catch (error) { return { error }; } }; const [state, submitAction, isPending] = useActionState( submitHandler, initialState ); return (); }Add new User
{" "}
Update src/app/serverexample/page.tsx to include the AddUser component:
import UserList from "./UserList"; // Import new line import AddUser from "./AddUser"; import "./App.css" export default async function ServerPage() { return (); }{/* insert Add User here */}
Running the application will now allow you to add new users via the form, with server-side processing handled seamlessly.
The AddUser component is at the heart of this example, showcasing how React Server Actions can revolutionize the way we handle client-server interactions. This component renders a form for adding new users and leverages the useActionState hook to create a smooth and seamless bridge between the client-side interface and server-side operations.
How It Works
From the perspective of a developer working on the client side, it appears as if the form submission is handled locally. However, the heavy lifting such as database manipulation occurs on the server.
The useActionState hook encapsulates this process, managing the state transitions and handling errors, while maintaining an intuitive API for developers.
So that's with forms, now lets test an example without forms.
update src/app/serverexample/UserDetail.tsx
"use client"; import { deleteUser } from "@/actions/user"; import { useTransition } from "react"; export function UserDetail({ user }) { const [pending, startTransition] = useTransition(); const handleDelete = () => { startTransition(() => { deleteUser(user.id); }); }; return ({pending ? (); }Deleting...
) : (> )}{user.name}{user.email}
Key Points:
Now, you can seamlessly delete a user within the application:
This approach is transformative because it abstracts away the complexities of client-server communication. Traditionally, such interactions would require handling API endpoints, managing asynchronous requests, and carefully coordinating client-side state with server responses. With React Server Actions and the useActionState hook, this complexity is reduced, allowing developers to focus more on building features rather than worrying about the underlying infrastructure.
By using this pattern, you gain:
You can find the full code in the repository
Disclaimer: All resources provided are partly from the Internet. If there is any infringement of your copyright or other rights and interests, please explain the detailed reasons and provide proof of copyright or rights and interests and then send it to the email: [email protected] We will handle it for you as soon as possible.
Copyright© 2022 湘ICP备2022001581号-3