Skip to Content
ExamplesNext.js JSON Form

Next.js JSON Form

A contact form built with Next.js App Router that uses a server action to validate input and persist submissions to a local JSON file. React 19’s useActionState handles form state — no useState needed.

How it works

The page is a server component that reads contacts.json on every request and renders the list. Below the list sits a client component wrapping a plain <form>. When the user submits, the browser calls a server action that validates the fields, appends the new contact to the JSON file, and calls revalidatePath so Next.js re-renders the page with fresh data.

nextjs-json-form/ ├── data/contacts.json ← JSON "database" (seeded with one entry) ├── app/layout.tsx ← root layout (minimal) ├── app/page.tsx ← server component: reads JSON, renders list + form ├── app/actions.ts ← server action: validates, writes JSON, revalidates └── app/contact-form.tsx ← client component: useActionState + isPending

data/contacts.json

Seed file with one entry so the list isn’t empty on first load. The server action appends to this array.

[ { "id": 1, "name": "Ada Lovelace", "email": "ada@example.com", "message": "First entry in the guestbook." } ]

app/layout.tsx

Minimal root layout — just the HTML shell and the system font stack.

import type { ReactNode } from 'react' export const metadata = { title: 'Next.js JSON Form' } export default function RootLayout({ children }: { children: ReactNode }) { return ( <html lang="en"> <body style={{ fontFamily: 'system-ui, sans-serif', maxWidth: 480, margin: '2rem auto' }}> {children} </body> </html> ) }

app/actions.ts

'use server' makes every export in this file a server action. The function receives the previous FormState plus the FormData submitted by the browser. It validates, reads the JSON file, appends the new contact, writes it back, and calls revalidatePath('/') so the page re-renders with the updated list.

app/actions.ts

app/page.tsx

This is a server component — it runs on the server and can call readFileSync directly. It reads the JSON file, maps the contacts into a list, and renders the client ContactForm below.

app/page.tsx

app/contact-form.tsx

'use client' marks this as a client component so it can use hooks. useActionState takes the server action and an initial state, returning [state, formAction, isPending]. The formAction is passed straight to <form action={...}> — React handles the submission, serialization, and state update. When state.success is true, the form was submitted and the page has already re-rendered with the new contact. isPending disables the button while the action runs.

app/contact-form.tsx

Run it

npx create-next-app@latest nextjs-json-form --ts --app --no-tailwind --no-eslint --no-src-dir cd nextjs-json-form mkdir data # create the files above: data/contacts.json, app/layout.tsx, app/page.tsx, app/actions.ts, app/contact-form.tsx npm run dev

Open http://localhost:3000 , fill out the form, and watch the contact appear in the list. Check data/contacts.json to see it persisted.

Last updated on