Skip to main content

React Order History: Display Past Purchases & Tracking

An order history page is where customers go after purchase to track packages, reorder items, view receipts, and request refunds. Building an effective order history in React requires fetching past orders from your backend, displaying shipment status with real-time tracking data, and making it easy to repurchase favorite items. This article shows you how to create an order history page that delights repeat customers, reduces support requests, and encourages larger average order values. You'll learn how stores like Amazon use order history as a tool for customer retention, not just a record-keeping feature.

Design the Order History Data Model

An order object in your database should look like:

{
"id": "order_abc123",
"userId": "user_xyz",
"createdAt": "2026-05-15T10:30:00Z",
"status": "delivered",
"items": [
{
"id": "prod_123",
"name": "Wireless Headphones",
"quantity": 1,
"price": 129.99,
"imageUrl": "https://..."
}
],
"shipping": {
"address": "123 Main St, Springfield, IL 62701",
"carrier": "FedEx",
"trackingNumber": "794629384729847",
"estimatedDelivery": "2026-05-18T18:00:00Z",
"actualDelivery": "2026-05-18T14:23:00Z"
},
"totals": {
"subtotal": 129.99,
"tax": 10.40,
"shipping": 0,
"total": 140.39
},
"payment": {
"method": "Credit Card (Visa ****1234)",
"status": "completed"
}
}

Create an Order History Page

Create src/pages/OrderHistory.jsx:

import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import axios from 'axios';

export default function OrderHistory() {
const [orders, setOrders] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
const fetchOrders = async () => {
try {
const response = await axios.get(
`${import.meta.env.VITE_API_URL}/orders`,
{
headers: {
Authorization: `Bearer ${localStorage.getItem('authToken')}`,
},
}
);
setOrders(response.data.orders);
setError(null);
} catch (err) {
setError('Failed to load order history');
console.error(err);
} finally {
setLoading(false);
}
};

fetchOrders();
}, []);

if (loading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<p className="text-gray-600">Loading your orders...</p>
</div>
);
}

if (error) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<p className="text-red-600">{error}</p>
</div>
);
}

if (orders.length === 0) {
return (
<div className="min-h-screen bg-gray-50 py-12 text-center">
<h1 className="text-3xl font-bold mb-4">No Orders Yet</h1>
<p className="text-gray-600 mb-8">
You haven't placed any orders. Start shopping today!
</p>
<Link
to="/"
className="bg-blue-600 text-white px-6 py-2 rounded font-semibold hover:bg-blue-700"
>
Continue Shopping
</Link>
</div>
);
}

return (
<div className="min-h-screen bg-gray-50 py-12">
<div className="max-w-4xl mx-auto px-4">
<h1 className="text-3xl font-bold mb-8">Your Orders</h1>

<div className="space-y-6">
{orders.map((order) => (
<OrderCard key={order.id} order={order} />
))}
</div>
</div>
</div>
);
}

function OrderCard({ order }) {
const [expanded, setExpanded] = useState(false);

const getStatusColor = (status) => {
switch (status) {
case 'delivered':
return 'bg-green-100 text-green-800';
case 'in-transit':
return 'bg-blue-100 text-blue-800';
case 'processing':
return 'bg-yellow-100 text-yellow-800';
case 'cancelled':
return 'bg-red-100 text-red-800';
default:
return 'bg-gray-100 text-gray-800';
}
};

const getStatusLabel = (status) => {
switch (status) {
case 'delivered':
return 'Delivered';
case 'in-transit':
return 'In Transit';
case 'processing':
return 'Processing';
case 'cancelled':
return 'Cancelled';
default:
return status;
}
};

const orderDate = new Date(order.createdAt).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});

return (
<div className="bg-white rounded-lg shadow">
{/* Header */}
<div
className="p-6 border-b cursor-pointer hover:bg-gray-50"
onClick={() => setExpanded(!expanded)}
>
<div className="flex justify-between items-start">
<div>
<p className="text-sm text-gray-600">Order ID: {order.id}</p>
<p className="text-sm text-gray-600 mt-1">{orderDate}</p>
</div>
<span className={`px-3 py-1 rounded text-sm font-semibold ${getStatusColor(order.status)}`}>
{getStatusLabel(order.status)}
</span>
</div>

<div className="mt-4 flex justify-between items-center">
<div>
<p className="text-lg font-semibold">${order.totals.total.toFixed(2)}</p>
<p className="text-sm text-gray-600 mt-1">
{order.items.length} item{order.items.length !== 1 ? 's' : ''}
</p>
</div>
<button className="text-blue-600 hover:text-blue-800 font-semibold">
{expanded ? 'Hide Details' : 'Show Details'}
</button>
</div>
</div>

{/* Expanded Details */}
{expanded && (
<div className="p-6 space-y-6 border-t">
{/* Items */}
<div>
<h3 className="font-semibold mb-3">Items</h3>
<div className="space-y-3">
{order.items.map((item) => (
<div key={item.id} className="flex gap-4">
<img
src={item.imageUrl}
alt={item.name}
className="w-16 h-16 object-cover rounded"
/>
<div className="flex-1">
<p className="font-medium">{item.name}</p>
<p className="text-sm text-gray-600">
Qty: {item.quantity} x ${item.price.toFixed(2)}
</p>
</div>
<p className="font-semibold">
${(item.quantity * item.price).toFixed(2)}
</p>
</div>
))}
</div>
</div>

{/* Tracking */}
{order.shipping.trackingNumber && (
<div>
<h3 className="font-semibold mb-3">Tracking</h3>
<div className="bg-gray-50 p-4 rounded">
<p className="text-sm">
<span className="text-gray-600">Carrier:</span>{' '}
<span className="font-medium">{order.shipping.carrier}</span>
</p>
<p className="text-sm mt-2">
<span className="text-gray-600">Tracking #:</span>{' '}
<span className="font-mono font-medium">
{order.shipping.trackingNumber}
</span>
</p>
<a
href={getTrackingLink(order.shipping.carrier, order.shipping.trackingNumber)}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800 text-sm font-semibold mt-2 inline-block"
>
Track Package
</a>
</div>
</div>
)}

{/* Shipping Address */}
<div>
<h3 className="font-semibold mb-3">Shipping Address</h3>
<div className="text-sm text-gray-700">
<p>{order.shipping.address}</p>
</div>
</div>

{/* Totals */}
<div className="border-t pt-4">
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-600">Subtotal</span>
<span>${order.totals.subtotal.toFixed(2)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Tax</span>
<span>${order.totals.tax.toFixed(2)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Shipping</span>
<span>${order.totals.shipping.toFixed(2)}</span>
</div>
<div className="flex justify-between font-semibold text-base border-t pt-2">
<span>Total</span>
<span>${order.totals.total.toFixed(2)}</span>
</div>
</div>
</div>

{/* Actions */}
<div className="flex gap-4 pt-4">
<button className="flex-1 border border-gray-300 py-2 rounded font-semibold hover:bg-gray-50">
Reorder Items
</button>
<button className="flex-1 border border-gray-300 py-2 rounded font-semibold hover:bg-gray-50">
Get Invoice
</button>
</div>
</div>
)}
</div>
);
}

function getTrackingLink(carrier, trackingNumber) {
switch (carrier.toLowerCase()) {
case 'fedex':
return `https://tracking.fedex.com/en/tracking/${trackingNumber}`;
case 'ups':
return `https://www.ups.com/track?tracknum=${trackingNumber}`;
case 'usps':
return `https://tools.usps.com/go/TrackConfirmAction?tLabels=${trackingNumber}`;
case 'dhl':
return `https://www.dhl.com/en/en/express/tracking.html?AWB=${trackingNumber}`;
default:
return '#';
}
}

This component fetches orders from your backend, displays them in an expandable card format, and includes order summaries, tracking links, and action buttons.

Implement Order Status Tracking Timeline

Enhance the tracking display with a visual timeline:

function TrackingTimeline({ order }) {
const statuses = [
{ label: 'Order Placed', date: order.createdAt, completed: true },
{ label: 'Processing', date: order.shippingAt, completed: order.status !== 'processing' },
{ label: 'In Transit', date: order.inTransitAt, completed: ['in-transit', 'delivered'].includes(order.status) },
{ label: 'Delivered', date: order.deliveredAt, completed: order.status === 'delivered' },
];

return (
<div className="space-y-4">
{statuses.map((status, idx) => (
<div key={idx} className="flex gap-4">
<div className="flex flex-col items-center">
<div
className={`w-4 h-4 rounded-full ${
status.completed ? 'bg-green-600' : 'bg-gray-300'
}`}
></div>
{idx < statuses.length - 1 && (
<div className="w-0.5 h-12 bg-gray-300 mt-2"></div>
)}
</div>
<div>
<p className="font-medium">{status.label}</p>
{status.date && (
<p className="text-sm text-gray-600">
{new Date(status.date).toLocaleDateString()}
</p>
)}
</div>
</div>
))}
</div>
);
}

Add a "Reorder" Feature

Let customers quickly rebuy items from past orders:

const handleReorder = async (orderId) => {
const orderResponse = await axios.get(
`${import.meta.env.VITE_API_URL}/orders/${orderId}`,
{
headers: {
Authorization: `Bearer ${localStorage.getItem('authToken')}`,
},
}
);

const order = orderResponse.data;
const cartStore = useCartStore.getState();

// Add all items from the order to cart
order.items.forEach((item) => {
cartStore.addItem(item, item.quantity);
});

// Redirect to cart
window.location.href = '/cart';
};

Key Takeaways

  • Order history is a retention tool: make it easy for customers to reorder and track shipments.
  • Display order status visually with badges and timelines so customers know where their package is.
  • Integrate with carrier tracking APIs (FedEx, UPS, USPS) to provide real-time delivery estimates.
  • Allow customers to generate invoices, request refunds, and contact support directly from order details.
  • The "Reorder" button increases repeat purchase rates by 15–20% on average.

Frequently Asked Questions

How do I sync tracking data from shipping carriers?

Use carrier APIs (FedEx, UPS) to fetch tracking status periodically. A cron job every 4 hours checks for updates:

// Update tracking every 4 hours
setInterval(async () => {
const orders = await db.query('SELECT * FROM orders WHERE status = $1', ['in-transit']);
for (const order of orders) {
const tracking = await fedexAPI.getTracking(order.tracking_number);
await db.query('UPDATE orders SET status = $1 WHERE id = $2', [tracking.status, order.id]);
}
}, 4 * 60 * 60 * 1000);

Should I show cancelled orders?

Yes, but in a separate section or with a clear "Cancelled" status. Users want to see their entire purchase history.

Can I let customers cancel or modify orders?

Only if the order hasn't shipped. Once it's in-transit, only refunds or returns are allowed. Check order status before allowing modifications.

How long should I keep order history?

Legally, keep orders for 7 years (for tax and accounting). Display recent orders prominently; archive very old orders in a separate "Archived Orders" section.

Further Reading