Building CRUD App with react-form, zod, react data grid, react-query and json-server
Mohamed Hammi
Posted on November 12, 2024
Goal :
Our goal is to develop a react CRUD application.
Our stack :
- react-form
- zod
- ag-grid-react
- react-query
- json-server
Setup environment :
Create a react project using vite :
npm create vite@latest crud-react -- --template react-ts
Install dependencies :
npm install react-hook-form zod @hookform/resolvers ag-grid-react react-query axios
Create and start Server :
Object (product) structure :
{
"id": "w38y",
"name": "Vitamin C Tablets",
"price": 19.99,
"expiryDate": "2025-01-01",
"emailSupplier": "contact@healthplus.com"
}
Create a file that contain sample data in /db/db.json
:
{
"products": [
{
"id": "w38y",
"name": "Vitamin C Tablets",
"price": 19.99,
"expiryDate": "2025-01-01",
"emailSupplier": "contact@healthplus.com"
},
{
"id": "a99x",
"name": "Omega-3 Fish Oil",
"price": 30.99,
"expiryDate": "2024-11-15",
"emailSupplier": "support@nutricore.com"
},
{
"id": "x82j",
"name": "Calcium + Vitamin D",
"price": 15.5,
"expiryDate": "2026-06-01",
"emailSupplier": "orders@welllifelabs.com"
},
{
"id": "a40i",
"name": "Zinc Lozenges",
"price": 12.99,
"expiryDate": "2024-09-30",
"emailSupplier": "sales@herbalessentials.com"
},
{
"id": "c52f",
"name": "Probiotic Capsules",
"price": 25.75,
"expiryDate": "2025-03-20",
"emailSupplier": "info@guthealthlabs.com"
}
]
}
Start json-server :
npx json-server db/db.json
Setup react query :
Update /src/App.tsx
:
import { QueryClient, QueryClientProvider } from "react-query";
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
</QueryClientProvider>
);
}
export default App;
Create /src/types.ts
:
export type FormData = {
name: string;
price: number;
expiryDate: string;
emailSupplier: string;
};
export type FormFieldNames = "name" | "price" | "expiryDate" | "emailSupplier";
export type Product = {
id: string;
name: string;
price: number;
expiryDate: string;
emailSupplier: string;
};
Create /src/server/productQuery.ts
:
import { useMutation, useQuery, useQueryClient } from "react-query";
import { FormData, Product } from "../types";
import axios from "axios";
const URL = "http://localhost:3000";
const PRODUCTS = "products";
export const save = async (product: FormData) =>
axios.post(`${URL}/${PRODUCTS}`, product);
export const useSave = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (newProduct: FormData) => save(newProduct),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [PRODUCTS] });
},
});
};
export const fetch = async () => {
const result = await axios.get(`${URL}/${PRODUCTS}`);
return result.data;
};
export const useProducts = () =>
useQuery<Product[]>({
queryKey: [PRODUCTS],
queryFn: fetch,
});
const remove = async (id: string) => {
await axios.delete(`${URL}/${PRODUCTS}/${id}`);
};
export const useRemove = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => remove(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [PRODUCTS] });
},
});
};
export const update = async (product: Product) =>
axios.put(`${URL}/${PRODUCTS}/${product.id}`, product);
export const useUpdate = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (product: Product) => update(product),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [PRODUCTS] });
},
});
};
Create Form :
Update /src/types.ts
:
import { z, ZodType } from "zod";
export type FormData = {
name: string;
price: number;
expiryDate: string;
emailSupplier: string;
};
export type FormFieldNames = "name" | "price" | "expiryDate" | "emailSupplier";
export const ProductSchema: ZodType<FormData> = z.object({
name: z.string().min(3),
price: z.number().min(1).max(1000),
expiryDate: z
.string()
.refine(
(date) => new Date(date) > new Date(),
"Expiry Date must be superior than current date",
),
emailSupplier: z.string().email(),
});
export type Product = {
id: string;
name: string;
price: number;
expiryDate: string;
emailSupplier: string;
};
Create /src/components/form/FormField.tsx
:
import { FieldError, UseFormRegister } from "react-hook-form";
import { FormData, FormFieldNames } from "../../types";
type FormFieldProps = {
type: string;
placeholder: string;
name: FormFieldNames;
register: UseFormRegister<FormData>;
error: FieldError | undefined;
valueAsNumber?: boolean;
step?: number | string;
};
const FormField = ({
type,
placeholder,
name,
register,
error,
valueAsNumber,
step,
}: FormFieldProps) => (
<>
<input
type={type}
placeholder={placeholder}
step={step}
{...register(name, { valueAsNumber })}
/>
{error && <span> {error.message} </span>}
</>
);
export default FormField;
Create /src/components/form/Form.tsx
:
import { useForm } from "react-hook-form";
import FormField from "./FormField";
import { FormData, ProductSchema } from "../../types";
import { zodResolver } from "@hookform/resolvers/zod";
import { useSave } from "../../server/productQuery";
const Form = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormData>({
resolver: zodResolver(ProductSchema),
});
const mutation = useSave();
const onSubmit = (data: FormData) => {
mutation.mutate(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<FormField
type="text"
placeholder="Name"
name="name"
register={register}
error={errors.name}
/>
<FormField
type="number"
placeholder="Price"
name="price"
step="0.01"
register={register}
error={errors.price}
valueAsNumber
/>
<FormField
type="date"
placeholder="Expiry Date"
name="expiryDate"
register={register}
error={errors.expiryDate}
/>
<FormField
type="email"
placeholder="Email"
name="emailSupplier"
register={register}
error={errors.emailSupplier}
/>
<button type="submit">Add</button>
</div>
</form>
);
};
export default Form;
Create Table :
Create /src/components/table/Products.tsx
:
import { useMemo } from "react";
import { Product } from "../../types";
import { useProducts, useRemove, useUpdate } from "../../server/productQuery";
import { AgGridReact } from "ag-grid-react";
import { ColDef, ColGroupDef } from "ag-grid-community";
import "ag-grid-community/styles/ag-grid.css";
import "ag-grid-community/styles/ag-theme-quartz.css";
const Products = () => {
const { data: products } = useProducts();
const removeMutation = useRemove();
const updateMutation = useUpdate();
const columns = useMemo<(ColDef | ColGroupDef<Product>)[]>(
() => [
{ field: "id", editable: false },
{ field: "name", editable: true },
{ field: "price", editable: true },
{ field: "expiryDate", editable: true },
{ field: "emailSupplier", editable: true },
{
field: "delete",
sortable: false,
editable: false,
cellRenderer: (params: { data: Product }) => (
<button onClick={() => removeMutation.mutate(params.data.id)}>
Delete
</button>
),
},
],
[],
);
return (
<div className="ag-theme-quartz" style={{ height: 500 }}>
<AgGridReact
rowData={products}
columnDefs={columns}
onCellValueChanged={(params) => {
updateMutation.mutate(params.data);
}}
/>
</div>
);
};
export default Products;
Update /src/App.tsx
:
import { QueryClient, QueryClientProvider } from "react-query";
import Form from "./components/form/Form";
import Products from "./components/table/Products";
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<Form />
<Products />
</QueryClientProvider>
);
}
export default App;
Git repository :
Thanks for following along!
💖 💪 🙅 🚩
Mohamed Hammi
Posted on November 12, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
webdev Building CRUD App with react-form, zod, react data grid, react-query and json-server
November 12, 2024