Jacob Paris
← Back to all content

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.

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.

tsx
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.

tsx
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.

tsx
// root.tsx
import { 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.

tsx
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.

tsx
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`,
},
},
)
}

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.

tsx
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.

tsx
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.

tsx
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.

tsx
{
message ? <Toast key={message} message={message} /> : null
}
Professional headshot
Moulton
Moulton

Hey there! I'm a developer, designer, and digital nomad building cool things with Remix, and I'm also writing Moulton, the Remix Community Newsletter

About once per month, I send an email with:

  • New guides and tutorials
  • Upcoming talks, meetups, and events
  • Cool new libraries and packages
  • What's new in the latest versions of Remix

Stay up to date with everything in the Remix community by entering your email below.

Unsubscribe at any time.