Introduction: Weekend Project Overview
Welcome to our Module 13 weekend project! Over the next two days, you'll be building a comprehensive e-commerce dashboard application using React and Redux Toolkit, with a focus on performance optimization. This project will integrate everything we've covered this week: Context API, Redux fundamentals, Redux Toolkit, routing, and performance optimization techniques.
To approach this complex project, we'll use George Polya's famous 4-step problem-solving method, which provides a structured framework that works remarkably well for software development:
This framework will help us break down what seems like a daunting task into manageable components and ensure our solution is well-thought-out before we start coding.
Phase 1: Understand the Problem
The first step in Polya's method is to fully understand the problem. Let's break down the requirements for our e-commerce dashboard application:
Project Requirements
- Core Functionality:
- Product catalog management
- Order tracking system
- User authentication
- Sales analytics dashboard
- Technical Requirements:
- Use React for UI components
- Implement Redux Toolkit for state management
- Set up React Router for navigation
- Optimize performance using techniques learned this week
- Follow best practices for code organization
- Performance Goals:
- Initial load under 2 seconds on standard connection
- Smooth navigation between routes
- Efficient handling of large data sets
- No UI freezing during complex operations
Understand the Domain
Before diving into code, let's understand the domain we're working with. An e-commerce dashboard typically involves:
- Products: Items for sale with attributes like name, price, inventory count, categories, and images
- Orders: Customer purchases with order date, status, items, quantities, shipping information, etc.
- Users: Customers and admin users with different permissions and data
- Analytics: Sales trends, popular products, customer behavior patterns
To clarify our understanding, let's ask some key questions:
- Who are the primary users of this dashboard? (Store administrators)
- What actions will they perform most frequently? (Check orders, update products)
- What data volume are we handling? (Potentially thousands of products and orders)
- What are the most performance-intensive operations? (Filtering large product lists, generating reports)
By thoroughly understanding both the functional requirements and the domain, we've established a solid foundation for our project. This understanding will guide our architecture decisions and help us identify potential performance bottlenecks early.
Phase 2: Devise a Plan
Now that we understand what we're building, let's devise a comprehensive plan. This includes our application architecture, component structure, state management approach, and performance optimization strategy.
Application Architecture
Project Structure
src/
├── components/
│ ├── common/ # Reusable UI components
│ ├── layout/ # Layout components
│ ├── products/ # Product-related components
│ ├── orders/ # Order-related components
│ ├── users/ # User-related components
│ └── analytics/ # Analytics-related components
├── features/ # Redux Toolkit slices
│ ├── products/
│ ├── orders/
│ ├── users/
│ └── analytics/
├── pages/ # Route components
├── services/ # API services
├── utils/ # Utility functions
├── hooks/ # Custom hooks
├── App.js
└── index.js
State Management Strategy
We'll use Redux Toolkit to manage application state with the following organization:
- Feature-based slices: Separate slices for products, orders, users, and analytics
- Normalized state: Using createEntityAdapter for efficient data storage and lookup
- Async logic: Using createAsyncThunk for API calls
- Local state: Using React's useState/useReducer for component-specific state
Performance Optimization Planning
We'll implement the following performance optimizations:
- Code Splitting: Lazy-load routes and heavy components
- Memoization: Use React.memo, useMemo, and useCallback strategically
- Virtualization: Implement windowing for long lists (products, orders)
- Selector Optimization: Use createSelector for derived state calculations
- Bundle Size Monitoring: Set up tools to track bundle size
Development Plan and Timeline
Let's break down our implementation into manageable chunks:
- Setup (2 hours): Create project structure, install dependencies, set up routing
- Core State Management (3 hours): Implement Redux Toolkit store and main slices
- Basic UI Components (4 hours): Implement critical UI components
- Feature Implementation (6 hours): Build out the main features
- Performance Optimization (3 hours): Apply performance techniques
- Testing and Refinement (2 hours): Test, debug, and polish
Remember to follow the principle of progressive enhancement: Get a basic version working first, then add features and optimizations incrementally.
Phase 3: Execute the Plan
Now let's dive into the implementation, following our plan step by step. We'll focus on key aspects of the implementation rather than building everything from scratch.
Step 1: Project Setup
Let's start by setting up our project with all the necessary dependencies:
# Create a new React application
npx create-react-app ecommerce-dashboard
# Navigate to the project directory
cd ecommerce-dashboard
# Install dependencies
npm install @reduxjs/toolkit react-redux react-router-dom
npm install axios chart.js react-chartjs-2 react-window date-fns
npm install json-server --save-dev
Next, let's set up our folder structure according to our plan:
# Create the main directories
mkdir -p src/{components/{common,layout,products,orders,users,analytics},features,pages,services,utils,hooks}
Step 2: Setting Up Redux Toolkit
Let's implement our store and a sample slice for products:
// src/features/products/productsSlice.js
import { createSlice, createAsyncThunk, createEntityAdapter } from '@reduxjs/toolkit';
import { fetchProducts } from '../../services/api';
// Create an entity adapter for normalized state management
const productsAdapter = createEntityAdapter({
// Products have an 'id' field as their primary key
selectId: (product) => product.id,
// Sort products by name
sortComparer: (a, b) => a.name.localeCompare(b.name),
});
// Initial state with loading status and error information
const initialState = productsAdapter.getInitialState({
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null,
activeFilters: {
category: null,
priceRange: { min: 0, max: null },
inStock: null,
},
});
// Async thunk for fetching products
export const fetchProductsAsync = createAsyncThunk(
'products/fetchProducts',
async (_, { rejectWithValue }) => {
try {
const response = await fetchProducts();
return response.data;
} catch (err) {
return rejectWithValue(err.response.data);
}
}
);
// Create the slice
const productsSlice = createSlice({
name: 'products',
initialState,
reducers: {
// Add a new product
addProduct: productsAdapter.addOne,
// Update an existing product
updateProduct: productsAdapter.updateOne,
// Remove a product
removeProduct: productsAdapter.removeOne,
// Set filters
setFilter: (state, action) => {
const { filter, value } = action.payload;
state.activeFilters[filter] = value;
},
// Clear all filters
clearFilters: (state) => {
state.activeFilters = initialState.activeFilters;
},
},
extraReducers: (builder) => {
builder
.addCase(fetchProductsAsync.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchProductsAsync.fulfilled, (state, action) => {
state.status = 'succeeded';
// Add the fetched products to the state
productsAdapter.setAll(state, action.payload);
})
.addCase(fetchProductsAsync.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload || 'Failed to fetch products';
});
},
});
// Export actions
export const {
addProduct,
updateProduct,
removeProduct,
setFilter,
clearFilters
} = productsSlice.actions;
// Export the reducer
export default productsSlice.reducer;
// Export selectors
export const {
selectAll: selectAllProducts,
selectById: selectProductById,
selectIds: selectProductIds,
} = productsAdapter.getSelectors((state) => state.products);
// Create a memoized selector for filtered products
import { createSelector } from '@reduxjs/toolkit';
export const selectFilteredProducts = createSelector(
[selectAllProducts, (state) => state.products.activeFilters],
(products, filters) => {
return products.filter(product => {
// Apply category filter
if (filters.category && product.category !== filters.category) {
return false;
}
// Apply price range filter
if (filters.priceRange.min > product.price) {
return false;
}
if (filters.priceRange.max && filters.priceRange.max < product.price) {
return false;
}
// Apply in-stock filter
if (filters.inStock !== null && product.inStock !== filters.inStock) {
return false;
}
return true;
});
}
);
Now, let's create our Redux store that combines all slices:
// src/app/store.js
import { configureStore } from '@reduxjs/toolkit';
import productsReducer from '../features/products/productsSlice';
// Import other reducers as they are created
// import ordersReducer from '../features/orders/ordersSlice';
// import usersReducer from '../features/users/usersSlice';
// import analyticsReducer from '../features/analytics/analyticsSlice';
export const store = configureStore({
reducer: {
products: productsReducer,
// Add other reducers here
// orders: ordersReducer,
// users: usersReducer,
// analytics: analyticsReducer,
},
});
Step 3: Implementing React Router
Let's set up our routing with code splitting for performance optimization:
// src/App.js
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { Provider } from 'react-redux';
import { store } from './app/store';
// Import layout components
import MainLayout from './components/layout/MainLayout';
import LoadingFallback from './components/common/LoadingFallback';
// Lazy load page components for code splitting
const Dashboard = lazy(() => import('./pages/Dashboard'));
const ProductsPage = lazy(() => import('./pages/ProductsPage'));
const ProductDetailPage = lazy(() => import('./pages/ProductDetailPage'));
const OrdersPage = lazy(() => import('./pages/OrdersPage'));
const OrderDetailPage = lazy(() => import('./pages/OrderDetailPage'));
const UsersPage = lazy(() => import('./pages/UsersPage'));
const AnalyticsPage = lazy(() => import('./pages/AnalyticsPage'));
const LoginPage = lazy(() => import('./pages/LoginPage'));
function App() {
return (
}>
} />
}>
} />
} />
} />
} />
} />
} />
} />
);
}
export default App;
Step 4: Implementing Performance-Optimized Components
Let's create a virtualized product list component that efficiently renders large datasets:
// src/components/products/ProductList.jsx
import React, { useEffect, useMemo } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { FixedSizeList as List } from 'react-window';
import { selectFilteredProducts, fetchProductsAsync } from '../../features/products/productsSlice';
import ProductListItem from './ProductListItem';
const ProductList = () => {
const dispatch = useDispatch();
const products = useSelector(selectFilteredProducts);
const status = useSelector(state => state.products.status);
const error = useSelector(state => state.products.error);
useEffect(() => {
// Only fetch products if we haven't already
if (status === 'idle') {
dispatch(fetchProductsAsync());
}
}, [status, dispatch]);
// Memoize the row renderer function to prevent unnecessary re-renders
const Row = useMemo(() =>
({ index, style }) => (
<ProductListItem
product={products[index]}
style={style}
/>
),
[products]
);
if (status === 'loading') {
return <div>Loading products...</div>;
}
if (status === 'failed') {
return <div>Error: {error}</div>;
}
return (
<div className="product-list-container">
Products ({products.length})
{products.length === 0 ? (
<div>No products found matching your filters.</div>
) : (
<List
height={600}
width="100%"
itemCount={products.length}
itemSize={80}
>
{Row}
</List>
)}
</div>
);
};
// Wrap with React.memo to prevent unnecessary re-renders
export default React.memo(ProductList);
Now, let's create a memoized component for product filter controls:
// src/components/products/ProductFilters.jsx
import React, { useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { setFilter, clearFilters } from '../../features/products/productsSlice';
const ProductFilters = () => {
const dispatch = useDispatch();
const { activeFilters } = useSelector(state => state.products);
const categories = useSelector(state => {
// Extract unique categories from products
const products = state.products.entities;
return [...new Set(Object.values(products).map(product => product.category))];
});
// Memoize callback functions to prevent unnecessary re-renders
const handleCategoryChange = useCallback((e) => {
const value = e.target.value === 'all' ? null : e.target.value;
dispatch(setFilter({ filter: 'category', value }));
}, [dispatch]);
const handlePriceMinChange = useCallback((e) => {
const min = parseFloat(e.target.value) || 0;
dispatch(setFilter({
filter: 'priceRange',
value: { ...activeFilters.priceRange, min }
}));
}, [dispatch, activeFilters.priceRange]);
const handlePriceMaxChange = useCallback((e) => {
const max = parseFloat(e.target.value) || null;
dispatch(setFilter({
filter: 'priceRange',
value: { ...activeFilters.priceRange, max }
}));
}, [dispatch, activeFilters.priceRange]);
const handleInStockChange = useCallback((e) => {
const value = e.target.value === 'all'
? null
: e.target.value === 'true';
dispatch(setFilter({ filter: 'inStock', value }));
}, [dispatch]);
const handleClearFilters = useCallback(() => {
dispatch(clearFilters());
}, [dispatch]);
return (
<div className="product-filters">
<h3>Filters</h3>
<div className="filter-group">
<label htmlFor="category-filter">Category:</label>
<select
id="category-filter"
value={activeFilters.category || 'all'}
onChange={handleCategoryChange}
>
<option value="all">All Categories</option>
{categories.map(category => (
<option key={category} value={category}>{category}</option>
))}
</select>
</div>
<div className="filter-group">
<label htmlFor="price-min">Price Range:</label>
<input
type="number"
id="price-min"
placeholder="Min"
value={activeFilters.priceRange.min}
onChange={handlePriceMinChange}
/>
<span>to</span>
<input
type="number"
id="price-max"
placeholder="Max"
value={activeFilters.priceRange.max || ''}
onChange={handlePriceMaxChange}
/>
</div>
<div className="filter-group">
<label htmlFor="in-stock-filter">Availability:</label>
<select
id="in-stock-filter"
value={activeFilters.inStock === null ? 'all' : activeFilters.inStock.toString()}
onChange={handleInStockChange}
>
<option value="all">All Products</option>
<option value="true">In Stock Only</option>
<option value="false">Out of Stock Only</option>
</select>
</div>
<button onClick={handleClearFilters}>Clear All Filters</button>
</div>
);
};
// Wrap with React.memo to prevent unnecessary re-renders
export default React.memo(ProductFilters);
Step 5: Analytics Dashboard with Performance Optimizations
Let's create an analytics dashboard that efficiently handles data visualization:
// src/pages/AnalyticsPage.jsx
import React, { useEffect, useState, useMemo } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
BarElement,
LineElement,
PointElement,
Title,
Tooltip,
Legend
} from 'chart.js';
import { Bar, Line } from 'react-chartjs-2';
import { fetchAnalyticsDataAsync } from '../features/analytics/analyticsSlice';
import { selectFilteredProducts } from '../features/products/productsSlice';
import { selectAllOrders } from '../features/orders/ordersSlice';
import { format, subDays, eachDayOfInterval } from 'date-fns';
// Register ChartJS components
ChartJS.register(
CategoryScale,
LinearScale,
BarElement,
LineElement,
PointElement,
Title,
Tooltip,
Legend
);
const AnalyticsPage = () => {
const dispatch = useDispatch();
const [timeRange, setTimeRange] = useState('7days');
const analyticsData = useSelector(state => state.analytics.data);
const products = useSelector(selectFilteredProducts);
const orders = useSelector(selectAllOrders);
const status = useSelector(state => state.analytics.status);
useEffect(() => {
dispatch(fetchAnalyticsDataAsync(timeRange));
}, [dispatch, timeRange]);
// Calculate date range based on timeRange
const dateRange = useMemo(() => {
const today = new Date();
switch(timeRange) {
case '7days':
return eachDayOfInterval({
start: subDays(today, 6),
end: today
});
case '30days':
return eachDayOfInterval({
start: subDays(today, 29),
end: today
});
default:
return eachDayOfInterval({
start: subDays(today, 6),
end: today
});
}
}, [timeRange]);
// Prepare sales data for chart - memoized to avoid recalculation
const salesChartData = useMemo(() => {
if (!analyticsData.dailySales) return null;
return {
labels: dateRange.map(date => format(date, 'MMM dd')),
datasets: [
{
label: 'Daily Sales',
data: dateRange.map(date => {
const formattedDate = format(date, 'yyyy-MM-dd');
return analyticsData.dailySales[formattedDate] || 0;
}),
borderColor: 'rgba(75, 192, 192, 1)',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
}
]
};
}, [analyticsData.dailySales, dateRange]);
// Prepare top products data - memoized to avoid recalculation
const topProductsChartData = useMemo(() => {
if (!analyticsData.topProducts) return null;
const topProductsArray = Object.entries(analyticsData.topProducts)
.sort((a, b) => b[1] - a[1])
.slice(0, 5);
return {
labels: topProductsArray.map(([productId]) => {
const product = products.find(p => p.id === parseInt(productId));
return product ? product.name : `Product ${productId}`;
}),
datasets: [
{
label: 'Units Sold',
data: topProductsArray.map(([_, count]) => count),
backgroundColor: 'rgba(54, 162, 235, 0.6)',
}
]
};
}, [analyticsData.topProducts, products]);
if (status === 'loading') {
return <div>Loading analytics data...</div>;
}
return (
<div className="analytics-page">
<h1>Sales Analytics</h1>
<div className="time-range-selector">
<label htmlFor="time-range">Time Range:</label>
<select
id="time-range"
value={timeRange}
onChange={(e) => setTimeRange(e.target.value)}
>
<option value="7days">Last 7 Days</option>
<option value="30days">Last 30 Days</option>
<option value="90days">Last 90 Days</option>
</select>
</div>
<div className="charts-container">
<div className="chart-wrapper">
<h2>Sales Trend</h2>
{salesChartData && (
<Line
data={salesChartData}
options={{
responsive: true,
plugins: {
legend: {
position: 'top',
},
title: {
display: true,
text: 'Daily Sales',
},
},
}}
/>
)}
</div>
<div className="chart-wrapper">
<h2>Top Selling Products</h2>
{topProductsChartData && (
<Bar
data={topProductsChartData}
options={{
indexAxis: 'y',
responsive: true,
plugins: {
legend: {
position: 'top',
},
title: {
display: true,
text: 'Top 5 Selling Products',
},
},
}}
/>
)}
</div>
</div>
<div className="summary-cards">
{/* Summary cards for key metrics */}
<div className="summary-card">
<h3>Total Revenue</h3>
<p>${analyticsData.totalRevenue?.toFixed(2) || '0.00'}</p>
</div>
<div className="summary-card">
<h3>Orders</h3>
<p>{analyticsData.totalOrders || 0}</p>
</div>
<div className="summary-card">
<h3>Average Order Value</h3>
<p>${analyticsData.averageOrderValue?.toFixed(2) || '0.00'}</p>
</div>
<div className="summary-card">
<h3>Conversion Rate</h3>
<p>{analyticsData.conversionRate?.toFixed(2) || '0.00'}%</p>
</div>
</div>
</div>
);
};
export default AnalyticsPage;
Step 6: Adding a Custom Performance Monitoring Hook
Let's create a custom hook for monitoring component render performance:
// src/hooks/useRenderMetrics.js
import { useRef, useEffect } from 'react';
/**
* A custom hook to monitor component render performance
* @param {string} componentName - The name of the component to monitor
* @param {Object} dependencies - Object with dependencies to track
*/
const useRenderMetrics = (componentName, dependencies = {}) => {
// Store render count and timing
const renderCount = useRef(0);
const lastRenderTime = useRef(performance.now());
const firstRenderTime = useRef(performance.now());
// Track previous dependencies to detect changes
const prevDeps = useRef(dependencies);
useEffect(() => {
// Increment render count
renderCount.current += 1;
// Calculate time since last render
const now = performance.now();
const timeSinceLastRender = now - lastRenderTime.current;
const totalTime = now - firstRenderTime.current;
// Find which dependencies changed
const changedDeps = Object.keys(dependencies).filter(key => {
return dependencies[key] !== prevDeps.current[key];
});
// Log performance metrics
console.log(`[${componentName}] Render #${renderCount.current}`);
if (renderCount.current > 1) {
console.log(` Time since last render: ${timeSinceLastRender.toFixed(2)}ms`);
console.log(` Total mounted time: ${totalTime.toFixed(2)}ms`);
if (changedDeps.length > 0) {
console.log(` Changed dependencies: ${changedDeps.join(', ')}`);
} else {
console.log(` No tracked dependencies changed`);
}
}
// Update references for next render
lastRenderTime.current = now;
prevDeps.current = dependencies;
// Cleanup function
return () => {
if (renderCount.current === 1) {
console.log(`[${componentName}] Unmounted after 1 render`);
} else {
console.log(`[${componentName}] Unmounted after ${renderCount.current} renders`);
console.log(` Total lifecycle time: ${(performance.now() - firstRenderTime.current).toFixed(2)}ms`);
}
};
});
return {
renderCount: renderCount.current,
timeSinceMount: performance.now() - firstRenderTime.current
};
};
export default useRenderMetrics;
To use this hook in a component:
// Example usage in a component
const ProductListItem = ({ product }) => {
// Only enable in development
if (process.env.NODE_ENV === 'development') {
useRenderMetrics('ProductListItem', { productId: product.id });
}
// Component logic...
};
Phase 4: Review and Reflect
The final step in Polya's method is to review our solution and reflect on what we've learned. Let's assess our implementation against our initial requirements and identify areas for further improvement.
Evaluating Our Implementation
- Functionality: We've implemented the core features of our e-commerce dashboard, including product management, order tracking, and analytics.
- State Management: We've successfully used Redux Toolkit with proper normalization and optimized selectors.
- Performance: We've applied several optimization techniques:
- Code splitting with React.lazy() and Suspense
- Memoization with useMemo, useCallback, and React.memo
- Virtualization for long lists
- Optimized selectors with createSelector
- Performance monitoring with custom hooks
- Architecture: We've followed best practices for code organization and separation of concerns.
Performance Testing
To validate our optimizations, we should run performance tests:
// Adding simple performance testing to our application
// In index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
// Mark the start time
performance.mark('app-start');
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
// Measure initial render time
window.addEventListener('load', () => {
performance.mark('app-loaded');
performance.measure('app-render-time', 'app-start', 'app-loaded');
const measure = performance.getEntriesByName('app-render-time')[0];
console.log(`Initial app render time: ${measure.duration.toFixed(2)}ms`);
// Report web vitals metrics
reportWebVitals(console.log);
});
Lessons Learned
Throughout this project, we've learned several important lessons:
- Thorough planning saves time: Following Polya's method helped us avoid common pitfalls and create a structured solution.
- Redux Toolkit simplifies state management: RTK's utilities like createSlice and createEntityAdapter dramatically reduce boilerplate.
- Performance is a feature: Building performance in from the beginning is easier than optimizing later.
- Component architecture matters: Well-structured components with clear responsibilities are easier to optimize.
- Data normalization is essential: Proper state shape with normalized data enables efficient updates and rendering.
Areas for Improvement
While our implementation meets the requirements, there are always ways to improve:
- Server-side rendering: For even faster initial loads, SSR could be implemented.
- Web Workers: Heavy calculations could be offloaded to a web worker.
- Advanced caching: Implementing a caching strategy for API responses.
- Automated performance testing: Setting up CI/CD with performance budgets.
- Accessibility improvements: Ensuring the dashboard is fully accessible.
By following Polya's 4-step method, we've created a complex React application with Redux Toolkit that meets our performance goals. The structured approach allowed us to break down a complex task into manageable steps and build a solution that is both functional and performant.
Weekend Project Checklist
To successfully complete this weekend project, follow this checklist:
- Setup your project
- Create a new React application
- Install required dependencies
- Set up the project structure
- Implement Redux Toolkit store
- Set up the store configuration
- Create feature slices for products, orders, users, and analytics
- Implement entity adapters for normalized state
- Create optimized selectors
- Build the UI components
- Create layout components
- Implement feature-specific components
- Apply performance optimizations (memo, virtualization)
- Set up routing with code splitting
- Implement core features
- Product catalog management
- Order tracking system
- User authentication
- Analytics dashboard
- Add performance optimizations
- Code splitting for routes and heavy components
- Component memoization
- List virtualization
- Selector optimization
- Performance monitoring
- Test and refine
- Measure performance metrics
- Identify and fix performance bottlenecks
- Test on different devices and network conditions
- Document your optimization strategies
Your project will be evaluated based on:
- Correct implementation of Redux Toolkit patterns
- Effective use of performance optimization techniques
- Code organization and best practices
- Feature completeness and usability
- Measured performance improvements
Remember to document your performance optimization strategy and include before/after metrics!