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;
Top comments (0)