Skip to content

Commit 5f4238d

Browse files
leerobJohnPhamous
andauthored
docs: useFormState (#55564)
This PR shows how to use a new React hook `useFormState` in the context of the [Forms and Mutations](https://nextjs.org/docs/app/building-your-application/data-fetching/forms-and-mutations) docs. It also updates the forms example (`next-forms`) to show the recommended patterns for loading / error states. Related: #55399 --- Co-authored-by: John Pham <johnphammail@gmail.com>
1 parent c923257 commit 5f4238d

9 files changed

Lines changed: 321 additions & 103 deletions

File tree

docs/02-app/01-building-your-application/01-routing/03-linking-and-navigating.mdx

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -64,31 +64,64 @@ export default function PostList({ posts }) {
6464

6565
You can use [`usePathname()`](/docs/app/api-reference/functions/use-pathname) to determine if a link is active. For example, to add a class to the active link, you can check if the current `pathname` matches the `href` of the link:
6666

67-
```jsx filename="app/ui/Navigation.js"
67+
```tsx filename="app/components/links.tsx"
6868
'use client'
6969

7070
import { usePathname } from 'next/navigation'
7171
import Link from 'next/link'
7272

73-
export function Navigation({ navLinks }) {
73+
export function Links() {
7474
const pathname = usePathname()
7575

7676
return (
77-
<>
78-
{navLinks.map((link) => {
79-
const isActive = pathname === link.href
77+
<nav>
78+
<ul>
79+
<li>
80+
<Link className={`link ${pathname === '/' ? 'active' : ''}`} href="/">
81+
Home
82+
</Link>
83+
</li>
84+
<li>
85+
<Link
86+
className={`link ${pathname === '/about' ? 'active' : ''}`}
87+
href="/"
88+
>
89+
About
90+
</Link>
91+
</li>
92+
</ul>
93+
</nav>
94+
)
95+
}
96+
```
97+
98+
```jsx filename="app/components/links.jsx"
99+
'use client'
100+
101+
import { usePathname } from 'next/navigation'
102+
import Link from 'next/link'
103+
104+
export function Links() {
105+
const pathname = usePathname()
80106

81-
return (
107+
return (
108+
<nav>
109+
<ul>
110+
<li>
111+
<Link className={`link ${pathname === '/' ? 'active' : ''}`} href="/">
112+
Home
113+
</Link>
114+
</li>
115+
<li>
82116
<Link
83-
className={isActive ? 'text-blue' : 'text-black'}
84-
href={link.href}
85-
key={link.name}
117+
className={`link ${pathname === '/about' ? 'active' : ''}`}
118+
href="/"
86119
>
87-
{link.name}
120+
About
88121
</Link>
89-
)
90-
})}
91-
</>
122+
</li>
123+
</ul>
124+
</nav>
92125
)
93126
}
94127
```

docs/02-app/01-building-your-application/02-data-fetching/03-forms-and-mutations.mdx

Lines changed: 109 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -360,14 +360,16 @@ export default async function submit(formData) {
360360

361361
<AppOnly>
362362

363-
Use the `useFormStatus` hook to show a loading state when a form is submitting on the server:
363+
Use the `useFormStatus` hook to show a loading state when a form is submitting on the server. The `useFormStatus` hook can only be used as a child of a `form` element using a Server Action.
364364

365-
```tsx filename="app/page.tsx" switcher
365+
For example, the following submit button:
366+
367+
```tsx filename="app/submit-button.tsx" switcher
366368
'use client'
367369

368370
import { experimental_useFormStatus as useFormStatus } from 'react-dom'
369371

370-
function SubmitButton() {
372+
export function SubmitButton() {
371373
const { pending } = useFormStatus()
372374

373375
return (
@@ -376,12 +378,12 @@ function SubmitButton() {
376378
}
377379
```
378380

379-
```jsx filename="app/page.jsx" switcher
381+
```jsx filename="app/submit-button.jsx" switcher
380382
'use client'
381383

382384
import { experimental_useFormStatus as useFormStatus } from 'react-dom'
383385

384-
function SubmitButton() {
386+
export function SubmitButton() {
385387
const { pending } = useFormStatus()
386388

387389
return (
@@ -390,9 +392,40 @@ function SubmitButton() {
390392
}
391393
```
392394

393-
> **Good to know:**
394-
>
395-
> - Displaying loading or error states currently requires using Client Components. We are exploring options for server-side functions to retrieve these values as we move forward in stability for Server Actions.
395+
`<SubmitButton />` can then be used in a form with a Server Action:
396+
397+
```tsx filename="app/page.tsx" switcher
398+
import { SubmitButton } from '@/app/submit-button'
399+
400+
export default async function Home() {
401+
return (
402+
<form action={...}>
403+
<input type="text" name="field-name" />
404+
<SubmitButton />
405+
</form>
406+
)
407+
}
408+
```
409+
410+
```jsx filename="app/page.jsx" switcher
411+
import { SubmitButton } from '@/app/submit-button'
412+
413+
export default async function Home() {
414+
return (
415+
<form action={...}>
416+
<input type="text" name="field-name" />
417+
<SubmitButton />
418+
</form>
419+
)
420+
}
421+
```
422+
423+
<details>
424+
<summary>Examples</summary>
425+
426+
- [Form with Loading & Error States](https://github.com/vercel/next.js/tree/canary/examples/next-forms)
427+
428+
</details>
396429

397430
</AppOnly>
398431

@@ -484,89 +517,116 @@ export default function Page() {
484517

485518
<AppOnly>
486519

487-
Server Actions can also return [serializable objects](https://developer.mozilla.org/docs/Glossary/Serialization). For example, your Server Action might handle errors from creating a new item, returning either a success or error message:
520+
Server Actions can also return [serializable objects](https://developer.mozilla.org/docs/Glossary/Serialization). For example, your Server Action might handle errors from creating a new item:
488521

489522
```ts filename="app/actions.ts" switcher
490523
'use server'
491524

492-
export async function create(formData: FormData) {
525+
export async function create(prevState: any, formData: FormData) {
493526
try {
494-
await createItem(formData.get('item'))
495-
revalidatePath('/')
496-
return { message: 'Success!' }
527+
await createItem(formData.get('todo'))
528+
return revalidatePath('/')
497529
} catch (e) {
498-
return { message: 'There was an error.' }
530+
return { message: 'Failed to create' }
499531
}
500532
}
501533
```
502534

503535
```js filename="app/actions.js" switcher
504536
'use server'
505537

506-
export async function create(formData) {
538+
export async function createTodo(prevState, formData) {
507539
try {
508-
await createItem(formData.get('item'))
509-
revalidatePath('/')
510-
return { message: 'Success!' }
540+
await createItem(formData.get('todo'))
541+
return revalidatePath('/')
511542
} catch (e) {
512-
return { message: 'There was an error.' }
543+
return { message: 'Failed to create' }
513544
}
514545
}
515546
```
516547

517-
Then, from a Client Component, you can read this value and save it to state, allowing the component to display the result of the Server Action to the viewer.
548+
Then, from a Client Component, you can read this value and display an error message.
518549

519-
```tsx filename="app/page.tsx" switcher
550+
```tsx filename="app/add-form.tsx" switcher
520551
'use client'
521552

522-
import { create } from './actions'
523-
import { useState } from 'react'
553+
import { experimental_useFormState as useFormState } from 'react-dom'
554+
import { experimental_useFormStatus as useFormStatus } from 'react-dom'
555+
import { createTodo } from '@/app/actions'
524556

525-
export default function Page() {
526-
const [message, setMessage] = useState<string>('')
557+
const initialState = {
558+
message: null,
559+
}
527560

528-
async function onCreate(formData: FormData) {
529-
const res = await create(formData)
530-
setMessage(res.message)
531-
}
561+
function SubmitButton() {
562+
const { pending } = useFormStatus()
563+
564+
return (
565+
<button type="submit" aria-disabled={pending}>
566+
Add
567+
</button>
568+
)
569+
}
570+
571+
export function AddForm() {
572+
const [state, formAction] = useFormState(createTodo, initialState)
532573

533574
return (
534-
<form action={onCreate}>
535-
<input type="text" name="item" />
536-
<button type="submit">Add</button>
537-
<p>{message}</p>
575+
<form action={formAction}>
576+
<label htmlFor="todo">Enter Task</label>
577+
<input type="text" id="todo" name="todo" required />
578+
<SubmitButton />
579+
<p aria-live="polite" className="sr-only">
580+
{state?.message}
581+
</p>
538582
</form>
539583
)
540584
}
541585
```
542586

543-
```jsx filename="app/page.jsx" switcher
587+
```jsx filename="app/add-form.jsx" switcher
544588
'use client'
545589

546-
import { create } from './actions'
547-
import { useState } from 'react'
590+
import { experimental_useFormState as useFormState } from 'react-dom'
591+
import { experimental_useFormStatus as useFormStatus } from 'react-dom'
592+
import { createTodo } from '@/app/actions'
548593

549-
export default function Page() {
550-
const [message, setMessage] = useState('')
594+
const initialState = {
595+
message: null,
596+
}
551597

552-
async function onCreate(formData) {
553-
const res = await create(formData)
554-
setMessage(res.message)
555-
}
598+
function SubmitButton() {
599+
const { pending } = useFormStatus()
556600

557601
return (
558-
<form action={onCreate}>
559-
<input type="text" name="item" />
560-
<button type="submit">Add</button>
561-
<p>{message}</p>
602+
<button type="submit" aria-disabled={pending}>
603+
Add
604+
</button>
605+
)
606+
}
607+
608+
export function AddForm() {
609+
const [state, formAction] = useFormState(createTodo, initialState)
610+
611+
return (
612+
<form action={formAction}>
613+
<label htmlFor="todo">Enter Task</label>
614+
<input type="text" id="todo" name="todo" required />
615+
<SubmitButton />
616+
<p aria-live="polite" className="sr-only">
617+
{state?.message}
618+
</p>
562619
</form>
563620
)
564621
}
565622
```
566623
567-
> **Good to know:**
568-
>
569-
> - Displaying loading or error states currently requires using Client Components. We are exploring options for server-side functions to retrieve these values as we move forward in stability for Server Actions.
624+
<details>
625+
<summary>Examples</summary>
626+
627+
- [Form with Loading & Error States](https://github.com/vercel/next.js/tree/canary/examples/next-forms)
628+
629+
</details>
570630
571631
</AppOnly>
572632

examples/next-forms/app/actions.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { z } from 'zod'
99
// text TEXT NOT NULL
1010
// );
1111

12-
export async function createTodo(formData: FormData) {
12+
export async function createTodo(prevState: any, formData: FormData) {
1313
const schema = z.object({
1414
todo: z.string().nonempty(),
1515
})
@@ -24,25 +24,29 @@ export async function createTodo(formData: FormData) {
2424
`
2525

2626
revalidatePath('/')
27-
28-
return { message: 'Saved successfully' }
27+
return { message: `Added todo ${data.todo}` }
2928
} catch (e) {
3029
return { message: 'Failed to create todo' }
3130
}
3231
}
3332

34-
export async function deleteTodo(formData: FormData) {
33+
export async function deleteTodo(prevState: any, formData: FormData) {
3534
const schema = z.object({
3635
id: z.string().nonempty(),
3736
})
3837
const data = schema.parse({
3938
id: formData.get('id'),
4039
})
4140

42-
await sql`
43-
DELETE FROM todos
44-
WHERE id = ${data.id};
45-
`
41+
try {
42+
await sql`
43+
DELETE FROM todos
44+
WHERE id = ${data.id};
45+
`
4646

47-
revalidatePath('/')
47+
revalidatePath('/')
48+
return { message: `Deleted todo ${data.todo}` }
49+
} catch (e) {
50+
return { message: 'Failed to delete todo' }
51+
}
4852
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
'use client'
2+
3+
import { experimental_useFormState as useFormState } from 'react-dom'
4+
import { experimental_useFormStatus as useFormStatus } from 'react-dom'
5+
import { createTodo } from '@/app/actions'
6+
7+
const initialState = {
8+
message: null,
9+
}
10+
11+
function SubmitButton() {
12+
const { pending } = useFormStatus()
13+
14+
return (
15+
<button type="submit" aria-disabled={pending}>
16+
Add
17+
</button>
18+
)
19+
}
20+
21+
export function AddForm() {
22+
const [state, formAction] = useFormState(createTodo, initialState)
23+
24+
return (
25+
<form action={formAction}>
26+
<label htmlFor="todo">Enter Task</label>
27+
<input type="text" id="todo" name="todo" required />
28+
<SubmitButton />
29+
<p aria-live="polite" className="sr-only" role="status">
30+
{state?.message}
31+
</p>
32+
</form>
33+
)
34+
}

0 commit comments

Comments
 (0)