Weekend Project: Building a Complex React Application

Applying Redux Toolkit with Performance Optimization

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:

flowchart TD A[1. Understand the Problem] --> B[2. Devise a Plan] B --> C[3. Execute the Plan] C --> D[4. Review/Reflect] D --> A

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

Understand the Domain

Before diving into code, let's understand the domain we're working with. An e-commerce dashboard typically involves:

To clarify our understanding, let's ask some key questions:

E-Commerce Dashboard Domain Model Products - name, description - price, inventory - categories, images Orders - order ID, date - customer, items - status, total Users - name, email - role, permissions - purchase history Analytics - sales metrics - product performance - customer insights

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

flowchart TD subgraph "Frontend Architecture" A[React Components] --> B[Redux Toolkit Store] B --> A A --> C[React Router] D[API Service Layer] --> B end subgraph "Backend Integration" D <--> E[Mock API / JSON Server] end subgraph "Performance Optimizations" F[Code Splitting] --> A G[Memoization] --> A H[Virtualization] --> A end

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:

Performance Optimization Planning

We'll implement the following performance optimizations:

Development Plan and Timeline

Let's break down our implementation into manageable chunks:

  1. Setup (2 hours): Create project structure, install dependencies, set up routing
  2. Core State Management (3 hours): Implement Redux Toolkit store and main slices
  3. Basic UI Components (4 hours): Implement critical UI components
  4. Feature Implementation (6 hours): Build out the main features
  5. Performance Optimization (3 hours): Apply performance techniques
  6. 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

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:

Areas for Improvement

While our implementation meets the requirements, there are always ways to improve:

flowchart LR A[Understand\nthe Problem] -->|Led to| B[Clear Requirements\nand Domain Model] B -->|Enabled| C[Effective Planning\nand Architecture] C -->|Resulted in| D[Structured\nImplementation] D -->|Produced| E[Optimized\nApplication] E -->|Identified| F[Future\nImprovements] F -->|Informs| A

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:

  1. Setup your project
    • Create a new React application
    • Install required dependencies
    • Set up the project structure
  2. 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
  3. Build the UI components
    • Create layout components
    • Implement feature-specific components
    • Apply performance optimizations (memo, virtualization)
    • Set up routing with code splitting
  4. Implement core features
    • Product catalog management
    • Order tracking system
    • User authentication
    • Analytics dashboard
  5. Add performance optimizations
    • Code splitting for routes and heavy components
    • Component memoization
    • List virtualization
    • Selector optimization
    • Performance monitoring
  6. 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:

Remember to document your performance optimization strategy and include before/after metrics!