Show toast notifications on form submit with Remix
Once nice UI affordance you can give your users is to show a confirmation message after they submit their form successfully. This is a great way to let them know their action was successful and that the page is loading.
- Submitting forms on the same page
- Use react-toast
- Use manual cookies
- Use a cookie session storage
- Example toast component
Most of the time, you'll be submitting forms to their own routes. You have components on a page, and in the same file you have an action
that is handling their submission.
In these cases, you can simply return the message from the action and use the useActionData
hook to show a toast notification when the form is submitted.
export async function action() { // handle form submission return json({ message: "Your form was submitted successfully", })}export default function Example() { const { message } = useActionData() return ( <div> <Form /> {message ? <Toast message={message} /> : null} </div> )}
Submitting forms across pages
If you submit a form
- to an action on a different page,
- to an action that redirects to a different page,
- or want to show the notification later;
Then the useActionData()
hook won't work. You'll need to store the confirmation message somewhere and read it when you want to show it.
The remix-toast library gives you some helpers that do this automatically for you.
There is redirectWithToast
, redirectWithSuccess
, and similar variants for Error, Info, and Warning.
export async function action() { // handle form submission return redirectWithToast("/", { message: "Your form was submitted successfully", })}
There are three routes that make sense for showing the toast notification.
- The page you're redirecting to
- A layout page that wraps the page you're redirecting to
- The root layout route
If you only need the toast messages in one place, prefer the former, but the root is a good place if you plan on using this pattern across your app.
// root.tsximport { getToast } from "remix-toast"export async function loader() { const { toast, headers } = getToast() return json({ toast }, { headers })}export default function Root() { const { toast } = useLoaderData() return ( <div> <Form /> {toast ? <Toast message={toast.message} /> : null} </div> )}
There are more examples on the react-toast docs for integrating with toast libraries like react-toastify
or sonner
Or roll your own toast
If you don't want to use a library, you can do this yourself.
In the response of the action, add a Set-Cookie
header with the message you want to show.
export async function action() { // handle form submission return redirect("/thank-you", { headers: { "Set-Cookie": `message=Your form was submitted successfully; Path=/;`, }, })}
Then, on the page you want to show the message, read the cookie and clear it.
export async function loader({ request,}: LoaderFunctionArgs) { const message = request.headers .get("Cookie") ?.match(/message=([^;]+)/)?.[1] return json( { message }, { headers: { "Set-Cookie": `message=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT`, }, }, )}
Using a cookie session storage
For a better developer experience, you can use Remix's built-in cookie session storage instead of manually parsing the cookies. This will also make it easier to use the same code in a production environment.
Create a session.server.ts
file as per the session docs.
Rewrite the action to use the getSession
and commitSession
functions.
import { commitSession, getSession,} from "./session.server.ts"export async function action({ params, request,}: ActionFunctionArgs) { // handle form submission const session = await getSession( request.headers.get("Cookie"), ) session.flash("message", `Task created!`) return new Response(null, { status: 201, headers: { "Set-Cookie": await commitSession(session), }, })}
And then read it in the loader of the page you want to show the message.
import { getSession } from "./session.server.ts"export async function loader({ request,}: LoaderFunctionArgs) { const session = await getSession( request.headers.get("Cookie"), ) const message = session.get("message") || null return json( { message }, { headers: { "Set-Cookie": await commitSession(session), }, }, )}
If you don't want to commit the session every time, there are ways to set up your Remix codebase to auto-commit sessions
Displaying a simple toast message
I recommend using a library like react-toastify or sonner for toasts, but you can build your own basic one.
Here is a sample toast component using Tailwind that you can use to display the message. It will automatically hide itself after a few seconds.
function Toast({ message, time = 5000,}: { message: string time?: number}) { const [show, setShow] = useState(true) useEffect(() => { const timeout = setTimeout(() => setShow(false), 2000) return () => clearTimeout(timeout) }, []) return ( <Transition show={show} enter="transition-opacity duration-300" enterFrom="opacity-0" enterTo="opacity-100" leave="transition-opacity duration-300" leaveFrom="opacity-100" leaveTo="opacity-0" > <div className="fixed bottom-4 right-4 rounded-lg border border-gray-100 bg-white px-4 py-2 text-left text-sm font-medium shadow-lg"> {message} </div> </Transition> )}
And you can display in your page like this.
{ message ? <Toast key={message} message={message} /> : null}