react app - creating a container and renders outputs from a rest API call
This is a simple component that serves as a container to load data and renders its output using another component called ProductItem. Here is an example of the container code. It uses react useEffect to call an API endpoint.
import { ProductItem } from "./ProductItem";
import { useEffect, useState } from "react";
import { Item } from "./Item";
import { ProductFetchEndpoint } from "../AppConstant";
import { CalculateTotal } from "./CalculateTotal";
const ProductContainer = () => {
const [items, setStoreItems] = useState<Item[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchProducts = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(ProductFetchEndpoint);
if (!response.ok) {
throw new Error("Failed to fetch products");
}
const data: Item[] = await response.json();
setStoreItems(data);
} catch (err) {
setError((err as Error).message);
} finally {
setLoading(false);
}
};
fetchProducts();
}, []);
if (loading) {
return <p>Loading products...</p>;
}
if (error) {
return <p>Error: {error}</p>;
}
return (
<div>
{items.map((item) => (
<ProductItem key={item.id} item={item} />
))}
<div>
<CalculateTotal items={items}/>
</div>
</div>
);
};
export default ProductContainer;
For the container tests, the fetch is mocked out.
import { render, screen, waitFor } from "@testing-library/react";
import ProductContainer from "./ProductContainer";
import { BuyOneFreeOneSpecialOffer, BulkBuySpecialOffer } from "../AppConstant";
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () =>
Promise.resolve(storeItems),
})
) as jest.Mock;
const storeItems = [
{
id: 1,
name: "Apple",
unitPrice: 2.0,
special: {
type: BulkBuySpecialOffer,
quantity: 3,
price: 5.0,
},
},
{
id: 2,
name: "Orange",
unitPrice: 1.5,
special: {
type: BuyOneFreeOneSpecialOffer,
quantity: 2,
freeQuantity: 1,
},
},
{
id: 3,
name: "Banana",
unitPrice: 1.0,
},
];
describe("Product Container loading stores items correctly", () => {
it("product prices loads correctly from the API endpoint", () => {
render(<ProductContainer />);
expect(screen.getByText("Loading products...")).toBeInTheDocument();
waitFor(() => {
expect(screen.getByText("Apple")).toBeInTheDocument();
expect(screen.getByText("Buy 3 for $5.00")).toBeInTheDocument();
expect(screen.getByText("Orange")).toBeInTheDocument();
expect(screen.getByText("Special: Buy 2 get 1 free")).toBeInTheDocument();
expect(screen.getByText("Banana")).toBeInTheDocument();
});
})
})
As for the ProductItem.tsx, this is what it looks like
import React, { useState } from "react";
import { ProductItemProps } from "./Item"
import { BuyOneFreeOneSpecialOffer, BulkBuySpecialOffer } from "../AppConstant";
export const ProductItem: React.FC<ProductItemProps> = ({ item }) => {
const [quantity, setQuantity] = useState(1);
const calculatePrice = (): number => {
const { unitPrice, special } = item;
if (!special) {
return unitPrice * quantity;
}
if (special.type === BulkBuySpecialOffer && special.price) {
const bulkSets = Math.floor(quantity / special.quantity);
const remainingItems = quantity % special.quantity;
return bulkSets * special.price + remainingItems * unitPrice;
}
if (special.type === BuyOneFreeOneSpecialOffer && special.freeQuantity) {
const paidItems = Math.ceil(quantity / (special.quantity + special.freeQuantity)) * special.quantity;
return paidItems * unitPrice;
}
return unitPrice * quantity; // return unit price as default
};
return (
<div style={{ border: "1px solid #ccc", padding: "10px", margin: "10px" }}>
<h3>{item.name}</h3>
<p>Unit Price: ${item.unitPrice.toFixed(2)}</p>
{item.special && (
<p>
Special:{" "}
{item.special.type === BulkBuySpecialOffer && item.special.price
? `Buy ${item.special.quantity} for $${item.special.price.toFixed(2)}`
: item.special.type === BuyOneFreeOneSpecialOffer && item.special.freeQuantity
? `Buy ${item.special.quantity} get ${item.special.freeQuantity} free`
: ""}
</p>
)}
<input
type="number"
min="1"
value={quantity}
className="storeInput"
onChange={(e) => setQuantity(Number(e.target.value))}
style={{ width: "50px", marginRight: "10px" }}
/>
<p>Total Price: ${calculatePrice().toFixed(2)}</p>
</div>
);
};
Then we have some tests created for it. In our test, we made some updates by calling fireEvent to change the UI behaviour.
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { ProductItem } from "./ProductItem";
import {act} from 'react';
const bulkSpecialProductMockData = {
id: 1,
name: "Apple",
unitPrice: 2.0,
special: {
type: "bulk",
quantity: 3,
price: 5.0,
},
};
const unitSpecialProductMockData = {
id: 1,
name: "Orange",
unitPrice: 2.0,
};
describe("Product Item loading special offers", () => {
it("loads bulk offers correctly", () => {
render(<ProductItem item={bulkSpecialProductMockData}/>);
expect(screen.getByText("Apple")).toBeInTheDocument();
expect(screen.getByText("Special: Buy 3 for $5.00")).toBeInTheDocument();
})
it("loads unit price offers correctly", () => {
render(<ProductItem item={unitSpecialProductMockData}/>);
expect(screen.getByText("Orange")).toBeInTheDocument();
waitFor(() => {
expect(screen.getByText("Unit Price")).toBeInTheDocument();
expect(screen.getByText("Special")).not.toBeInTheDocument();
});
})
it("ensure unit price total are correctly calculated", () => {
const { debug } = render(<ProductItem item={unitSpecialProductMockData}/>);
const input = screen.getByRole("spinbutton");
fireEvent.change(input, { target: { value: "5"}})
expect(screen.getByText("Orange")).toBeInTheDocument();
waitFor(() => {
expect(screen.getByText("10.00")).toBeInTheDocument();
});
})
})
Comments