Components: Product List – (Redux Toolkit & Ant Design )

admin . 2:59 am
I’ll help you create a demo for ListProduct with the steps you requested. Let me start by examining the current structure and then implement the changes.
Read file: package.json
Great! I can see that Redux Toolkit and Ant Design are already installed. Now let me check if there’s a Redux store setup and then implement the required functionality.
Ran tool
Read file: src/index.tsx
Now I’ll implement the complete solution step by step:
Step 1: Create Redux store and slice for products
import { configureStore } from '@reduxjs/toolkit';
import productsReducer from './slices/productsSlice';
export const store = configureStore({
reducer: {
products: productsReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
export interface Product {
id: number;
title: string;
price: number;
description: string;
category: string;
image: string;
rating: {
rate: number;
count: number;
};
}
interface ProductsState {
products: Product[];
loading: boolean;
error: string | null;
filteredProducts: Product[];
filters: {
category: string;
minPrice: number;
maxPrice: number;
};
}
const initialState: ProductsState = {
products: [],
loading: false,
error: null,
filteredProducts: [],
filters: {
category: '',
minPrice: 0,
maxPrice: 1000,
},
};
export const fetchProducts = createAsyncThunk(
'products/fetchProducts',
async () => {
const response = await fetch('https://fakestoreapi.com/products');
const data = await response.json();
return data as Product[];
}
);
const productsSlice = createSlice({
name: 'products',
initialState,
reducers: {
setCategoryFilter: (state, action: PayloadAction<string>) => {
state.filters.category = action.payload;
state.filteredProducts = state.products.filter(product => {
const categoryMatch = !state.filters.category ||
product.category.toLowerCase().includes(state.filters.category.toLowerCase());
const priceMatch = product.price >= state.filters.minPrice &&
product.price <= state.filters.maxPrice;
return categoryMatch && priceMatch;
});
},
setPriceFilter: (state, action: PayloadAction<{ min: number; max: number }>) => {
state.filters.minPrice = action.payload.min;
state.filters.maxPrice = action.payload.max;
state.filteredProducts = state.products.filter(product => {
const categoryMatch = !state.filters.category ||
product.category.toLowerCase().includes(state.filters.category.toLowerCase());
const priceMatch = product.price >= state.filters.minPrice &&
product.price <= state.filters.maxPrice;
return categoryMatch && priceMatch;
});
},
clearFilters: (state) => {
state.filters = {
category: '',
minPrice: 0,
maxPrice: 1000,
};
state.filteredProducts = state.products;
},
},
extraReducers: (builder) => {
builder
.addCase(fetchProducts.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchProducts.fulfilled, (state, action) => {
state.loading = false;
state.products = action.payload;
state.filteredProducts = action.payload;
})
.addCase(fetchProducts.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message || 'Failed to fetch products';
});
},
});
export const { setCategoryFilter, setPriceFilter, clearFilters } = productsSlice.actions;
export default productsSlice.reducer;
Step 2: Create Redux hooks for TypeScript
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
import type { RootState, AppDispatch } from './index';
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
I need to install react-redux first. Let me check if it’s already installed and then install it if needed.
cd ./mitrade-admin && npm install react-redux @types/react-redux
Step 3: Update the main index.tsx to include Redux Provider
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import './index.css';
import App from './App';
import { store } from './store';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
Step 4: Update ListProduct component with Redux integration and filters
import React, { useEffect, useState } from 'react';
import { Table, Input, Select, Slider, Button, Card, Row, Col, Image, Tag, Spin, Alert } from 'antd';
import { SearchOutlined, ClearOutlined } from '@ant-design/icons';
import { useAppDispatch, useAppSelector } from '../store/hooks';
import { fetchProducts, setCategoryFilter, setPriceFilter, clearFilters, Product } from '../store/slices/productsSlice';
const { Search } = Input;
const { Option } = Select;
const ListProduct = () => {
const dispatch = useAppDispatch();
const { products, loading, error, filteredProducts, filters } = useAppSelector((state) => state.products);
const [searchText, setSearchText] = useState('');
useEffect(() => {
dispatch(fetchProducts());
}, [dispatch]);
// Get unique categories for filter dropdown
const categories = Array.from(new Set(products.map(product => product.category)));
const handleCategoryChange = (value: string) => {
dispatch(setCategoryFilter(value));
};
const handlePriceChange = (value: number[]) => {
dispatch(setPriceFilter({ min: value[0], max: value[1] }));
};
const handleClearFilters = () => {
dispatch(clearFilters());
setSearchText('');
};
const handleSearch = (value: string) => {
setSearchText(value);
};
// Filter products based on search text
const searchFilteredProducts = filteredProducts.filter(product =>
product.title.toLowerCase().includes(searchText.toLowerCase()) ||
product.description.toLowerCase().includes(searchText.toLowerCase())
);
const columns = [
{
title: 'Image',
dataIndex: 'image',
key: 'image',
width: 100,
render: (image: string) => (
<Image
src={image}
alt="Product"
width={60}
height={60}
style={{ objectFit: 'cover' }}
fallback="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMIAAADDCAYAAADQvc6UAAABRWlDQ1BJQ0MgUHJvZmlsZQAAKJFjYGASSSwoyGFhYGDIzSspCnJ3UoiIjFJgf8LAwSDCIMogwMCcmFxc4BgQ4ANUwgCjUcG3awyMIPqyLsis7PPOq3QdDFcvjV3jOD1boQVTPQrgSkktTgbSf4A4LbmgqISBgTEFyFYuLykAsTuAbJEioKOA7DkgdjqEvQHEToKwj4DVhAQ5A9k3gGyB5IxEoBmML4BsnSQk8XQkNtReEOBxcfXxUQg1Mjc0dyHgXNJBSWpFCYh2zi+oLMpMzyhRcASGUqqCZ16yno6CkYGRAQMDKMwhqj/fAIcloxgHQqxAjIHBEugw5sUIsSQpBobtQPdLciLEVJYzMPBHMDBsayhILEqEO4DxG0txmrERhM29nYGBddr//5/DGRjYNRkY/l7////39v///y4Dmn+LgeHANwDrkl1AuO+pmgAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAwqADAAQAAAABAAAAwwAAAAD9b/HnAAAHlklEQVR4Ae3dP3Ik1RnG4W+FgYxN"
/>
),
},
{
title: 'Title',
dataIndex: 'title',
key: 'title',
render: (title: string) => (
<div style={{ maxWidth: 300 }}>
<div style={{ fontWeight: 'bold', marginBottom: 4 }}>{title}</div>
</div>
),
},
{
title: 'Category',
dataIndex: 'category',
key: 'category',
render: (category: string) => (
<Tag color="blue">{category}</Tag>
),
},
{
title: 'Price',
dataIndex: 'price',
key: 'price',
sorter: (a: Product, b: Product) => a.price - b.price,
render: (price: number) => (
<span style={{ fontWeight: 'bold', color: '#1890ff' }}>
${price.toFixed(2)}
</span>
),
},
{
title: 'Rating',
dataIndex: 'rating',
key: 'rating',
sorter: (a: Product, b: Product) => a.rating.rate - b.rating.rate,
render: (rating: { rate: number; count: number }) => (
<div>
<div style={{ fontWeight: 'bold' }}>⭐ {rating.rate}</div>
<div style={{ fontSize: '12px', color: '#666' }}>({rating.count} reviews)</div>
</div>
),
},
{
title: 'Description',
dataIndex: 'description',
key: 'description',
render: (description: string) => (
<div style={{ maxWidth: 400 }}>
{description.length > 100
? `${description.substring(0, 100)}...`
: description
}
</div>
),
},
];
if (loading) {
return (
<div style={{ textAlign: 'center', padding: '50px' }}>
<Spin size="large" />
<div style={{ marginTop: 16 }}>Loading products...</div>
</div>
);
}
if (error) {
return (
<div style={{ padding: '20px' }}>
<Alert
message="Error"
description={error}
type="error"
showIcon
/>
</div>
);
}
return (
<div style={{ padding: '20px' }}>
<Card title="Product List" style={{ marginBottom: 20 }}>
<Row gutter={[16, 16]} style={{ marginBottom: 20 }}>
<Col xs={24} sm={12} md={8} lg={6}>
<Search
placeholder="Search products..."
allowClear
onSearch={handleSearch}
style={{ width: '100%' }}
/>
</Col>
<Col xs={24} sm={12} md={8} lg={6}>
<Select
placeholder="Filter by category"
style={{ width: '100%' }}
allowClear
value={filters.category || undefined}
onChange={handleCategoryChange}
>
{categories.map(category => (
<Option key={category} value={category}>
{category}
</Option>
))}
</Select>
</Col>
<Col xs={24} sm={12} md={8} lg={6}>
<div>
<div style={{ marginBottom: 8 }}>Price Range: ${filters.minPrice} - ${filters.maxPrice}</div>
<Slider
range
min={0}
max={1000}
value={[filters.minPrice, filters.maxPrice]}
onChange={handlePriceChange}
style={{ width: '100%' }}
/>
</div>
</Col>
<Col xs={24} sm={12} md={8} lg={6}>
<Button
icon={<ClearOutlined />}
onClick={handleClearFilters}
style={{ width: '100%' }}
>
Clear Filters
</Button>
</Col>
</Row>
<div style={{ marginBottom: 16 }}>
<strong>Total Products:</strong> {searchFilteredProducts.length}
{searchFilteredProducts.length !== products.length && (
<span style={{ color: '#666', marginLeft: 8 }}>
(filtered from {products.length})
</span>
)}
</div>
</Card>
<Table
dataSource={searchFilteredProducts}
columns={columns}
rowKey="id"
pagination={{
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) =>
`${range[0]}-${range[1]} of ${total} items`,
}}
scroll={{ x: 1200 }}
/>
</div>
);
};
export default ListProduct;
Let me fix the TypeScript errors:
Now let’s test the application to make sure everything works:
npm start
Perfect! I’ve successfully implemented all the requirements you requested. Here’s a summary of what I’ve created:
✅ Step 1: Sample Ant Design Table with Product Data Structure
- Created a comprehensive table with columns for:
- Image: Product images with fallback
- Title: Product titles with proper formatting
- Category: Displayed as colored tags
- Price: Formatted with dollar signs and sorting
- Rating: Shows star rating and review count with sorting
- Description: Truncated for better display
✅ Step 2: Redux Toolkit Integration with API
- Redux Store Setup: Created
src/store/index.ts
with store configuration - Products Slice: Created
src/store/slices/productsSlice.ts
with: - Async thunk to fetch from
https://fakestoreapi.com/products
- State management for loading, error, and data
- TypeScript interfaces for Product data structure
- Redux Provider: Updated
src/index.tsx
to wrap the app with Redux Provider - TypeScript Hooks: Created
src/store/hooks.ts
for type-safe Redux usage
✅ Step 3: Data Binding and Filtering
- Search Functionality: Text search across product titles and descriptions
- Category Filter: Dropdown to filter by product category
- Price Range Filter: Slider to filter products by price range
- Clear Filters: Button to reset all filters
- Real-time Filtering: Combined filters work together
- Product Count: Shows total and filtered product counts
🎨 Additional Features Added:
- Loading States: Spinner while fetching data
- Error Handling: Alert component for API errors
- Responsive Design: Mobile-friendly layout with Ant Design Grid
- Pagination: Configurable table pagination
- Sorting: Price and rating columns are sortable
- Image Fallback: Handles broken image URLs gracefully
How to Use:
- The app will automatically fetch products when the ListProduct page loads
- Use the search box to find products by title or description
- Use the category dropdown to filter by specific categories
- Use the price slider to set minimum and maximum price ranges
- Click “Clear Filters” to reset all filters
- Sort by price or rating by clicking the column headers
- Navigate through pages using the pagination controls
The application is now running in the background and should be accessible at http://localhost:3000
. Navigate to the ListProduct page to see the fully functional product listing with all the filtering capabilities you requested!