| Hook | Purpose | Example | Short Explanation |
|---|---|---|---|
| useState | Store and manage component data |
const [products, setProducts] = useState([]);
|
Used to store values that can change, such as form data or API response. |
| useEffect | Run code on component load or update |
useEffect(() => { loadProducts(); }, []);
|
Executes code when the component loads or when dependencies change. |
| useRef | Access or control elements without re-render |
const loadingRef = useRef(null);
|
Used to control the loading bar without triggering component re-render. |
| useLocation | Detect route (URL) changes |
const location = useLocation();
|
Used to trigger actions like loading bar when navigation changes. |
In this practical, we integrate a React frontend with an ASP.NET Core Web API to perform complete CRUD (Create, Read, Update, Delete) operations.
React runs on http://localhost:5173 and the API runs on a different port.
Due to browser security rules, the API must explicitly allow requests from React.
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowReactApp",
builder =>
{
builder
.WithOrigins("http://localhost:5173")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
app.UseRouting();
app.UseCors("AllowReactApp");
src/
├── components/
│ ├── Products.jsx
│ ├── ProductForm.jsx
├── services/
│ └── productService.js
├── App.jsx
The productService file contains all API calls. This keeps React components clean and reusable.
const API_URL = "https://localhost:7079/api/ProductDB";
export const getProducts = async () => {
const response = await fetch(API_URL);
return await response.json();
};
export const addProduct = async (product) => {
await fetch(API_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(product)
});
};
export const updateProduct = async (id, product) => {
await fetch(`${API_URL}/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(product)
});
};
export const deleteProduct = async (id) => {
await fetch(`${API_URL}/${id}`, {
method: "DELETE"
});
};
"Content-Type": "application/json" tells the server the data format, and JSON.stringify() converts JavaScript objects into JSON before sending them to the API.import { useEffect, useState } from "react";
import { getProducts, deleteProduct } from "../services/productService";
import { toast } from "react-toastify";
function Products({ onEdit }) {
const [products, setProducts] = useState([]);
useEffect(() => {
loadProducts();
}, []);
const loadProducts = async () => {
const data = await getProducts();
setProducts(data);
};
const handleDelete = async (id) => {
if (!window.confirm("Delete product?")) return;
await deleteProduct(id);
toast.success("Product deleted");
loadProducts();
};
return (
<table className="table table-bordered">
<tbody>
{products.map(p => (
<tr key={p.id}>
<td>{p.name}</td>
<td>{p.price}</td>
<td>
<button onClick={() => onEdit(p)}>Edit</button>
<button onClick={() => handleDelete(p.id)}>Delete</button>
</td>
</tr>
))}
</tbody>
</table>
);
}
export default Products;
onEdit ProponEdit(product) is called, the selected product is sent back to the parent component.
onEdit follows proper parent–child communication in React.
import { useEffect, useState } from "react";
import { addProduct, updateProduct } from "../services/productService";
import { toast } from "react-toastify";
function ProductForm({ selectedProduct, onSaved }) {
const [product, setProduct] = useState({ name: "", price: "" });
useEffect(() => {
if (selectedProduct) {
setProduct(selectedProduct);
}
}, [selectedProduct]);
const handleChange = (e) => {
setProduct({
...product,
[e.target.name]: e.target.value
});
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!product.name || !product.price) {
toast.error("All fields required");
return;
}
if (product.id) {
await updateProduct(product.id, product);
toast.success("Product updated");
} else {
await addProduct(product);
toast.success("Product added");
}
onSaved();
setProduct({ name: "", price: "" });
};
return (
<form onSubmit={handleSubmit}>
<input
name="name"
value={product.name}
onChange={handleChange}
placeholder="Product Name"
/>
<input
name="price"
value={product.price}
onChange={handleChange}
placeholder="Price"
/>
<button type="submit">Save</button>
</form>
);
}
export default ProductForm;
setProduct().
onChange, inputs with value become read-only.
handleChange function updates the correct field using the input name.
...) copies existing object properties.
product.id.
onSaved ProponSaved does internally.
onSaved() informs the parent that the save operation is complete.
const [selectedProduct, setSelectedProduct] = useState(null);
const [refreshKey, setRefreshKey] = useState(0);
<ProductForm
selectedProduct={selectedProduct}
onSaved={() => {
setSelectedProduct(null);
setRefreshKey(prev => prev + 1);
}}
/>
<Products
key={refreshKey}
onEdit={(p) => setSelectedProduct(p)}
/>
refreshKey after save or update.
refreshKey on the Products component.