React + TypeScript + TailwindCSS + WebSockets + Zustand + Leaflet.js
π§ Overview
The frontend of Freight Tracker has come a long way! In Phase 2 and Phase 3, we shifted from a static layout to a dynamic, interactive logistics dashboard powered by live updates and rich UI components. In this post, I'll walk through the main features implemented, how we handled state management and live updates, and some challenges I encountered (and solved).
π¦ Phase 2: CRUD Pages, State Management & Styling
β 1. Shipment Type Definitions
We created proper TypeScript types (Shipment
, ShipmentStatus
, ShipmentStatsDTO
, etc.) to mirror the backend schema and ensure type-safe code across components.
export type ShipmentStatus = "PENDING" | "IN_TRANSIT" | "DELIVERED" | "CANCELLED";
export interface Shipment {
id: number;
origin: string;
destination: string;
status: ShipmentStatus;
trackingNumber: string;
carrier?: string;
priority?: "LOW" | "MEDIUM" | "HIGH";
lastUpdatedTime: string;
}
π§ 2. Global State with Zustand
Zustand was introduced to maintain and manipulate shipment data globally. This allowed pages like the list, details, and map view to share consistent data without prop drilling.
const useShipmentStore = create((set) => ({
byId: {},
upsert: (shipment) => set((state) => ({
byId: { ...state.byId, [shipment.id]: shipment }
})),
bulkLoad: (shipments) => set({
byId: Object.fromEntries(shipments.map(s => [s.id, s]))
}),
}));
πΌοΈ 3. Basic Pages with Routing
We added multiple pages using react-router-dom
:
-
/
β HomePage -
/shipments
β ShipmentsPage -
/shipments/:id
β ShipmentDetailsPage -
/dashboard
β MapDashboardPage (added in Phase 3)
Routing was kept simple, with a layout container and top navigation.
ποΈ 4. Styling with TailwindCSS
All pages were styled with Tailwind for quick, consistent design. We used colors, spacing, typography, and component states (like animate-pulse
on loading) to make the UI pleasant and professional.
β‘ Phase 3: WebSocket Updates, Maps, and Visualization
π 1. Real-Time WebSocket Integration
Using @stomp/stompjs
and a WebSocket backend endpoint (/ws
), we added live updates to shipments. Any change to a shipment in the backend triggers a broadcast message, which is parsed and upserts the new shipment into Zustand.
stompClient?.subscribe("/topic/shipments", (message) => {
const shipment: Shipment = JSON.parse(message.body);
useShipmentStore.getState().upsert(shipment);
});
This ensures all views (including the map) reflect changes instantly β no need to refresh!
πΊοΈ 2. Interactive Map with Leaflet
We created a rich dashboard map to visualize routes between origin and destination cities. Highlights:
-
Leaflet.js with
react-leaflet
for the map - Geocoding city names using OpenCage API
- Route lines colored based on shipment status
- Markers with popups for extra detail
<Polyline positions={[originCoords, destinationCoords]} color={getColorForStatus(shipment.status)}>
<Popup>
<strong>Tracking:</strong> {shipment.trackingNumber}
<br />
<strong>Status:</strong> {shipment.status}
</Popup>
</Polyline>
We also cached geocode results in localStorage
to avoid repeated lookups.
π‘ 3. State-Driven Map Updates
Instead of fetching shipment data directly in the map page, we relied on Zustand. However, we included a fallback to getShipments()
on first load in case the store was still empty. We watched Object.values(byId).length
to trigger re-renders when shipments were updated.
useEffect(() => {
const shipments = Object.values(byId);
if (shipments.length === 0) {
const fresh = await getShipments();
bulkLoad(fresh);
}
...
}, [byId]);
This worked hand-in-hand with WebSocket updates β ensuring the map reflects changes as soon as they happen.
π¦ 4. Small Optimizations
- Loading indicator while routes are geocoded
- Conditional polyline coloring
- Map centering and zoom configuration
- Clean Leaflet icon setup
- Using
React.Fragment
with stable key props for rendering
π§ Lessons Learned
- Zustand is great for lightweight state management with zero boilerplate
- WebSocket integration with React is smooth with STOMP + Zustand
- It's important to debounce or cache expensive calls like geocoding
- Use localStorage smartly to persist data across sessions
- Routing doesn't cause component remounts by default β understand how effects work in SPA frameworks like React!
π Next Steps
- Add authentication and role-based views (client/admin separation)
- Real-time truck location tracking
- Better filtering and sorting options
- UI polish and animations
- Backend caching for geocodes (to avoid using frontend localStorage in production)
Top comments (0)