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 might be lacking an important concept for building interactive notifications like Toasts/Snackbars #55860

Closed
schimi-dev opened this issue Sep 23, 2023 · 6 comments
Labels
Documentation Related to Next.js' official documentation. locked

Comments

@schimi-dev
Copy link

What is the improvement or update you wish to see?

Since the docs: useFormState PR was merged, the Documentation around Forms and Mutations no longer provides any example on top of which you can build Toast/Snackbar notifications.

The PR removed the following Code from the Documentation:

'use client'
 
import { create } from './actions'
import { useState } from 'react'
 
export default function Page() {
  const [message, setMessage] = useState<string>('')
 
  async function onCreate(formData: FormData) {
    const res = await create(formData)
    setMessage(res.message)
  }
 
  return (
    <form action={onCreate}>
      <input type="text" name="item" />
      <button type="submit">Add</button>
      <p>{message}</p>
    </form>
  )
}

However, wrapping a Server Action in a Client Action and then adjusting React state in that Client Action seems like a Core principal of how Server Actions can be used. So, I think that at least one example showcasing this should be in the Next.js documentation. This comes of course with the tradeoff that Progressive Enhancement does not work, but it is an important principal for interactivity.

@leerob I tried to build Toast/Snackbar Notifications with useFormState as you suggested, but useFormState is lacking an important concept that allows to close these notification Client-side. This is described below in detail.

Is there any context that might help us understand?

Before the docs: useFormState PR was merged, the recommended pattern in the docs for showing error and success states for Server Actions was as follows:

'use client'
 
import { create } from './actions'
import { useState } from 'react'
 
export default function Page() {
  const [message, setMessage] = useState<string>('')
 
  async function onCreate(formData: FormData) {
    const res = await create(formData)
    setMessage(res.message)
  }
 
  return (
    <form action={onCreate}>
      <input type="text" name="item" />
      <button type="submit">Add</button>
      <p>{message}</p>
    </form>
  )
}

With that approach, it was nicely possible to implement Toast/Snackbar notifications, because you could easilty reset React state and close these notifications via Client-side click handlers or timeouts.

The Scenario was as follows:

  1. A Toast/Snackbar notification is shown by adjusting React state inside a Client Action after the Server Action completes.
  2. When the user clicks on a Cancel-Icon inside that Toast/Snackbar notification you can easily adjust the React state again, to close that Notification. (with a solely Client-side event handler)

The new useFormState approach:

With docs: useFormState the above approach was removed in favor of useFormState. However, with useFormState it does currently not seem possible to implement Toast/Snackbar notifications, as you do not have the possibility to reset the state Client-side. (e.g. via a Client-side onClick event on a Cancel-Icon inside the Toast/Snackbar)

Scenario for Implementing Toast/Snackbar notifications with useFormState:

  1. A Toast notification is opened when the state of useFormState becomes a certain value by executing the formAction. (This part is covered by the docs and should work fine.)
  2. When the user clicks on a Cancel-Icon inside that Toast Notification the state needs to be reset without executing the formAction or any other Server-side code. -> This part does not work with useFormState.

Does the docs page already exist? Please link to it.

https://nextjs.org/docs/app/building-your-application/data-fetching/forms-and-mutations

@schimi-dev schimi-dev added the Documentation Related to Next.js' official documentation. label Sep 23, 2023
@schimi-dev
Copy link
Author

Here are two code snippets to further clarify what I described above:

The approach that was removed by docs: useFormState (works fine)

'use client'

import { create } from './actions'
import { useState } from 'react'
import Snackbar from './components/Snackbar'

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

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

    return (
        <form action={onCreate}>
            <input type="text" name="item" />
            <button type="submit">Add</button>
            <Snackbar
                open={!!message}
                onClose={() => setMessage('')} // Being able to do this is important for using Snackbar/Toast notifications
            >
                {message}
            </Snackbar>
        </form>
    )
}

The new useFormState approach (does not work as it is not possible to dismiss a notification Client-side)

'use client'

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

const initialState = {
    message: null,
}

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 />
            <button type="submit">Add</button>
            <Snackbar
                open={!!state?.message}
                onClose={????????????} // How do I dismiss a Snackbar/Toast with `useFormState`?
            >
                {state?.message}
            </Snackbar>
        </form>
    )
}

@infodusha
Copy link

Can't you do:

export function AddForm() {
    const [hidden, setHidden] = useState(false);
    const [state, formAction] = useFormState(createTodo, initialState);

    return (
        <form action={formAction}>
            <label htmlFor="todo">Enter Task</label>
            <input type="text" id="todo" name="todo" required />
            <button type="submit">Add</button>
            <Snackbar
                open={!!state?.message && !hidden}
                onClose={() => setHidden(true)}
            >
                {state?.message}
            </Snackbar>
        </form>
    )
}

@schimi-dev
Copy link
Author

That works for one form submission, but after that I have to manage resetting the hidden state to false again, which is just shifting this limitation somewhere else. I know that there are several workarounds for this problem like adjusting some state, using refs and hacks to manually reset them and force a rerender, or maybe even useEffect. You could also use such patterns to directly derive a resettable open state based on changes in form state, but these are patterns I would rather not like to roll out into building blocks of a larger application.

By the way useFormState is great, and I just moved away from Toast notifications for now. Also, it solves a problem regarding undefined being returned on the client-side when a Server Action calls redirect conditionally e.g. on a session timeout and a success message in success case. The previous pattern had a problem w.r.t. that.

I was just wondering if any opinion/practice for this will find its way into the Next.js documentation, because Toast/Snackbar notifications are somtething that many developers coming from SPAs are really used to.

@steve-marmalade
Copy link

@schimi-dev I came here with the same question, and really appreciate your detailed and thoughtful framing of the problem. I feel like it might be worth keeping this issue open, as the points you raised don't seem adequately answered by the docs (which, as you stated, have significantly de-emphasized Client Actions even though the alternatives don't cover all use cases).

@leerob
Copy link
Member

leerob commented Nov 8, 2023

Open to contributions for the docs if you think there's a minimal way to add this example in the forms and mutations docs!

Copy link
Contributor

This closed issue has been automatically locked because it had no new activity for 2 weeks. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Nov 23, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Documentation Related to Next.js' official documentation. locked
Projects
None yet
Development

No branches or pull requests

4 participants