Next.js

Next.js is a popular web framework built on React, known for its server-side rendering (SSR) support and file-based routing. It provides an excellent developer experience by automatically configuring the necessary tools for React and TypeScript, making it particularly user-friendly for beginners.

Initialize a New Project

To create a new Next.js project, run:

npx create-next-app@latest

It will generated following files:

├── README.md
├── next.config.mjs
├── "package-lock.json"
├── package.json
├── src
│   └── app
│       ├── favicon.ico
│       ├── fonts
│       │   ├── GeistMonoVF.woff
│       │   └── GeistVF.woff
│       ├── globals.css
│       ├── layout.tsx
│       ├── page.module.css
│       └── page.tsx
└── tsconfig.json

Start the dev server:

npm run dev

Routing

Next.js uses file-based routing. For example, the URL http://localhost:3000/my-page corresponds to the file at src/app/my-page/page.tsx.

If you access http://localhost:3000/my-page in the browser, you'll see a 404 page. To create the page, add the following code to src/app/my-page/page.tsx:

export default function MyPage() {
  return <h1>My Page</h1>
}

Once saved, The page in browser should update it self as expected.

Fetching Data

Now let's add a new page src/app/users/page.tsx, to render some dynamic data:

export default async function Users() {
  const response = await fetch('https://dummyjson.com/users')
  const { users }: {
    users: Array<{
      id: number,
      firstName: string,
      lastName: string,
    }>
  } = await data.json()

  return (
    <div>
      {users.map((user) => (
        <div key={user.id}>
          <div>
            {user.firstName} {user.lastName}
          </div>
        </div>
      ))}
    </div>
  )
}

Visit http://localhost:3000/users to see the rendered page.

Dynamic Routing

To create a user detail page, first, add a link in users/page.tsx:

import Link from 'next/link'

{users.map((user) => (
  <div key={user.id}>
    <div>
      <Link href={`/users/${user.id}`}>{user.firstName} {user.lastName}</Link>
    </div>
  </div>
))}

Clicking one of the links will lead to a URL like http://localhost:3000/users/26, which initially shows a 404 page.

To handle this route, create a new file at src/app/users/[id]/page.tsx with the following content:

export default async function User({ params }: { params: { id: string } }) {
  const response = await fetch(`https://dummyjson.com/users/${params.id}`)
  const user: {
    firstName: string,
    lastName: string,
    email: string,
  } = await data.json()

  return (
    <div>
      <h1>{user.firstName} {user.lastName}</h1>
      <div>{user.email}</div>
    </div>
  )
}

Once saved the web page in browser should update it self and show the user email.

Server Actions

Server actions are similar to RPCs; the client can invoke server-side functions as if they were client-side. Behind the scenes, it still makes an HTTP request. The compiler and the framework did a lot of work here like code splitting and transforming, so you don't need to manually create routes and call the APIs.

Here's how it looks like. Create a new page at src/app/products/page.tsx with the following content:

export default function Products() {
  
  async function addProduct(data: FormData) {
    "use server"
    console.log(data.get('name'))
  }

  return (
    <form action={addProduct}>
      <div>
        <label>
          Name: <input name="name" />
        </label>
      </div>
      <button type="submit">Submit</button>
    </form>
  )
}

Visit http://localhost:3000/products, fill out the form, and submit. In the network panel, you’ll see a POST request to the current path. Additionally, the server-side log will print the name, confirming that the function executed on the server.

useActionState

useActionState allows you to access the result of a form action.

It is a client only hook, and you will have to put "use client" on top of you soruce code file.

"use client"

import { useActionState } from 'react'
import { addProduct } from './action'

export default function Products() {
  
  const [state, submitAction, isPending] = useActionState(addProduct, {message: ''})

  return (
    <form action={submitAction}>
      <div>
        <label>Name: <input name="name" /></label>
      </div>
      <button disabled={isPending} type="submit">Create</button>
      <div>{state.message}</div>
    </form>
  )
}

And the action code have to be moved to a file with "use server":

"use server"

export async function addProduct(prevState: {message: string}, data: FormData) {
  return { message: `${data.get('name')} saved` }
}