Introduction
Many of the things that run in applications need to pass important information as a form of feedback to the user, so that they know the processing status. This can be in the form of information about updates, reminders, alerts, among other features.
And this is where the use of Toast notifications is important, because they are not an intrusive element on the screen and provide immediate feedback for a certain period of time, allowing the user to continue with their tasks.
This brings us to today's article, in which we are going to create an application where we insert data into a database and the expectation is that a notification will be shown when the promise is resolved or rejected.
What will be covered
- Drizzle ORM configuration
- Definition of the table schema
- Validation of forms
- Implementation of toast management on the server and client side
Prerequisites
It is expected that you have prior knowledge of building applications using Remix.js and using ORM's. Another thing worth mentioning is that the Tailwind CSS framework will be used, however step-by-step instructions on how to configure it will not be shown, so I recommend following this guide.
Getting started
Let's start by generating the base template:
npx create-remix@latest toastmix
Next, we will install the necessary dependencies:
npm install drizzle-orm better-sqlite3
npm install --dev drizzle-kit @types/better-sqlite3
With the dependencies installed, we can move on to drizzle configuration so that the paths to the database schema can be defined and in which path the migrations should be generated.
// drizzle.config.ts
import type { Config } from "drizzle-kit";
export default {
schema: "./app/db/schema.ts",
out: "./migrations",
driver: "better-sqlite",
dbCredentials: {
url: "./local.db",
},
verbose: true,
strict: true,
} satisfies Config;
The next step is related to defining the schema of the database tables and as you may have already noticed, we will use SQLite in this project and for this we will use the Drizzle primitives targeted at that dialect.
// @/app/db/schema.ts
import { sqliteTable, integer, text } from "drizzle-orm/sqlite-core";
export const users = sqliteTable("users", {
id: integer("id").primaryKey({ autoIncrement: true }),
username: text("username").unique().notNull(),
});
In the code snippet above we defined that the users
table has two columns called id
and username
, ensuring that the value of the latter must be unique. Next we can move on to creating the client that will be used to interact with the database.
// @/app/db/index.ts
import {
drizzle,
type BetterSQLite3Database,
} from "drizzle-orm/better-sqlite3";
import Database from "better-sqlite3";
import * as schema from "./schema";
const sqlite = new Database("local.db");
export const db: BetterSQLite3Database<typeof schema> = drizzle(sqlite, {
schema,
});
With this we can close the topics related to the database and we can now redirect our efforts to dealing with the application forms. First, we will install the following dependencies:
npm install remix-validated-form zod @remix-validated-form/with-zod
The dependencies that remain to be installed are related to the implementation of the toast/notification management strategy in the application. We will need to define them on the client and server side.
npm install remix-toast react-hot-toast
In this article we will use the react-hot-toast
package on the client side but it is worth highlighting that we can use any other library on the client side because the solution is agnostic. Therefore, you can use the package that you are most comfortable using.
Now that we have the dependencies we need, we can now move on to creating the components that will be reused in the application. Starting first with <Input />
:
// @/app/components/Input.tsx
import type { FC } from "react";
import { useField } from "remix-validated-form";
interface InputProps {
name: string;
label: string;
}
export const Input: FC<InputProps> = ({ name, label }) => {
const { error, getInputProps } = useField(name);
return (
<div className="h-20">
<label
className="relative block rounded-md border border-gray-200 shadow-sm focus-within:border-blue-600 focus-within:ring-1 focus-within:ring-blue-600"
htmlFor={name}
>
<input
className="peer border-none bg-transparent placeholder-transparent focus:border-transparent focus:outline-none focus:ring-0"
{...getInputProps({ id: name })}
/>
<span className="pointer-events-none absolute start-2.5 top-0 -translate-y-1/2 bg-white p-0.5 text-xs text-gray-700 transition-all peer-placeholder-shown:top-1/2 peer-placeholder-shown:text-sm peer-focus:top-0 peer-focus:text-xs">
{label}
</span>
</label>
{error && <span className="text-red-500 text-xs ml-1">{error}</span>}
</div>
);
};
Another component that needs to be created is related to the form submission event, we can now work on the <SubmitButton />
:
// @/app/components/SubmitButton.tsx
import type { ButtonHTMLAttributes, FC } from "react";
import { useIsSubmitting } from "remix-validated-form";
export const SubmitButton: FC<ButtonHTMLAttributes<HTMLButtonElement>> = (
props
) => {
const isSubmitting = useIsSubmitting();
return (
<button
{...props}
type="submit"
disabled={isSubmitting}
className="inline-block rounded border border-indigo-600 bg-indigo-600 w-32 h-10 text-sm font-medium text-white hover:bg-transparent hover:text-indigo-600 focus:outline-none focus:ring active:text-indigo-500"
>
{isSubmitting ? "Submitting..." : "Submit"}
</button>
);
};
With this we can close the topic of creating reusable components and we can move on to creating the application page in today's example. Starting by importing the necessary dependencies, along with the components and clients that we created in this article:
// @/app/routes/_index.tsx
import {
json,
type DataFunctionArgs,
type LoaderFunctionArgs,
type MetaFunction,
} from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { withZod } from "@remix-validated-form/with-zod";
import { useEffect } from "react";
import notify, { Toaster } from "react-hot-toast";
import { getToast, redirectWithError, redirectWithSuccess } from "remix-toast";
import { ValidatedForm, validationError } from "remix-validated-form";
import { z } from "zod";
import { Input } from "~/components/Input";
import { SubmitButton } from "~/components/SubmitButton";
import { db } from "~/db";
import { users } from "~/db/schema";
export const meta: MetaFunction = () => {
return [
{ title: "List" },
{ name: "description", content: "List page content" },
];
};
// ...
Next, we will create the validator that will be used to check the integrity of the form data, which will be used on the client and backend side. Another thing we will define is the loader
, in which we will obtain the toast notification if this HTTP request is present and we will also obtain the complete list of users
present in the database.
// @/app/routes/_index.tsx
// ...
export const validator = withZod(
z.object({
username: z.string().min(1, { message: "Required" }),
})
);
export const loader = async ({ request }: LoaderFunctionArgs) => {
const { toast, headers } = await getToast(request);
const datums = await db.query.users.findMany();
return json({ toast, datums }, { headers });
};
// ...
The next step involves defining the action
of the route, in which we will validate the form data and proceed to insert it into the database. If the promise is resolved successfully, we will use the primitive redirectWithSuccess()
which takes two arguments, the route to which we want to redirect and what message should be sent in the toast notification. However, if the promise is rejected we will use the primitive redirectWithError()
which also receives two arguments similar to the previous primitive.
// @/app/routes/_index.tsx
// ...
export const action = async ({ request }: DataFunctionArgs) => {
try {
const result = await validator.validate(await request.formData());
if (result.error) return validationError(result.error);
await db.insert(users).values({ username: result.data.username });
return redirectWithSuccess("/", "Successful operation");
} catch {
return redirectWithError("/", "An error occurred");
}
};
// ...
Last but not least, in the page component we will take advantage of the useLoaderData
hook to obtain the toast and the database data that is returned from the loader
. And we will use a useEffect
to issue the toast notification if it is present and depending on its type.
// @/app/routes/_index.tsx
// ...
export default function Index() {
const { toast, datums } = useLoaderData<typeof loader>();
useEffect(() => {
switch (toast?.type) {
case "success":
notify.success(toast.message);
return;
case "error":
notify.error(toast.message);
return;
default:
return;
}
}, [toast]);
return (
<div className="h-screen w-full flex justify-center items-center">
<Toaster />
<span class="o"><</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">w-2/4 space-y-4</span><span class="dl">"</span><span class="o">></span>
<span class="o"><</span><span class="nx">ValidatedForm</span> <span class="nx">validator</span><span class="o">=</span><span class="p">{</span><span class="nx">validator</span><span class="p">}</span> <span class="nx">method</span><span class="o">=</span><span class="dl">"</span><span class="s2">post</span><span class="dl">"</span><span class="o">></span>
<span class="o"><</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">flex space-x-4</span><span class="dl">"</span><span class="o">></span>
<span class="o"><</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">w-full</span><span class="dl">"</span><span class="o">></span>
<span class="o"><</span><span class="nx">Input</span> <span class="nx">name</span><span class="o">=</span><span class="dl">"</span><span class="s2">username</span><span class="dl">"</span> <span class="nx">label</span><span class="o">=</span><span class="dl">"</span><span class="s2">Username</span><span class="dl">"</span> <span class="o">/></span>
<span class="o"><</span><span class="sr">/div</span><span class="err">>
<SubmitButton />
</div>
</ValidatedForm>
<div className="overflow-x-auto rounded-lg border border-gray-200">
<table className="min-w-full divide-y-2 divide-gray-200 bg-white text-sm">
<thead className="text-left">
<tr>
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900">
Name
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{datums.map((datum) => (
<tr key={datum.id}>
<td className="whitespace-nowrap px-4 py-2 font-medium text-gray-900">
{datum.username}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}
Conclusion
I hope you found this article helpful, whether you're using the information in an existing project or just giving it a try for fun.
Please let me know if you notice any mistakes in the article by leaving a comment. And, if you'd like to see the source code for this article, you can find it on the github repository linked below.
Top comments (0)