What are flash messages?
Flash messages in this case refers to any notifications that you want to send to the user from the server as a result of an action that has been performed on their behalf. This can include messages from form errors to success updates.
AstroJS provides a context for you to interact with in API routes, .astro
pages or components and middlewares. The context allows you to interact with the request, params, cookies and local data that can be set on it globally.
Using this context object, we will add flash messages to cookies which will expire in 5 seconds (you can choose a lower expiry or maxAge for your flash messages as you don’t need it to be around during the next render or page navigation).
Requirements
This guide assumes that you have created an astro project already, if not, visit the installation page to create a new one.
You will also need to enable On Demand Rendering Mode with any Astro SSR Adapter for this to work. Since we will be using API Endpoints, Actions and updating cookies, we need to enable an SSR Adapter in astro for it to work. In the example below, I use the node ssr apapter.
Getting Started
We will work on an example where we will use Astro Actions to submit a contact form and then we will use cookies to send flash messages. Astro Actions allow us to define backend functions that run on the server and so it has access to the context
object and can either be called manually through javascript even in react or ui framework components in astro. They are kind of like react server actions but more opinionated (in a very good way). Astro actions provide us with type saftey, input validation with zod, handling client erros and are a great alternative for API Endpoints and form submissions. We can call actions on the server or client and in our case we will use it in a form as a post request which will allow the form to work even without javascript enabled (of course for the flash messages, we will use javascript on the client to display it but you can also just display it directly on the page without using javascript).
We will first create our action (in src/actions/index.ts
) with will validate the email of the user with zod:
// src/actions/index.ts
import { ActionError, defineAction } from "astro:actions";
import { z } from "astro:schema";
export const server = {
contact: defineAction({
accept: "form",
input: z.object({
email: z
.string({
required_error: "Email is required",
invalid_type_error: "Email must be text",
})
.email("Email must be a valid email address"),
message: z
.string({
required_error: "Message is required",
invalid_type_error: "Message must be text",
})
}),
handler: async (input, context) => {
// send the message to the user
await sendMessageToUser(input); // this function is just a placeholder since we are not focusing on how to actually send the message
// this is where we will set our cookies
context.cookies.set(
"flash",
{
message: "Thank you for sending us a message. We will get back to you soon!",
type: "info",
},
{ maxAge: 5 }
);
return { success: true };
},
})
}
The important part of the code above is where we access the context to create the cookie, context.cookies.set()
. The first parameter is the name of the cookie, in this case, flash
, the second parameter is the data we want to save which can be a string or an object in which case, we save an object with a message and type properties. The last parameter is the cookie options, this can include if the cookie should be secure
, the path
, maxAge
, expiry
and other usual cookie options.
The form is not soo much important for the functionality to work but I will just show an example below that uses the action above with a redirect to the /success
page. You can simply add the astro action with action={actions.contact}
but if you want astro to redirect by default on success, you can add the redirect path before it as in action={"/my-redirect-path" + actions.myAction}
.
---
import { actions } from "astro:actions";
import { isInputError } from "astro:actions";
const result = Astro.getActionResult(actions.contact);
const inputErrors = isInputError(result?.error) ? result.error.fields : {};
---
<form method="POST" action={"/success" + actions.contact}>
<div class="grid gap-4">
<div class="grid gap-2">
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
name="email"
required
aria-describedby="email-error"
aria-invalid={Boolean(inputErrors.email)}
/>
<p
id="email-error"
role="alert"
class:list={[
"text-[0.8rem] font-medium text-muted-foreground flex items-center gap-2",
{
block: Boolean(inputErrors.email),
},
{
hidden: !Boolean(inputErrors.email),
},
]}
>
{inputErrors.email?.join(", ")}
</p>
</div>
<div class="grid gap-2">
<label htmlFor="message">Message</label>
<textarea
id="message"
name="message"
required
aria-describedby="message-error"
aria-invalid={Boolean(inputErrors.message)}
/>
<p
id="message-error"
role="alert"
class:list={[
"text-[0.8rem] font-medium text-muted-foreground flex items-center gap-2",
{
block: Boolean(inputErrors.message),
},
{
hidden: !Boolean(inputErrors.message),
},
]}
>
{inputErrors.message?.join(", ")}
</p>
</div>
</div>
</form>
Once we set the cookie, we need to be able to access it in our page, in my index.astro
page where I have my form, I access the cookie data. You can also convieniently access this data in your layout instead to make sure that it’s available across all your pages. This can be useful if you redirect to other pages in your form and you want to be able to pass flash messages along to the new page you redirect to.
---
const flash = Astro.cookies.get("flash")?.json(); // get the data as json
// The output of of our flash should be undefined | {message: string; type: string;}
// if you only stored a string, then you can access it as Astro.cookies.get('flash')?.value
---
After you access this data, you can then use any library or custom code to render toasts or notifications to show the flash message. Using Shadcn ui, you can even use an alert to show the toast. Although by default, the alert component does not come with any dismiss action, you can still use it for static messages that you want to stay on the page till at least the user refreshes the page by which time, your cookie should have expired.
Even though, our cookie is set to expire after 5 seconds, we can still manually delete it after the toast or notification has been sent using an api route:
// src/pages/toasts.ts
import type { APIRoute } from "astro";
export const GET: APIRoute = async ({ url, cookies }) => {
const action = url.searchParams.get("action");
if (!action) {
return new Response(null, { status: 200 });
}
if (action === "clear") {
cookies.delete("flash");
}
return new Response(null, { status: 200 });
};
The api route by default does not fail or throw errors because we just want to clear our toasts so we don’t want to deal with annoying try catch
patterns just to clear toast cookies.
We check for an action param from the url search params and if there’s an action param with a value of clear, we delete the flash message. This api route can be useful later if we also want to support setting the flash messages using an api route so you can support multiple action param values like create
and then set the cookie for the flash message.
Showcase
Just to show how you might implement the toasts in your component, find below an example using react
and shadcn ui with the Toast component. If you want to follow this example, you will need to follow this shadcn guide to setup shadcn with astro which will guide you to install the tailwind and react astro integrations and then you can go ahead to install the Toast component from shadcn.
// src/components/react/flash-toast.tsx
import React from "react";
import { Toaster } from "./ui/toaster";
import { useToast } from "@/hooks/react/use-toast";
export const FlashToast = ({
type,
message,
}: {
message: string | null;
type: string | null;
}) => {
const { toast } = useToast();
React.useEffect(() => {
if (type) {
switch (type) {
case "error":
toast({
title: "Error",
description: message,
variant: "destructive",
});
break;
case "success":
toast({ title: "Success", description: message });
break;
case "warning":
toast({
title: "Warning",
description: message,
variant: "destructive",
});
break;
default:
toast({ title: "Info", description: message });
break;
}
// clear toast
fetch("/toasts?action=clear");
}
}, []);
return (
<>
<Toaster />
</>
);
};
In the example above, I also choose to clear the toasts using the api endpoint we created earlier. You can choose to skip this step and just make sure your cookie expires early enough so that it’s not persisted or shown again when the user refreshes the page or navigates to another page immediately or before it’s expired.
After creating your react component, you can then go to your astro layout or index.astro
file where you added the expression to get the flash message, const flash = Astro.cookies.get("flash")?.json();
, import and use the client component. We will use the astro client directive, client:idle
to make sure that our toast loads once the page is done with loading it’s initial payload.
---
import { FlashToast } from "@/components/react/flash-toast";
const flash = Astro.cookies.get("flash")?.json(); // get the data as json
---
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="description"
content="Welcome to astro authentication made easy."
/>
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="generator" content={Astro.generator} />
<title>My Awesome App</title>
</head>
<body class="antialiased relative scroll-smooth">
<h1> My Awesome App </h1>
<!-- Form goes here -->
<form>...</form>
<!--\ Form ends here -->
<FlashToast
client:load
message={flash?.message}
type={flash?.type}
/>
</body>
</html>
Of course this example only uses react but since astro allows you to use react/preact/svelte/alpinejs/vue/solid-js
and other ui frameworks, you can easily implement the toast differently in your application or even write your own toast with just the style tag and script tag in your astro page or layout.