Flash Notifications

How can we display a success or failure message after a POST, which should not always be displayed at the form, but rather as a message on the page to which the request was redirected, preferably using a toast or even when redirecting? It could help when accessing protected routes, and you need to know why you can't access them.
7 Replies
peerreynders
peerreynders•4w ago
Assuming: - You have a toast provider that makes posting toasts available to all components under it . - In the component use an action to trigger the POST request. - Monitor that action with a submission. - If that submission throws an Error, pass it on to the toast provider inside the component.
It could help when accessing protected routes
How would this even happen (during a POST request)? Are you redirecting to routes that a visitor doesn't have access to? Protected routes are loaded via GET or because of client side navigation. The navigation could cause an authorisation check but that would be typically handled over a query which uses GET.
action - SolidDocs
Documentation for SolidJS, the signals-powered UI framework
query - SolidDocs
Documentation for SolidJS, the signals-powered UI framework
clidegamer254
clidegamer254OP•4w ago
How about a success notification in redirect or rather a protected route that does not depend on any actions how would flash notifications be handled for this case @peerreynders
peerreynders
peerreynders•4w ago
How about a success post
Monitor for thesubmission result provided the action returns something or when the submission is no longer pending and there is no error (you may have to clear results and errors if you issue more than one submission between navigations).
rather a protected route
A route authentication check typically looks something like this:
const userId = await getAuthUser();
if (!userId) throw redirect("/login");
const userId = await getAuthUser();
if (!userId) throw redirect("/login");
Conceivably you could do an authorisation check in a similar manner (while the actual back end operations still have their own, independent guards). In the worst case you could just put the encoded error text into the (error) redirect URL as an error-message searchParam so that the page can just grab it from there. The other possibility is to have the toast provider in the Router root (where it exists for the entire client session) and have the toast provider subscribe to toast events from an external JS module. This example uses the same approach to make navigate available outside of the component tree.
MDN Web Docs
encodeURIComponent() - JavaScript | MDN
The encodeURIComponent() function encodes a URI by replacing each instance of certain characters by one, two, three, or four escape sequences representing the UTF-8 encoding of the character (will only be four escape sequences for characters composed of two surrogate characters). Compared to encodeURI(), this function encodes more characters, in...
peerreynders
StackBlitz
solid-router external navigation - StackBlitz
external.ts holds a link object that flipper.ts sends NavigationEvents to which are forwarded to the root component in app.tsx
GitHub
strello/src/lib/index.ts at 9c9ae973d96cc045914e696757a1b5f31efc6fa...
Contribute to solidjs-community/strello development by creating an account on GitHub.
clidegamer254
clidegamer254OP•4w ago
Another way would possibly be cookies. But they'd need to expire the instant they're subscribed to I'll have a look at these implementations many thanks for the input 😉
peerreynders
peerreynders•4w ago
Another way would possibly be cookies. But they'd need to expire the instant they're subscribed to
Your authentication cookie should be httpOnly so you would need a separate cookie that is client modifiable. Reading the cookie should invalidate it (an only a valid cookie should be able to be read). Also manipulating cookies server side can be at times tricky if the server decides to eagerly stream a response.
clidegamer254
clidegamer254OP•4w ago
Will implement and give feedback on approached :p @peerreynders I was able to get something working using this flash utility:
import { query } from "@solidjs/router";
import { getRequestEvent } from "solid-js/web";
import { toast } from "solid-sonner";
import { getCookie, HTTPEvent } from "vinxi/http";

export const setFlashCookieHeader = (
message: string,
type: string,
age: string = "5",
): ResponseInit["headers"] => {
const headers = new Headers({
"Set-Cookie": `flash=${message}_${type}; Max-Age=${age}; HttpOnly`,
});
return headers;
};
/**
* Get flash message from cookie
*/
export const getStatus = query(async () => {
"use server";
const fetchEvent = getRequestEvent();
const event = fetchEvent?.nativeEvent as HTTPEvent;
const flash = getCookie(event, "flash");
// console.log(flash)
return flash;
}, "flash");

/**
* Set the flash using a toast
* @param flash
* @returns
*/
export const setFlash = (flash: string | undefined) => {
if (flash) {
const message = flash?.split("_")[0];
const type = flash?.split("_")[1];
let timer;
switch (true) {
case type === "success":
timer = setTimeout(() => toast.success(message), 100);
break;
case type == "error":
timer = setTimeout(() => toast.error(message), 100);
break;
case type == "info":
timer = setTimeout(() => toast.info(message), 100);
break;
default:
break;
}
return timer;
}
};
import { query } from "@solidjs/router";
import { getRequestEvent } from "solid-js/web";
import { toast } from "solid-sonner";
import { getCookie, HTTPEvent } from "vinxi/http";

export const setFlashCookieHeader = (
message: string,
type: string,
age: string = "5",
): ResponseInit["headers"] => {
const headers = new Headers({
"Set-Cookie": `flash=${message}_${type}; Max-Age=${age}; HttpOnly`,
});
return headers;
};
/**
* Get flash message from cookie
*/
export const getStatus = query(async () => {
"use server";
const fetchEvent = getRequestEvent();
const event = fetchEvent?.nativeEvent as HTTPEvent;
const flash = getCookie(event, "flash");
// console.log(flash)
return flash;
}, "flash");

/**
* Set the flash using a toast
* @param flash
* @returns
*/
export const setFlash = (flash: string | undefined) => {
if (flash) {
const message = flash?.split("_")[0];
const type = flash?.split("_")[1];
let timer;
switch (true) {
case type === "success":
timer = setTimeout(() => toast.success(message), 100);
break;
case type == "error":
timer = setTimeout(() => toast.error(message), 100);
break;
case type == "info":
timer = setTimeout(() => toast.info(message), 100);
break;
default:
break;
}
return timer;
}
};
and it's consumed like so in an action:
throw redirect("/backend", {
headers: setFlashCookieHeader("Logged in", "success"),
});
throw redirect("/backend", {
headers: setFlashCookieHeader("Logged in", "success"),
});
This works on actions login/register/logout, however, it does not work when one manually navigates to a protected route, as the cookie is set on the client, but it doesn't show on the page. Somehow, the middleware can pick it up, but I don't think there's a way to stream it back on the client.
peerreynders
peerreynders•4w ago
These POST requests you were initially talking about, are they client side fetches or "use server" RPC? It seems weird to use a server request to obtain the value of a cookie that was set for the benefit of the client by the server. In the case of authentication that extra step is justified because the session cookie is maintained only for the benefit of the (amnesiac) server; it's "httpOnly" so that rogue client side JS scripts can't get a hold of the cookie and re-purpose it for whatever nefarious means. While Set-Cookie headers aren't accessible on the client side Response, cookies surface via document.cookie. That should work in your favour as you should be able to Set-Cookie during a "use server" RPC while being able to access it via document.cookie. A vanilla-ish example:
// file: index.ts
// "serve": "listhen ./index.ts",
import { createApp, createRouter, defineEventHandler, setCookie } from 'h3';

const app = createApp();
const router = createRouter();

router.post(
'/api',
defineEventHandler((event) => {
const epochMs = Date.now();
setCookie(event, 'flash', String(epochMs), { maxAge: 5 });
return { isoTime: new Date(epochMs).toISOString() };
})
);

app.use(router);

export { app };
// file: index.ts
// "serve": "listhen ./index.ts",
import { createApp, createRouter, defineEventHandler, setCookie } from 'h3';

const app = createApp();
const router = createRouter();

router.post(
'/api',
defineEventHandler((event) => {
const epochMs = Date.now();
setCookie(event, 'flash', String(epochMs), { maxAge: 5 });
return { isoTime: new Date(epochMs).toISOString() };
})
);

app.use(router);

export { app };
// file: src/client/entry.ts
import { parse, serialize } from 'cookie-es';

const INVALIDATE_OPTION = { expires: new Date(0) };

const invalidateCookieUpdate = (record: Record<string, string>, key: string) =>
key in record ? serialize(key, record[key], INVALIDATE_OPTION) : '';

async function fetchTime(
setTime: (isoTime: string) => void,
setFlash: (payload: string) => void
) {
const response = await fetch('/api', { method: 'POST' });
if (response.ok) {
const result = (await response.json()) as unknown;
if (
result &&
typeof result === 'object' &&
'isoTime' in result &&
typeof result.isoTime === 'string'
) {
setTime(result.isoTime);
}
}

const cookies = parse(document.cookie);
const flash = cookies['flash'];
if (flash) {
document.cookie = invalidateCookieUpdate(cookies, 'flash');
setFlash(flash);
}
}

const button = document.querySelector<HTMLButtonElement>('button');
if (!(button instanceof HTMLButtonElement)) {
throw new Error('No button found');
}

const nodes = document.querySelectorAll<HTMLTableCellElement>('td');
if (nodes.length < 2) {
throw new Error('Output nodes not found');
}

const setTime = (isoTime: string) => {
nodes[0].textContent = isoTime;
};

const setFlash = (payload: string) => {
nodes[1].textContent = new Date(Number(payload)).toISOString();
};

button.addEventListener('click', (e) => {
const button = e.currentTarget;
if (!(button instanceof HTMLButtonElement)) return;

button.disabled = true;
fetchTime(setTime, setFlash).finally(() => (button.disabled = false));
});
// file: src/client/entry.ts
import { parse, serialize } from 'cookie-es';

const INVALIDATE_OPTION = { expires: new Date(0) };

const invalidateCookieUpdate = (record: Record<string, string>, key: string) =>
key in record ? serialize(key, record[key], INVALIDATE_OPTION) : '';

async function fetchTime(
setTime: (isoTime: string) => void,
setFlash: (payload: string) => void
) {
const response = await fetch('/api', { method: 'POST' });
if (response.ok) {
const result = (await response.json()) as unknown;
if (
result &&
typeof result === 'object' &&
'isoTime' in result &&
typeof result.isoTime === 'string'
) {
setTime(result.isoTime);
}
}

const cookies = parse(document.cookie);
const flash = cookies['flash'];
if (flash) {
document.cookie = invalidateCookieUpdate(cookies, 'flash');
setFlash(flash);
}
}

const button = document.querySelector<HTMLButtonElement>('button');
if (!(button instanceof HTMLButtonElement)) {
throw new Error('No button found');
}

const nodes = document.querySelectorAll<HTMLTableCellElement>('td');
if (nodes.length < 2) {
throw new Error('Output nodes not found');
}

const setTime = (isoTime: string) => {
nodes[0].textContent = isoTime;
};

const setFlash = (payload: string) => {
nodes[1].textContent = new Date(Number(payload)).toISOString();
};

button.addEventListener('click', (e) => {
const button = e.currentTarget;
if (!(button instanceof HTMLButtonElement)) return;

button.disabled = true;
fetchTime(setTime, setFlash).finally(() => (button.disabled = false));
});

Did you find this page helpful?