7 min read

useMemo in complex UI scenarios

useMemo in complex UI scenarios

In complex React applications like order management systems, unnecessary recalculations can slow down UIs and frustrate users. useMemo helps by caching expensive computations, ensuring they only rerun when dependencies change—perfect for dashboards handling dynamic order lists, filters, and summaries.

Why useMemo Matters in Order Workflows

Order management UIs often display filtered lists, aggregated stats, and derived data from hundreds of orders. Without optimization, every keystroke in a search bar or state update triggers full recomputes, leading to lag.

useMemo memoizes values, computing them only when dependencies shift. This is crucial for props vs state scenarios where parent components pass large datasets to children.

Consider a dashboard with orders from function and class components. Vanilla re-renders would recalculate totals on every tick; useMemo fixes that.

When to Use useMemo

Use useMemo for expensive operations: sorting/filtering large datasets, complex calculations, or derived data from state. Avoid for cheap ops to prevent overhead.

In order management:

  • Filter 1000+ orders by status/date.
  • Compute totals with taxes/discounts.
  • Process lists with keys for dynamic UIs.

Measure with React DevTools Profiler before/after. Pitfalls mirror useState issues: stale closures if deps wrong.

Basic useMemo Syntax and Setup

Declare useMemo like this:

import { useMemo } from 'react';

const memoizedValue = useMemo(() => {
  return expensiveCalculation(dependencies);
}, [dependency1, dependency2]);

The callback runs only if listed dependencies change. Empty deps [] make it run once, like useEffect with no updates.

In our order app, import it alongside useState pitfalls hooks for stable state.

Simple Example: Order Total Calculation

Without useMemo, totals recompute on every keystroke in unrelated inputs.

src/OrderStats.js (basic, no memo):

import React, { useState } from 'react';

const OrderStats = ({ orders }) => {
  const [unrelatedInput, setUnrelatedInput] = useState('');

  const total = orders.reduce((sum, order) => sum + order.amount, 0); // Recomputes every render!

  return (
    <div>
      <input value={unrelatedInput} onChange={(e) => setUnrelatedInput(e.target.value)} placeholder="Type anything" />
      <h3>Total: ${total.toFixed(2)}</h3>
    </div>
  );
};

export default OrderStats;

With useMemo:

import React, { useState, useMemo } from 'react';

const OrderStats = ({ orders, stats }) => {
  const [searchTerm, setSearchTerm] = useState('');

  // Demo useMemo: Expensive search that shouldn't trigger on every keystroke
  const matchingOrders = useMemo(() => {
    console.log('🔄 Computing matching orders...'); // You'll see this ONLY when orders change
    return orders.filter(order => 
      order.customer.toLowerCase().includes(searchTerm.toLowerCase())
    );
  }, [orders, searchTerm]); // Only recompute when orders OR searchTerm change

  return (
    <div className="stats">
      <h3>📊 Order Statistics ({matchingOrders.length} matching)</h3>
      
      {/* This input changes frequently but WON'T trigger matchingOrders recompute */}
      <input 
        value={searchTerm} 
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="Search customers (watch console!)"
        style={{marginBottom: '10px'}}
      />
      
      <div className="stats-grid">
        <div>Subtotal: ${stats.subtotal}</div>
        <div>Tax (18%): ${stats.tax}</div>
        <div>Discount: ${stats.discount}</div>
        <div className="grand-total">Grand Total: ${stats.grandTotal}</div>
        <div>Total Orders: {stats.orderCount}</div>
      </div>
    </div>
  );
};

export default OrderStats;

Intermediate: Filtering Orders

Extend to controlled inputs. Filter orders by status without recomputing full list.

src/OrderFilters.js:

import React, { useState, useMemo } from 'react';

const OrderFilters = ({ filters, onFiltersChange }) => {
  const [localFilters, setLocalFilters] = useState(filters);

  // Memoize filter validation
  const isValidDate = useMemo(() => {
    return !localFilters.date || !isNaN(Date.parse(localFilters.date));
  }, [localFilters.date]);

  const handleStatusChange = (e) => {
    const newFilters = { ...localFilters, status: e.target.value };
    setLocalFilters(newFilters);
    onFiltersChange(newFilters);
  };

  const handleDateChange = (e) => {
    const newFilters = { ...localFilters, date: e.target.value };
    setLocalFilters(newFilters);
    onFiltersChange(newFilters);
  };

  return (
    <div className="filters">
      <label>Status: 
        <select value={localFilters.status} onChange={handleStatusChange}>
          <option value="all">All Statuses</option>
          <option value="pending">Pending</option>
          <option value="shipped">Shipped</option>
          <option value="delivered">Delivered</option>
        </select>
      </label>
      
      <label>Date From: 
        <input 
          type="date" 
          value={localFilters.date} 
          onChange={handleDateChange}
          max={new Date().toISOString().split('T')[0]}
          disabled={!isValidDate}
        />
        {localFilters.date && !isValidDate && (
          <span style={{color: 'red'}}> Invalid date</span>
        )}
      </label>
      
      <button onClick={() => {
        setLocalFilters({ status: 'all', date: '' });
        onFiltersChange({ status: 'all', date: '' });
      }}>
        Clear Filters
      </button>
    </div>
  );
};

export default OrderFilters;

Integrates useEffect dependencies. Filters only rerun on filter changes.

Advanced: Order Dashboard with Sorting & Stats

Build a full dashboard. Memoize filtered+sorted list AND stats. Uses reusable components.

src/useOrderData.js (custom hook from custom hooks article):

import { useState, useEffect } from 'react';

export const useOrderData = (initialOrders) => {
  const [orders, setOrders] = useState([]);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    // Simulate API delay and real-time updates
    const timer = setTimeout(() => {
      setOrders(initialOrders);
      setIsLoading(false);
    }, 1500);

    // Simulate real-time order update every 10 seconds
    const interval = setInterval(() => {
      setOrders(prev => prev.map(order => 
        Math.random() > 0.95 
          ? { ...order, status: 'delivered' }
          : order
      ));
    }, 10000);

    return () => {
      clearTimeout(timer);
      clearInterval(interval);
    };
  }, [initialOrders]);

  return { orders, setOrders, isLoading };
};

src/OrderList.js (with keys for dynamic UIs):

import React from 'react';

const OrderList = ({ orders }) => {
  return (
    <div className="order-section">
      <h3>📋 Orders ({orders.length})</h3>
      <ul className="order-list">
        {orders.map(order => (
          <li key={order.id} className={`order ${order.status}`}>
            <strong>ID: {order.id}</strong> 
            {order.customer} | 
            ₹{parseFloat(order.amount).toFixed(2)} | 
            <span className="status">{order.status.toUpperCase()}</span> | 
            {order.date}
          </li>
        ))}
      </ul>
    </div>
  );
};

export default OrderList;

src/OrderDashboard.js (core: chains memoization):

import React, { useState, useMemo } from 'react';
import OrderFilters from './OrderFilters';
import OrderList from './OrderList';
import OrderStats from './OrderStats';
import { useOrderData } from './useOrderData';

const OrderDashboard = () => {
  const initialOrders = [
    { id: 1, customer: 'John Doe', amount: 299.99, status: 'pending', date: '2026-01-15' },
    { id: 2, customer: 'Jane Smith', amount: 149.50, status: 'shipped', date: '2026-01-20' },
    { id: 3, customer: 'Bob Wilson', amount: 499.00, status: 'delivered', date: '2026-01-10' },
    { id: 4, customer: 'Alice Brown', amount: 89.99, status: 'pending', date: '2026-01-25' },
    { id: 5, customer: 'Mike Chen', amount: 399.75, status: 'shipped', date: '2026-01-18' },
    // Generate 20 more realistic orders
    ...Array.from({ length: 20 }, (_, i) => ({
      id: i + 6,
      customer: `Customer ${i + 6}`,
      amount: (50 + Math.random() * 450).toFixed(2),
      status: ['pending', 'shipped', 'delivered'][Math.floor(Math.random() * 3)],
      date: `2026-01-${String(10 + Math.floor(Math.random() * 20)).padStart(2, '0')}`
    }))
  ];

  const { orders } = useOrderData(initialOrders);
  const [filters, setFilters] = useState({ status: 'all', date: '' });
  const [sortBy, setSortBy] = useState('date');
  const [sortDir, setSortDir] = useState('desc');

  // Memo 1: Filtered orders
  const filteredOrders = useMemo(() => {
    let result = orders;
    if (filters.status !== 'all') {
      result = result.filter(o => o.status === filters.status);
    }
    if (filters.date) {
      result = result.filter(o => new Date(o.date) >= new Date(filters.date));
    }
    return result;
  }, [orders, filters]);

  // Memo 2: Sorted orders
  const sortedOrders = useMemo(() => {
    return [...filteredOrders].sort((a, b) => {
      let aVal = a[sortBy], bVal = b[sortBy];
      if (sortBy === 'amount') { 
        aVal = parseFloat(aVal); 
        bVal = parseFloat(bVal); 
      }
      if (sortBy === 'date') { 
        aVal = new Date(aVal); 
        bVal = new Date(bVal); 
      }
      return sortDir === 'asc' ? (aVal > bVal ? 1 : -1) : (aVal < bVal ? 1 : -1);
    });
  }, [filteredOrders, sortBy, sortDir]);

  // Memo 3: Stats with tax/discount logic
  const stats = useMemo(() => {
    const subtotal = sortedOrders.reduce((sum, o) => sum + parseFloat(o.amount), 0);
    const tax = subtotal * 0.18;
    const discount = sortedOrders.length > 5 ? subtotal * 0.1 : 0;
    return {
      subtotal: subtotal.toFixed(2),
      tax: tax.toFixed(2),
      discount: discount.toFixed(2),
      grandTotal: (subtotal + tax - discount).toFixed(2),
      orderCount: sortedOrders.length
    };
  }, [sortedOrders]);

  return (
    <div className="dashboard">
      <h1>Order Management Dashboard</h1>
      <p>Watch performance: Filter, sort, type in stats - memos prevent lag!</p>
      
      <OrderFilters 
        filters={filters}
        onFiltersChange={setFilters}
      />
      
      <div className="controls">
        <label>Sort By: 
          <select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
            <option value="date">Date</option>
            <option value="amount">Amount</option>
            <option value="status">Status</option>
            <option value="customer">Customer</option>
          </select>
        </label>
        <label>Direction: 
          <select value={sortDir} onChange={(e) => setSortDir(e.target.value)}>
            <option value="asc">Ascending</option>
            <option value="desc">Descending</option>
          </select>
        </label>
      </div>
      
      <OrderStats orders={sortedOrders} stats={stats} />
      <OrderList orders={sortedOrders} />
    </div>
  );
};

export default OrderDashboard;

src/App.js:

import React from 'react';
import OrderDashboard from './OrderDashboard';
import './styles.css';

function App() {
  return (
    <div className="App">
      <OrderDashboard />
    </div>
  );
}

export default App;

src/styles.css:

* { box-sizing: border-box; }
body { 
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 
  margin: 0; padding: 20px; background: #f5f5f5; 
}
.App { max-width: 1200px; margin: 0 auto; }
.dashboard { background: white; padding: 30px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); }

.filters, .controls { 
  display: flex; gap: 20px; align-items: center; margin: 20px 0; 
  padding: 20px; background: #f8f9fa; border-radius: 8px; 
}
label { display: flex; flex-direction: column; font-weight: 500; }
select, input, button { 
  padding: 10px 12px; border: 2px solid #e1e5e9; border-radius: 6px; 
  font-size: 14px; margin-top: 5px; 
}
select:focus, input:focus { outline: none; border-color: #007bff; }
button { 
  background: #007bff; color: white; border: none; cursor: pointer; 
  font-weight: 600; transition: background 0.2s; 
}
button:hover { background: #0056b3; }

.stats { 
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 
  color: white; padding: 25px; border-radius: 12px; margin: 20px 0; 
}
.stats-grid { 
  display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); 
  gap: 15px; margin-top: 15px; 
}
.grand-total { 
  grid-column: 1 / -1; font-size: 24px; font-weight: bold; 
  background: rgba(255,255,255,0.2); padding: 15px; border-radius: 8px; 
}

.order-section { margin-top: 30px; }
.order-list { 
  list-style: none; padding: 0; max-height: 600px; overflow-y: auto; 
}
.order { 
  padding: 20px; margin: 10px 0; border-radius: 10px; 
  box-shadow: 0 2px 10px rgba(0,0,0,0.1); transition: transform 0.2s; 
}
.order:hover { transform: translateY(-2px); }
.order.pending { background: #fff3cd; border-left: 5px solid #ffc107; }
.order.shipped { background: #d1ecf1; border-left: 5px solid #17a2b8; }
.order.delivered { background: #d4edda; border-left: 5px solid #28a745; }
.status { 
  padding: 4px 12px; border-radius: 20px; font-size: 12px; 
  font-weight: bold; text-transform: uppercase; 
}
.pending .status { background: #ffc107; color: #212529; }
.shipped .status { background: #17a2b8; color: white; }
.delivered .status { background: #28a745; color: white; }

This dashboard filters/sorts 25+ orders efficiently. Typing in stats input? No lag. Change filters? Only affected memos recompute. Integrates lifting state up via props.

Pitfalls and Best Practices

Over-memoizing adds overhead—profile first. Always list all deps; ESLint helps.

Avoid in hot paths; prefer useCallback for functions. Test with React DevTools Profiler.

In order systems, prioritize user-impacting calcs like real-time totals.

Real-World Order Management Case Study

Imagine an e-commerce OMS dashboard. Raw orders from API, filters for status/region, charts for trends.

Without memo: Filter + sort + aggregate on every filter change = 200ms lag.

With layered useMemo:

  • Filter: deps [orders, filters]
  • Sort: deps [filteredOrders, sortBy]
  • Aggregates: deps [sortedOrders]

Result: 20ms renders, scalable to enterprise.

Extend prior useState batching: memo prevents stale closures.

When to Skip useMemo

Simple ops like orders.length don't need it. Overuse bloats components.

Stick to >10ms ops or loops over large arrays.

Next Article

"Validation with Formik/React Hook Form" – handling input validation in our order forms.