Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: useFormState #55564

Merged
merged 7 commits into from Sep 22, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -64,31 +64,64 @@ export default function PostList({ posts }) {

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:

```jsx filename="app/ui/Navigation.js"
```tsx filename="app/components/links.tsx"
'use client'

import { usePathname } from 'next/navigation'
import Link from 'next/link'

export function Navigation({ navLinks }) {
export function Links() {
const pathname = usePathname()

return (
<>
{navLinks.map((link) => {
const isActive = pathname === link.href
<nav>
<ul>
<li>
<Link className={`link ${pathname === '/' ? 'active' : ''}`} href="/">
Home
</Link>
</li>
<li>
<Link
className={`link ${pathname === '/about' ? 'active' : ''}`}
href="/"
>
About
</Link>
</li>
</ul>
</nav>
)
}
```

```jsx filename="app/components/links.jsx"
'use client'

import { usePathname } from 'next/navigation'
import Link from 'next/link'

export function Links() {
const pathname = usePathname()

return (
return (
<nav>
<ul>
<li>
<Link className={`link ${pathname === '/' ? 'active' : ''}`} href="/">
Home
</Link>
</li>
<li>
<Link
className={isActive ? 'text-blue' : 'text-black'}
href={link.href}
key={link.name}
className={`link ${pathname === '/about' ? 'active' : ''}`}
href="/"
>
{link.name}
About
</Link>
)
})}
</>
</li>
</ul>
</nav>
)
}
```
Expand Down
Expand Up @@ -361,14 +361,16 @@ export default async function submit(formData) {

<AppOnly>

Use the `useFormStatus` hook to show a loading state when a form is submitting on the server:
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.

```tsx filename="app/page.tsx" switcher
For example, the following submit button:

```tsx filename="app/submit-button.tsx" switcher
'use client'

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

function SubmitButton() {
export function SubmitButton() {
const { pending } = useFormStatus()

return (
Expand All @@ -377,12 +379,12 @@ function SubmitButton() {
}
```

```jsx filename="app/page.jsx" switcher
```jsx filename="app/submit-button.jsx" switcher
'use client'

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

function SubmitButton() {
export function SubmitButton() {
const { pending } = useFormStatus()

return (
Expand All @@ -391,9 +393,40 @@ function SubmitButton() {
}
```

> **Good to know:**
>
> - 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.
leerob marked this conversation as resolved.
Show resolved Hide resolved
`<SubmitButton />` can then be used in a form with a Server Action:

```tsx filename="app/page.tsx" switcher
import { SubmitButton } from '@/app/submit-button'

export default async function Home() {
return (
<form action={...}>
<input type="text" name="field-name" />
<SubmitButton />
</form>
)
}
```

```jsx filename="app/page.jsx" switcher
import { SubmitButton } from '@/app/submit-button'

export default async function Home() {
return (
<form action={...}>
<input type="text" name="field-name" />
<SubmitButton />
</form>
)
}
```

<details>
<summary>Examples</summary>

- [Form with Loading & Error States](https://github.com/vercel/next.js/tree/canary/examples/next-forms)

</details>

</AppOnly>

Expand Down Expand Up @@ -485,89 +518,116 @@ export default function Page() {

<AppOnly>

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:
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:

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

export async function create(formData: FormData) {
export async function create(prevState: any, formData: FormData) {
try {
await createItem(formData.get('item'))
revalidatePath('/')
return { message: 'Success!' }
await createItem(formData.get('todo'))
return revalidatePath('/')
} catch (e) {
return { message: 'There was an error.' }
return { message: 'Failed to create' }
}
}
```

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

export async function create(formData) {
export async function createTodo(prevState, formData) {
try {
await createItem(formData.get('item'))
revalidatePath('/')
return { message: 'Success!' }
await createItem(formData.get('todo'))
return revalidatePath('/')
} catch (e) {
return { message: 'There was an error.' }
return { message: 'Failed to create' }
}
}
```

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.
Then, from a Client Component, you can read this value and display an error message.

```tsx filename="app/page.tsx" switcher
```tsx filename="app/add-form.tsx" switcher
'use client'

import { create } from './actions'
import { useState } from 'react'
import { experimental_useFormState as useFormState } from 'react-dom'
import { experimental_useFormStatus as useFormStatus } from 'react-dom'
import { createTodo } from '@/app/actions'

export default function Page() {
const [message, setMessage] = useState<string>('')
const initialState = {
message: null,
}

async function onCreate(formData: FormData) {
const res = await create(formData)
setMessage(res.message)
}
function SubmitButton() {
const { pending } = useFormStatus()

return (
<button type="submit" aria-disabled={pending}>
Add
</button>
)
}

export function AddForm() {
const [state, formAction] = useFormState(createTodo, initialState)

return (
<form action={onCreate}>
<input type="text" name="item" />
<button type="submit">Add</button>
<p>{message}</p>
<form action={formAction}>
<label htmlFor="todo">Enter Task</label>
<input type="text" id="todo" name="todo" required />
<SubmitButton />
<p aria-live="polite" className="sr-only">
{state?.message}
</p>
</form>
)
}
```

```jsx filename="app/page.jsx" switcher
```jsx filename="app/add-form.jsx" switcher
'use client'

import { create } from './actions'
import { useState } from 'react'
import { experimental_useFormState as useFormState } from 'react-dom'
import { experimental_useFormStatus as useFormStatus } from 'react-dom'
import { createTodo } from '@/app/actions'

export default function Page() {
const [message, setMessage] = useState('')
const initialState = {
message: null,
}

async function onCreate(formData) {
const res = await create(formData)
setMessage(res.message)
}
function SubmitButton() {
const { pending } = useFormStatus()

return (
<form action={onCreate}>
<input type="text" name="item" />
<button type="submit">Add</button>
<p>{message}</p>
<button type="submit" aria-disabled={pending}>
Add
</button>
)
}

export function AddForm() {
const [state, formAction] = useFormState(createTodo, initialState)

return (
<form action={formAction}>
<label htmlFor="todo">Enter Task</label>
<input type="text" id="todo" name="todo" required />
<SubmitButton />
<p aria-live="polite" className="sr-only">
{state?.message}
</p>
</form>
)
}
```

> **Good to know:**
>
> - 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.
<details>
<summary>Examples</summary>

- [Form with Loading & Error States](https://github.com/vercel/next.js/tree/canary/examples/next-forms)

</details>

</AppOnly>

Expand Down
22 changes: 13 additions & 9 deletions examples/next-forms/app/actions.ts
Expand Up @@ -9,7 +9,7 @@ import { z } from 'zod'
// text TEXT NOT NULL
// );

export async function createTodo(formData: FormData) {
export async function createTodo(prevState: any, formData: FormData) {
const schema = z.object({
todo: z.string().nonempty(),
})
Expand All @@ -24,25 +24,29 @@ export async function createTodo(formData: FormData) {
`

revalidatePath('/')

return { message: 'Saved successfully' }
return { message: `Added todo ${data.todo}` }
} catch (e) {
return { message: 'Failed to create todo' }
}
}

export async function deleteTodo(formData: FormData) {
export async function deleteTodo(prevState: any, formData: FormData) {
const schema = z.object({
id: z.string().nonempty(),
})
const data = schema.parse({
id: formData.get('id'),
})

await sql`
DELETE FROM todos
WHERE id = ${data.id};
`
try {
await sql`
DELETE FROM todos
WHERE id = ${data.id};
`

revalidatePath('/')
revalidatePath('/')
return { message: 'Deleted todo' }
leerob marked this conversation as resolved.
Show resolved Hide resolved
} catch (e) {
return { message: 'Failed to delete todo' }
}
}
34 changes: 34 additions & 0 deletions examples/next-forms/app/add-form.tsx
@@ -0,0 +1,34 @@
'use client'

import { experimental_useFormState as useFormState } from 'react-dom'
import { experimental_useFormStatus as useFormStatus } from 'react-dom'
import { createTodo } from '@/app/actions'

const initialState = {
message: null,
}

function SubmitButton() {
const { pending } = useFormStatus()

return (
<button type="submit" aria-disabled={pending}>
Add
</button>
)
}

export function AddForm() {
const [state, formAction] = useFormState(createTodo, initialState)

return (
<form action={formAction}>
<label htmlFor="todo">Enter Task</label>
<input type="text" id="todo" name="todo" required />
<SubmitButton />
<p aria-live="polite" className="sr-only">
leerob marked this conversation as resolved.
Show resolved Hide resolved
{state?.message}
</p>
</form>
)
}