NextJS Server Components are a game changer

I've been building full stack webapps for few years now and i'm always left with disguest/confusion/disbelief on how complicated they're to build. I boiled down one of the key reasons being that to read/write to a DB from react app has to go through many layers, both to make the request as well as get the result.

This is where server components come in. They remove any need for a REST/Graphql layer. You just retrieve all DB data strait in the react component you need the data from. I created an example shopping cart below to show off how easy server components are to work with. I created an example shopping cart project to see how much easier it is to build when you're loading data directly in react. Shopping cart is a good example because its data driven.

Demo

Video below gives soome basic functionality of adding a new "Avacado" product to the database with a price, the viewing the full lsit of products, and then deleting the new product. All fully server side, no javascript, using nextjs.

Full code available on Github

When watching that, if you're not wowed by all that functionality being server side, you're either a PHP / old school dev and use to this kind of simplicity, or this article isn't for you. Feel free to leave. In the react world, normally that functionality, would require at least 3 endpoints: one to fetch all products, one to create a product, one to delete a product. All the endpoints would be written far away from the code that used it, we'd then in react need some kind of client to hit those endpoints, some `useEffect`, with loading state, etc.

When working with server components, all that lift is skipped and instead the code looks like this:


// copy from https://github.com/jbsiddall/demo-server-components-shopping-cart/blob/main/app/admin/page.tsx

import { prisma } from "../prisma"
import Link from "next/link"
import { TrashIcon } from '@heroicons/react/24/solid'
import { formDataToMap } from "../util"
import { z } from "zod"
import { revalidatePath } from "next/cache"

const Validator = z.object({
    productId: z.string()
})

export const dynamic = 'auto';
export  const revalidate = 5

export default async function Admin() {
    const allProducts = await prisma.product.findMany()

    const deleteProduct = async (formData: FormData) => {
        'use server';
        const {productId} = Validator.parse(formDataToMap(formData))
        await prisma.product.delete({where: {id: parseInt(productId)}})
        revalidatePath('/admin')
    }

    return (
        <>
            <h2>Products</h2>
            <div className="not-prose self-end">
                <Link className="btn btn-primary" href="/admin/product/add">Add New Product</Link>
            </div>
            <table className="table">
                <thead>
                    <tr>
                        <th>ID</th>
                        <th>Name</th>
                        <th>Price</th>
                        <th>Action</th>
                    </tr>
                </thead>
                <tbody>
                    {allProducts.map(({id, name, price}) => (
                        <tr key={id} className="bg-base-200">
                            <td>{id}</td>
                            <td>{name}</td>
                            <td>{price}</td>
                            <td className="flex justify-end">
                                <form action={deleteProduct}>
                                    <input type='hidden' name='productId' value={id} />
                                    <button className="btn btn-error">
                                        <TrashIcon color="#FFF" width={16} height={16} />
                                    </button>
                                </form>
                            </td>
                        </tr>
                    ))}
                </tbody>
            </table>
        </>
    )
}

Things to note: