4 Rounds: Online Assessment, JS + Framework, Machine Coding & System Design
Staff Software Engineer @ Walmart Global Tech
Mentored 100+ frontend developers through successful interviews
3 hands-on problems focused on React components and CSS. Test cases are strict — edge cases like empty state and API failure carry weight.
Difficulty: Medium | Time: 25 minutes
import { useState, useEffect, useRef, useCallback } from 'react';
function InfiniteScroll() {
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [hasMore, setHasMore] = useState(true);
const observerRef = useRef(null);
const fetchItems = useCallback(async (pageNum) => {
if (loading || !hasMore) return;
setLoading(true);
setError(null);
try {
const response = await fetch(
`/api/items?page=${pageNum}&limit=20`
);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: Failed to fetch`);
}
const data = await response.json();
setItems(prev => [...prev, ...data.items]);
setHasMore(data.hasMore);
setPage(pageNum + 1);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}, [loading, hasMore]);
// Initial fetch
useEffect(() => {
fetchItems(1);
}, []);
// Intersection Observer for infinite scroll trigger
const lastItemRef = useCallback((node) => {
if (loading) return;
// Disconnect previous observer
if (observerRef.current) {
observerRef.current.disconnect();
}
observerRef.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasMore && !error) {
fetchItems(page);
}
}, { threshold: 0.1 });
if (node) observerRef.current.observe(node);
}, [loading, hasMore, page, error, fetchItems]);
// Empty state
if (!loading && items.length === 0 && !error) {
return (
<div className="empty-state">
<p>No items found</p>
</div>
);
}
return (
<div className="infinite-scroll-container">
{items.map((item, index) => {
const isLast = index === items.length - 1;
return (
<div
key={item.id}
ref={isLast ? lastItemRef : null}
className="item-card"
>
<h3>{item.title}</h3>
<p>{item.description}</p>
</div>
);
})}
{loading && <div className="loader">Loading...</div>}
{error && (
<div className="error-state">
<p>Error: {error}</p>
<button onClick={() => fetchItems(page)}>
Retry
</button>
</div>
)}
{!hasMore && items.length > 0 && (
<p className="end-message">You've reached the end</p>
)}
</div>
);
}
💡 Key Points for Scoring:
Difficulty: Medium | Time: 20 minutes
// Debounce implementation from scratch
function debounce(fn, delay) {
let timerId = null;
function debounced(...args) {
clearTimeout(timerId);
timerId = setTimeout(() => {
fn.apply(this, args);
}, delay);
}
debounced.cancel = () => {
clearTimeout(timerId);
timerId = null;
};
return debounced;
}
// Search Bar Component with debounce
function DebouncedSearchBar() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
const abortControllerRef = useRef(null);
// Create debounced search function
const debouncedSearch = useRef(
debounce(async (searchTerm) => {
if (!searchTerm.trim()) {
setResults([]);
setLoading(false);
return;
}
// Cancel previous in-flight request
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
abortControllerRef.current = new AbortController();
setLoading(true);
try {
const response = await fetch(
`/api/search?q=${encodeURIComponent(searchTerm)}`,
{ signal: abortControllerRef.current.signal }
);
const data = await response.json();
setResults(data.results);
} catch (err) {
if (err.name !== 'AbortError') {
console.error('Search failed:', err);
}
} finally {
setLoading(false);
}
}, 300)
).current;
const handleChange = (e) => {
const value = e.target.value;
setQuery(value);
debouncedSearch(value);
};
// Cleanup on unmount
useEffect(() => {
return () => {
debouncedSearch.cancel();
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, []);
return (
<div className="search-container">
<input
type="text"
value={query}
onChange={handleChange}
placeholder="Search restaurants..."
/>
{loading && <div className="spinner" />}
<ul className="results-list">
{results.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
{!loading && query && results.length === 0 && (
<p>No results found for "{query}"</p>
)}
</div>
);
}
💡 Key Points:
Difficulty: Easy-Medium | Time: 15 minutes
auto-fit and minmax — no media queries for column count.
🎯 Solution:
/* Responsive grid — no media queries needed for columns! */
.image-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
padding: 1rem;
}
.image-card {
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.2s, box-shadow 0.2s;
}
.image-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
.image-card img {
width: 100%;
height: 200px;
object-fit: cover;
display: block;
}
.image-card .info {
padding: 1rem;
}
/* For very small screens — single column override */
@media (max-width: 320px) {
.image-grid {
grid-template-columns: 1fr;
}
}
// React component
function ResponsiveImageGrid({ images }) {
return (
<div className="image-grid">
{images.map((img) => (
<div key={img.id} className="image-card">
<img
src={img.url}
alt={img.alt}
loading="lazy"
width="280"
height="200"
/>
<div className="info">
<h4>{img.title}</h4>
<p>{img.description}</p>
</div>
</div>
))}
</div>
);
}
💡 Key Insights:
Edge cases carry weight at Swiggy. Empty states, error recovery, and API failure handling are what separate a pass from a fail. Learn how our cohort members handle edge cases →
Deep JavaScript and React questions. They want you to write polyfills on screen — definitions alone do not pass.
Difficulty: Medium | Time: 10 minutes
useDebounce that debounces a value. When the input value changes, the debounced value should only update after the specified delay has passed without further changes.
🎯 Solution:
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
// Set timer to update debounced value
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Cleanup: clear timer if value changes before delay
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// Usage in a search component:
function SearchComponent() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearch = useDebounce(searchTerm, 500);
// This effect only fires when debouncedSearch changes
// (500ms after user stops typing)
useEffect(() => {
if (debouncedSearch) {
fetchSearchResults(debouncedSearch);
}
}, [debouncedSearch]);
return (
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
);
}
💡 Why this pattern works:
Difficulty: Hard | Time: 20 minutes
function VirtualizedList({ items, itemHeight, containerHeight }) {
const [scrollTop, setScrollTop] = useState(0);
const totalHeight = items.length * itemHeight;
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = Math.min(
items.length - 1,
Math.floor((scrollTop + containerHeight) / itemHeight)
);
// Buffer: render extra items above/below for smooth scrolling
const bufferSize = 5;
const visibleStart = Math.max(0, startIndex - bufferSize);
const visibleEnd = Math.min(items.length - 1, endIndex + bufferSize);
const visibleItems = [];
for (let i = visibleStart; i <= visibleEnd; i++) {
visibleItems.push({
item: items[i],
index: i,
style: {
position: 'absolute',
top: i * itemHeight,
height: itemHeight,
width: '100%'
}
});
}
const handleScroll = (e) => {
setScrollTop(e.currentTarget.scrollTop);
};
return (
<div
onScroll={handleScroll}
style={{ height: containerHeight, overflow: 'auto', position: 'relative' }}
>
{/* Total height spacer */}
<div style={{ height: totalHeight }}>
{visibleItems.map(({ item, index, style }) => (
<div key={item.id} style={style}>
<OrderItem data={item} />
</div>
))}
</div>
</div>
);
}
// With react-window (production approach):
import { FixedSizeList } from 'react-window';
function OrderList({ orders }) {
const Row = ({ index, style }) => (
<div style={style}>
<OrderItem data={orders[index]} />
</div>
);
return (
<FixedSizeList
height={600}
itemCount={orders.length}
itemSize={72}
width="100%"
>
{Row}
</FixedSizeList>
);
}
📊 Performance Comparison:
Without Virtualization (10,000 items):
DOM Nodes: ~30,000
Initial Render: 2,400ms
Memory Usage: ~35MB
Scroll FPS: 12-18 (janky)
With Virtualization (same 10,000 items):
DOM Nodes: ~40 (visible + buffer)
Initial Render: 12ms
Memory Usage: ~3MB
Scroll FPS: 58-60 (smooth)
Difficulty: Hard | Time: 20 minutes
function useOrderTracker(orderId) {
const [orderStatus, setOrderStatus] = useState(null);
const [riderLocation, setRiderLocation] = useState(null);
const [connectionState, setConnectionState] = useState('connecting');
const wsRef = useRef(null);
const retryCountRef = useRef(0);
const maxRetries = 5;
const connect = useCallback(() => {
const ws = new WebSocket(
`wss://api.swiggy.com/orders/${orderId}/track`
);
wsRef.current = ws;
ws.onopen = () => {
setConnectionState('connected');
retryCountRef.current = 0; // Reset retry count on success
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'STATUS_UPDATE':
setOrderStatus(data.payload);
break;
case 'RIDER_LOCATION':
setRiderLocation(data.payload);
break;
case 'ORDER_DELIVERED':
setOrderStatus({ ...data.payload, isDelivered: true });
ws.close(1000); // Clean close
break;
}
};
ws.onerror = () => {
setConnectionState('error');
};
ws.onclose = (event) => {
// Don't reconnect if intentionally closed
if (event.code === 1000) return;
setConnectionState('disconnected');
// Exponential backoff reconnection
if (retryCountRef.current < maxRetries) {
const delay = Math.min(
1000 * Math.pow(2, retryCountRef.current),
30000 // Max 30 seconds
);
retryCountRef.current++;
setTimeout(connect, delay);
} else {
// Fallback to polling after max retries
setConnectionState('polling');
startPollingFallback();
}
};
}, [orderId]);
// Polling fallback when WebSocket fails
const startPollingFallback = useCallback(() => {
const intervalId = setInterval(async () => {
try {
const response = await fetch(
`/api/orders/${orderId}/status`
);
const data = await response.json();
setOrderStatus(data.status);
setRiderLocation(data.riderLocation);
if (data.status.isDelivered) {
clearInterval(intervalId);
}
} catch (err) {
console.error('Polling failed:', err);
}
}, 5000);
return () => clearInterval(intervalId);
}, [orderId]);
useEffect(() => {
connect();
return () => {
if (wsRef.current) {
wsRef.current.close(1000);
}
};
}, [connect]);
return { orderStatus, riderLocation, connectionState };
}
// Usage:
function OrderTracker({ orderId }) {
const { orderStatus, riderLocation, connectionState } =
useOrderTracker(orderId);
return (
<div>
{connectionState === 'polling' && (
<div className="banner-warning">
Live updates unavailable. Refreshing every 5s.
</div>
)}
<OrderStatusBar status={orderStatus} />
<DeliveryMap location={riderLocation} />
</div>
);
}
💡 Key Design Decisions:
Difficulty: Medium | Time: 10 minutes
Promise.race from scratch. It takes an iterable of promises and returns a single promise that resolves/rejects with the value/reason of the first promise that settles.
🎯 Solution:
function promiseRace(promises) {
return new Promise((resolve, reject) => {
const promiseArray = Array.from(promises);
// Edge case: empty iterable — promise never settles
// (matches native Promise.race behavior)
if (promiseArray.length === 0) return;
promiseArray.forEach((promise) => {
// Wrap in Promise.resolve to handle non-promise values
Promise.resolve(promise)
.then(resolve) // First to resolve wins
.catch(reject); // First to reject wins
});
});
}
// Test cases:
async function test() {
// Fast promise wins
const result1 = await promiseRace([
new Promise(res => setTimeout(() => res('slow'), 200)),
new Promise(res => setTimeout(() => res('fast'), 50)),
new Promise(res => setTimeout(() => res('medium'), 100)),
]);
console.log(result1); // 'fast'
// Rejection wins if it settles first
try {
await promiseRace([
new Promise(res => setTimeout(() => res('slow'), 200)),
new Promise((_, rej) => setTimeout(() => rej('error!'), 50)),
]);
} catch (e) {
console.log(e); // 'error!'
}
// Non-promise values resolve immediately
const result3 = await promiseRace([42, Promise.resolve('async')]);
console.log(result3); // 42
}
test();
💡 Key Insights:
// Timeout any slow API call
function fetchWithTimeout(url, timeout = 5000) {
return promiseRace([
fetch(url),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Request timed out')), timeout)
)
]);
}
Polyfills and React internals are non-negotiable at Swiggy. Writing code on screen under pressure is a different skill from knowing the answer. Practice with us →
Build a Restaurant Listing App in React from scratch. State modeling decides this round — lift cart state to context, keep filters local.
Time: 60 minutes
// ===== State Architecture =====
// CartContext.jsx — GLOBAL state (shared across components)
const CartContext = createContext();
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM': {
const existing = state.items.find(
i => i.id === action.payload.id
);
if (existing) {
return {
...state,
items: state.items.map(i =>
i.id === action.payload.id
? { ...i, quantity: i.quantity + 1 }
: i
)
};
}
return {
...state,
items: [...state.items, { ...action.payload, quantity: 1 }]
};
}
case 'REMOVE_ITEM':
return {
...state,
items: state.items.filter(i => i.id !== action.payload.id)
};
case 'UPDATE_QUANTITY': {
const { id, quantity } = action.payload;
if (quantity <= 0) {
return { ...state, items: state.items.filter(i => i.id !== id) };
}
return {
...state,
items: state.items.map(i =>
i.id === id ? { ...i, quantity } : i
)
};
}
default:
return state;
}
}
function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, { items: [] });
const totalPrice = useMemo(() =>
state.items.reduce(
(sum, item) => sum + item.price * item.quantity, 0
), [state.items]
);
const totalItems = useMemo(() =>
state.items.reduce((sum, item) => sum + item.quantity, 0),
[state.items]
);
return (
<CartContext.Provider value={{ ...state, totalPrice, totalItems, dispatch }}>
{children}
</CartContext.Provider>
);
}
function useCart() {
return useContext(CartContext);
}
// ===== Restaurant Listing with LOCAL filter state =====
function RestaurantListing() {
const [restaurants, setRestaurants] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// Filters are LOCAL — only this component needs them
const [cuisine, setCuisine] = useState('all');
const [sortBy, setSortBy] = useState('rating');
useEffect(() => {
async function fetchRestaurants() {
try {
setLoading(true);
const response = await fetch('/api/restaurants');
if (!response.ok) throw new Error('Failed to fetch');
const data = await response.json();
setRestaurants(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
fetchRestaurants();
}, []);
// Derived state — filter + sort applied
const displayedRestaurants = useMemo(() => {
let filtered = restaurants;
if (cuisine !== 'all') {
filtered = filtered.filter(r => r.cuisine === cuisine);
}
return [...filtered].sort((a, b) => {
if (sortBy === 'rating') return b.rating - a.rating;
if (sortBy === 'deliveryTime') return a.deliveryTime - b.deliveryTime;
return 0;
});
}, [restaurants, cuisine, sortBy]);
if (loading) return <SkeletonGrid count={6} />;
if (error) return <ErrorFallback message={error} onRetry={() => window.location.reload()} />;
return (
<div>
<FilterBar
cuisine={cuisine}
onCuisineChange={setCuisine}
sortBy={sortBy}
onSortChange={setSortBy}
cuisines={[...new Set(restaurants.map(r => r.cuisine))]}
/>
<div className="restaurant-grid">
{displayedRestaurants.map(restaurant => (
<RestaurantCard key={restaurant.id} data={restaurant} />
))}
</div>
{displayedRestaurants.length === 0 && (
<p className="empty">No restaurants match your filters</p>
)}
</div>
);
}
// ===== Error Boundary =====
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error('ErrorBoundary caught:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="error-fallback">
<h3>Something went wrong</h3>
<p>{this.state.error?.message}</p>
<button onClick={() => this.setState({ hasError: false })}>
Try Again
</button>
</div>
);
}
return this.props.children;
}
}
// ===== Skeleton Loading =====
function SkeletonGrid({ count = 6 }) {
return (
<div className="restaurant-grid">
{Array.from({ length: count }, (_, i) => (
<div key={i} className="skeleton-card">
<div className="skeleton-image pulse" />
<div className="skeleton-text pulse" />
<div className="skeleton-text short pulse" />
</div>
))}
</div>
);
}
/* Mobile Responsive Layout */
.restaurant-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
padding: 1rem;
}
@media (max-width: 640px) {
.restaurant-grid {
grid-template-columns: 1fr;
}
}
/* Skeleton Animation */
.pulse {
animation: pulse 1.5s ease-in-out infinite;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
}
@keyframes pulse {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
🎯 Evaluation Criteria:
State architecture is the hidden test in machine coding. Where you put state reveals your seniority level. Junior: everything in one component. Senior: context for shared, local for scoped. Learn our state modeling framework →
Design Swiggy's Restaurant Page with Real-Time Order Updates. Draw boxes. Talk trade-offs. Never say "I will use Redux" without justifying why.
Time: 60 minutes
┌──────────────────────────────────────────────────────────────┐
│ App Shell (always loaded — navbar, cart icon, location) │
├──────────────────────────────────────────────────────────────┤
│ │
│ Route-Level Code Splitting: │
│ ┌────────────────┐ ┌────────────────┐ ┌──────────────┐ │
│ │ Home Page │ │ Restaurant Page │ │ Order Track │ │
│ │ (lazy loaded) │ │ (lazy loaded) │ │ (lazy loaded)│ │
│ └────────────────┘ └────────────────┘ └──────────────┘ │
│ │
│ Restaurant Page Breakdown: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ RestaurantHeader (image, name, rating) — SSR │ │
│ ├──────────────────────────────────────────────────────┤ │
│ │ MenuCategories (tabs/accordion) │ │
│ │ ├── MenuItem (image, name, price, add-to-cart) │ │
│ │ ├── MenuItem │ │
│ │ └── MenuItem │ │
│ ├──────────────────────────────────────────────────────┤ │
│ │ CartDrawer (slides in from right) — Portal │ │
│ ├──────────────────────────────────────────────────────┤ │
│ │ OrderStatusBar (real-time WebSocket updates) │ │
│ └──────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
// Route-level splitting
const Home = lazy(() => import('./pages/Home'));
const Restaurant = lazy(() => import('./pages/Restaurant'));
const OrderTrack = lazy(() => import('./pages/OrderTrack'));
// Component-level splitting (heavy components)
const CartDrawer = lazy(() => import('./components/CartDrawer'));
const MenuImages = lazy(() => import('./components/MenuImages'));
function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/restaurant/:id" element={<Restaurant />} />
<Route path="/track/:orderId" element={<OrderTrack />} />
</Routes>
</Suspense>
);
}
2️⃣ State Management Strategy (with justification):
// WHY this choice matters:
//
// Redux: Overkill for this. Boilerplate heavy, adds 7KB.
// Use only if you need time-travel debugging or complex
// middleware (saga for payment flows).
//
// Context + useReducer: Perfect for cart (shared, changes often).
// Native React, no library needed, good for 2-3 consumers.
//
// Zustand: Best for larger apps. 1KB, no Provider wrapping,
// works outside React (WebSocket handlers).
// Pick this if interviewer pushes for scalability.
//
// React Query: Server state (menus, restaurant data).
// Automatic caching, deduplication, background refetch.
// Decision for Swiggy Restaurant Page:
const stateStrategy = {
serverState: 'React Query', // Restaurant data, menu, reviews
clientState: 'Zustand', // Cart, UI preferences
realTimeState: 'WebSocket + Zustand', // Order tracking
formState: 'Local useState', // Search, filters (ephemeral)
};
// Zustand store for cart (justification: accessed from WebSocket handler)
import { create } from 'zustand';
const useCartStore = create((set, get) => ({
items: [],
restaurantId: null,
addItem: (item) => set((state) => {
// Prevent adding from different restaurant
if (state.restaurantId && state.restaurantId !== item.restaurantId) {
// Show "clear cart?" modal
return state;
}
const existing = state.items.find(i => i.id === item.id);
if (existing) {
return {
items: state.items.map(i =>
i.id === item.id ? { ...i, qty: i.qty + 1 } : i
)
};
}
return {
items: [...state.items, { ...item, qty: 1 }],
restaurantId: item.restaurantId
};
}),
getTotal: () => {
const { items } = get();
return items.reduce((sum, i) => sum + i.price * i.qty, 0);
}
}));
3️⃣ Data Fetching + Caching:
// React Query configuration
function useRestaurantMenu(restaurantId) {
return useQuery({
queryKey: ['restaurant', restaurantId, 'menu'],
queryFn: () => fetchMenu(restaurantId),
staleTime: 5 * 60 * 1000, // Menu valid for 5 min
cacheTime: 30 * 60 * 1000, // Keep in cache for 30 min
refetchOnWindowFocus: false, // Don't refetch menu on tab switch
retry: 2,
// Show stale data while refetching
placeholderData: (previousData) => previousData,
});
}
// Prefetching strategy:
// When user hovers on restaurant card → prefetch menu
function RestaurantCard({ restaurant }) {
const queryClient = useQueryClient();
const handleHover = () => {
queryClient.prefetchQuery({
queryKey: ['restaurant', restaurant.id, 'menu'],
queryFn: () => fetchMenu(restaurant.id),
staleTime: 5 * 60 * 1000
});
};
return (
<Link
to={`/restaurant/${restaurant.id}`}
onMouseEnter={handleHover}
>
{/* card content */}
</Link>
);
}
4️⃣ Performance Optimization:
// LCP Optimization — Restaurant hero image
// 1. Preload the hero image
<link rel="preload" as="image" href={restaurant.heroImage} />
// 2. Use responsive images with srcset
<img
src={restaurant.heroImage}
srcSet={`
${restaurant.heroImage}?w=400 400w,
${restaurant.heroImage}?w=800 800w,
${restaurant.heroImage}?w=1200 1200w
`}
sizes="(max-width: 768px) 100vw, 800px"
fetchPriority="high"
alt={restaurant.name}
/>
// 3. Image CDN with auto-format (WebP/AVIF)
function getOptimizedImageUrl(url, { width, quality = 80 }) {
return `${CDN_BASE}/${url}?w=${width}&q=${quality}&f=auto`;
}
// Lazy loading for below-fold menu images
function MenuItemImage({ src, alt }) {
return (
<img
src={getOptimizedImageUrl(src, { width: 200 })}
alt={alt}
loading="lazy"
decoding="async"
width="200"
height="200"
/>
);
}
// Bundle performance budget
// Entry: < 150KB (gzipped)
// Route chunk: < 50KB each
// Total JS: < 300KB first load
5️⃣ Error Handling + Fallback UI:
// Layered error handling strategy:
//
// Layer 1: React Query retry (network errors, 5xx)
// Layer 2: Error Boundary (render crashes)
// Layer 3: Fallback UI (graceful degradation)
// Layer 4: Offline support (Service Worker + cached cart)
// Offline cart preservation
function useOfflineCart() {
const cart = useCartStore();
// Persist cart to localStorage on change
useEffect(() => {
localStorage.setItem('swiggy_cart', JSON.stringify(cart.items));
}, [cart.items]);
// Restore on app load
useEffect(() => {
const saved = localStorage.getItem('swiggy_cart');
if (saved) {
try {
const items = JSON.parse(saved);
// Validate items still exist in menu (background check)
cart.restoreItems(items);
} catch (e) {
localStorage.removeItem('swiggy_cart');
}
}
}, []);
}
// Skeleton states for each section
function RestaurantPage({ id }) {
const { data: menu, isLoading, error } = useRestaurantMenu(id);
return (
<ErrorBoundary fallback={<ErrorFallback />}>
{isLoading ? (
<MenuSkeleton />
) : error ? (
<ErrorCard message="Failed to load menu" onRetry={refetch} />
) : (
<MenuList items={menu} />
)}
</ErrorBoundary>
);
}
Swiggy-Specific Edge Cases to Discuss:
System design at Swiggy is about justifying every decision with trade-offs. The interviewer will challenge your choices — be ready to defend or pivot. Learn our system design framework →
Hunt inline props and missing keys. Show you think about render performance before adding features.
Live order tracker always comes. Lead with exponential backoff reconnection and polling fallback.
Cart to context, filters stay local. Mixing state ownership fails you. Be deliberate.
Offline cart, slow 3G image loading, cross-restaurant cart conflicts, LCP optimization.
Join our cohort and get structured preparation with 1-on-1 guidance from a Staff Engineer who has mentored 100+ developers.