Introduction
The frontend landscape in 2025 has evolved significantly, with React Server Components becoming mainstream, hooks patterns maturing, and performance optimization techniques becoming more sophisticated. This guide explores the most critical interview topics that separate senior developers from the rest.
Part 1: React Hooks Intricacies
Question 1: Explain the closure trap in useEffect and how to avoid it
The Problem:
The closure trap occurs when a useEffect hook captures stale values from previous renders because it doesn't properly declare its dependencies. This happens because JavaScript closures capture variables from their lexical scope at the time of function creation.
Example of the Problem:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
console.log(count); // Always logs 0!
setCount(count + 1); // Always sets to 1!
}, 1000);
return () => clearInterval(timer);
}, []); // Empty dependency array causes closure trap
return <div>{count}</div>;
}
Why It Happens:
The interval callback captures the initial count value (0) when the effect first runs. Since the dependency array is empty, the effect never re-runs, and the callback never gets updated with new count values.
Solutions:
- Functional Updates: Use the function form of setState
setCount(c => c + 1); // Works correctly
- Proper Dependencies: Include all used variables
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(timer);
}, [count]); // Re-creates interval on each count change
- useRef for Latest Values: When you need current values without re-running effects
const countRef = useRef(count);
countRef.current = count;
useEffect(() => {
const timer = setInterval(() => {
setCount(countRef.current + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
Question 2: When would you use useMemo vs useCallback, and what are their performance implications?
useMemo: Memoizes a computed value
useCallback: Memoizes a function reference
Key Differences:
// useMemo - returns the result of the function
const expensiveValue = useMemo(() => {
return computeExpensiveValue(a, b);
}, [a, b]);
// useCallback - returns the function itself
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
// These are equivalent:
const memoizedCallback = useCallback(fn, deps);
const memoizedCallback = useMemo(() => fn, deps);
When to Use useMemo:
- Expensive calculations: When computing a value is CPU-intensive
- Referential equality: When passing objects/arrays to child components that use React.memo
- Dependency in other hooks: When the value is used in dependency arrays
When to Use useCallback:
- Passing callbacks to optimized children: Components wrapped in React.memo
- Dependencies in useEffect: When the function is a dependency in another hook
- Event handlers for expensive renders: But only when the child component is memoized
Performance Tradeoffs:
Both hooks have overhead:
- Memory cost: Storing previous values/functions
- Comparison cost: Checking dependencies on each render
Don't use them when:
- The computation is cheap (faster to just recalculate)
- Child components aren't memoized
- The dependencies change frequently anyway
Good Example:
function ProductList({ products, filter }) {
// Good: Expensive filtering operation
const filteredProducts = useMemo(() => {
return products.filter(p =>
complexFilterLogic(p, filter)
).sort((a, b) => complexSort(a, b));
}, [products, filter]);
// Good: Prevents re-render of memoized child
const handleClick = useCallback((id) => {
selectProduct(id);
}, [selectProduct]);
return (
<div>
{filteredProducts.map(p =>
<MemoizedProduct
key={p.id}
product={p}
onClick={handleClick}
/>
)}
</div>
);
}
Question 3: Explain the rules of hooks and why they exist at the implementation level
The Rules:
- Only call hooks at the top level (not in loops, conditions, or nested functions)
- Only call hooks from React function components or custom hooks
Why These Rules Exist:
React relies on the order of hook calls to maintain state between renders. React doesn't use variable names or IDs to track which state belongs to which hook call.
Internal Implementation Concept:
// Simplified React internals
let currentFiber = null;
let hookIndex = 0;
function useState(initialValue) {
const fiber = currentFiber;
const hooks = fiber.hooks || [];
const hook = hooks[hookIndex] || { state: initialValue };
hooks[hookIndex] = hook;
hookIndex++;
const setState = (newValue) => {
hook.state = newValue;
scheduleRender(fiber);
};
return [hook.state, setState];
}
What Breaks When Rules Are Violated:
function BrokenComponent({ condition }) {
const [a, setA] = useState(1);
if (condition) {
const [b, setB] = useState(2); // ❌ Conditional hook
}
const [c, setC] = useState(3);
}
// First render (condition=true):
// hooks[0] = state for 'a'
// hooks[1] = state for 'b'
// hooks[2] = state for 'c'
// Second render (condition=false):
// hooks[0] = state for 'a'
// hooks[1] = state for 'c' (WRONG! This gets the old 'b' state)
Correct Pattern:
function CorrectComponent({ condition }) {
const [a, setA] = useState(1);
const [b, setB] = useState(2); // Always call
const [c, setC] = useState(3);
// Use condition in render logic instead
return <div>{condition ? b : null}</div>;
}
Part 2: React Server Components (RSC)
Question 4: Explain the fundamental difference between Server Components and traditional SSR
Traditional SSR (Server-Side Rendering):
- Server renders HTML from React components
- Sends HTML to browser
- Browser downloads JavaScript bundle
- React hydrates the HTML (attaches event handlers, makes it interactive)
- All components become client-side components after hydration
React Server Components:
- Server components run ONLY on the server
- They never ship to the client (zero JavaScript bundle impact)
- Can directly access databases, file systems, etc.
- Output is a special format (not HTML), describing the UI
- Client components are rendered on the client as usual
Key Differences:
| Aspect | SSR | RSC |
|---|---|---|
| JavaScript bundle | Full bundle sent | Only client components sent |
| Re-rendering | Client-side | Can refetch from server |
| Data fetching | useEffect or getServerSideProps | Direct in component |
| State/Interactivity | After hydration | Only in Client Components |
Example:
// Server Component (no 'use client' directive)
async function ProductPage({ id }) {
// Runs on server only - direct DB access
const product = await db.products.findById(id);
const reviews = await db.reviews.findByProduct(id);
return (
<div>
<h1>{product.name}</h1>
<ProductImage src={product.image} />
{/* Client Component for interactivity */}
<AddToCartButton product={product} />
<ReviewList reviews={reviews} />
</div>
);
}
// Client Component
'use client'
function AddToCartButton({ product }) {
const [added, setAdded] = useState(false);
return (
<button onClick={() => setAdded(true)}>
{added ? 'Added!' : 'Add to Cart'}
</button>
);
}
Benefits:
- Reduced JavaScript bundle size
- Better initial load performance
- Automatic code splitting
- Direct backend access without API routes
- Can use server-only libraries
Question 5: How do you handle data mutations in a Server Component architecture?
The Challenge:
Server Components can't use hooks like useState or handle events directly. You need patterns for mutations that work in this model.
Solution 1: Server Actions
// Server Component
async function TodoList() {
const todos = await db.todos.findAll();
async function addTodo(formData) {
'use server' // Marks this as a Server Action
const text = formData.get('text');
await db.todos.create({ text });
revalidatePath('/todos'); // Refresh this route
}
return (
<div>
{todos.map(todo => <div key={todo.id}>{todo.text}</div>)}
<form action={addTodo}>
<input name="text" />
<button type="submit">Add</button>
</form>
</div>
);
}
Solution 2: Client Components with API Routes
// Server Component
async function ProductPage({ id }) {
const product = await getProduct(id);
// Pass data to Client Component
return <ProductDetails product={product} />;
}
// Client Component
'use client'
function ProductDetails({ product }) {
const [optimisticStock, setOptimisticStock] = useState(product.stock);
async function handlePurchase() {
// Optimistic update
setOptimisticStock(prev => prev - 1);
try {
await fetch('/api/purchase', {
method: 'POST',
body: JSON.stringify({ productId: product.id })
});
// Trigger server re-fetch
router.refresh();
} catch (error) {
// Revert optimistic update
setOptimisticStock(product.stock);
}
}
return (
<div>
<p>Stock: {optimisticStock}</p>
<button onClick={handlePurchase}>Buy Now</button>
</div>
);
}
Solution 3: Hybrid Approach with Optimistic Updates
'use client'
import { useOptimistic } from 'react';
function CommentSection({ comments, postId }) {
const [optimisticComments, addOptimisticComment] = useOptimistic(
comments,
(state, newComment) => [...state, { ...newComment, pending: true }]
);
async function submitComment(formData) {
const text = formData.get('text');
addOptimisticComment({ text, author: 'You' });
await createComment(postId, text); // Server Action
}
return (
<div>
{optimisticComments.map((comment, i) => (
<div key={i} style={{ opacity: comment.pending ? 0.5 : 1 }}>
{comment.text}
</div>
))}
<form action={submitComment}>
<input name="text" />
<button>Submit</button>
</form>
</div>
);
}
Question 6: What are the security implications of Server Components and how do you prevent data leaks?
Key Security Concerns:
- Accidental Data Exposure: Server Components can access sensitive data, but you must be careful what you pass to Client Components
Problem Example:
// ❌ DANGEROUS: Server Component
async function UserProfile({ userId }) {
const user = await db.users.findById(userId);
// This passes EVERYTHING to the client!
return <ClientSideProfile user={user} />;
}
// The entire user object (including passwordHash, email, etc.)
// is serialized and sent to the browser
Safe Approach:
// ✅ SAFE: Server Component
async function UserProfile({ userId }) {
const user = await db.users.findById(userId);
// Only pass what's needed
const publicData = {
name: user.name,
avatar: user.avatar,
bio: user.bio
};
return <ClientSideProfile user={publicData} />;
}
- Server Action Security: Validate everything, never trust client input
// ✅ Proper validation
async function updateProfile(formData) {
'use server'
// 1. Authentication
const session = await getSession();
if (!session) throw new Error('Unauthorized');
// 2. Authorization
const userId = formData.get('userId');
if (userId !== session.userId) {
throw new Error('Forbidden');
}
// 3. Input validation
const bio = formData.get('bio');
if (bio.length > 500) {
throw new Error('Bio too long');
}
// 4. Sanitization
const sanitizedBio = sanitizeHtml(bio);
await db.users.update(userId, { bio: sanitizedBio });
}
- Environment Variable Exposure
// ❌ WRONG: Client Component
'use client'
function Dashboard() {
// This tries to access server-only env var
const apiKey = process.env.SECRET_API_KEY; // undefined in browser!
}
// ✅ CORRECT: Server Component
async function Dashboard() {
const data = await fetch('https://api.example.com', {
headers: {
'Authorization': process.env.SECRET_API_KEY // Only on server
}
});
return <DataDisplay data={data} />;
}
Best Practices:
- Always validate Server Action inputs
- Use type-safe parsers (like Zod) for form data
- Never pass sensitive data to Client Components
- Implement proper authentication checks in Server Actions
- Use environment variables prefixed with
NEXT_PUBLIC_only for truly public data
Part 3: Performance Tradeoffs
Question 7: Explain the performance implications of code splitting strategies
What is Code Splitting?
Breaking your JavaScript bundle into smaller chunks that are loaded on-demand rather than all at once.
Strategy 1: Route-Based Splitting
// Automatic with Next.js app router
app/
products/page.js // Separate chunk
checkout/page.js // Separate chunk
profile/page.js // Separate chunk
Pros:
- Simple to implement
- Reduces initial bundle size
- Clear loading boundaries
Cons:
- May delay rendering of routes
- Doesn't help with large shared dependencies
Strategy 2: Component-Based Splitting
import dynamic from 'next/dynamic';
const HeavyChart = dynamic(() => import('./HeavyChart'), {
loading: () => <Spinner />,
ssr: false // Don't render on server
});
function Dashboard() {
return (
<div>
<QuickStats />
{/* Only loads when this component renders */}
<HeavyChart data={data} />
</div>
);
}
Pros:
- Fine-grained control
- Can defer expensive components
- Reduces main bundle size
Cons:
- More complex to manage
- Can cause layout shifts
- Network waterfalls if not careful
Strategy 3: Vendor Splitting
// webpack config
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
},
common: {
minChunks: 2,
name: 'common',
chunks: 'all'
}
}
}
}
Pros:
- Better caching (vendor code changes less)
- Shared dependencies in one file
- Predictable bundle structure
Cons:
- Large vendor bundle if not careful
- May download unused code
Performance Tradeoffs to Consider:
-
Bundle Size vs Request Count
- Fewer, larger bundles: Less overhead, but more unused code
- Many small bundles: More requests, more overhead
-
Initial Load vs Navigation Speed
- Eager loading: Fast navigation, slow initial
- Lazy loading: Fast initial, potential delays later
-
Caching vs Freshness
- Aggressive splitting: Better caching
- Less splitting: Simpler updates
Optimal Strategy (2025):
// Combine approaches
// 1. Route-based for pages
// 2. Component-based for heavy components
// 3. Vendor splitting for libraries
// Example: Next.js app structure
app/
layout.js // Core layout (eager)
page.js // Home (eager)
products/
page.js // Products route (lazy)
[id]/
page.js // Product detail (lazy)
components/
Header.js // Shared (in main bundle)
ProductGrid.js // Shared (in main bundle)
Reviews.js // Dynamic import (lazy)
Analytics.js // Dynamic import (lazy, low priority)
Measurement Approach:
// Monitor with performance API
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.initiatorType === 'script') {
console.log('Script load:', entry.name, entry.duration);
}
}
});
observer.observe({ entryTypes: ['resource'] });
Question 8: When should you use virtual scrolling vs pagination vs infinite scroll?
Virtual Scrolling (Windowing)
Render only visible items plus a buffer, reuse DOM elements as you scroll.
import { FixedSizeList } from 'react-window';
function VirtualList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>
{items[index].name}
</div>
);
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{Row}
</FixedSizeList>
);
}
Best for:
- 1,000+ items available in memory
- Fixed or predictable item sizes
- Tables, logs, data grids
- When you need instant access to any item
Performance:
- DOM nodes: ~20-50 (constant)
- Memory: All data in RAM
- Scrolling: Smooth, instant
Tradeoffs:
- Complex implementation
- Accessibility challenges
- SEO: Content not in HTML
- Poor for dynamic heights
Pagination
Divide data into discrete pages, load one page at a time.
function PaginatedList({ totalItems, itemsPerPage = 20 }) {
const [page, setPage] = useState(1);
const [items, setItems] = useState([]);
useEffect(() => {
fetch(`/api/items?page=${page}&limit=${itemsPerPage}`)
.then(r => r.json())
.then(setItems);
}, [page]);
return (
<div>
{items.map(item => <ItemCard key={item.id} {...item} />)}
<button onClick={() => setPage(p => p - 1)} disabled={page === 1}>
Previous
</button>
<span>Page {page}</span>
<button onClick={() => setPage(p => p + 1)}>
Next
</button>
</div>
);
}
Best for:
- Search results
- E-commerce product listings
- When users need to find specific items
- When you need stable URLs for each page
Performance:
- DOM nodes: Fixed per page
- Memory: Only current page
- Navigation: Requires page load
Tradeoffs:
- Slower browsing experience
- Good for SEO (each page indexable)
- Good for accessibility
- Users can share specific pages
Infinite Scroll
Load more items as user scrolls near the bottom.
function InfiniteScrollList() {
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const observerRef = useRef();
const lastItemRef = useCallback(node => {
if (observerRef.current) observerRef.current.disconnect();
observerRef.current = new IntersectionObserver(entries => {
if (entries[0].isIntersecting && hasMore) {
setPage(p => p + 1);
}
});
if (node) observerRef.current.observe(node);
}, [hasMore]);
useEffect(() => {
fetch(`/api/items?page=${page}`)
.then(r => r.json())
.then(newItems => {
setItems(prev => [...prev, ...newItems]);
setHasMore(newItems.length > 0);
});
}, [page]);
return (
<div>
{items.map((item, i) => {
if (i === items.length - 1) {
return <div ref={lastItemRef} key={item.id}>{item.name}</div>;
}
return <div key={item.id}>{item.name}</div>;
})}
{!hasMore && <div>No more items</div>}
</div>
);
}
Best for:
- Social media feeds
- News feeds
- Content discovery
- Mobile applications
- When users browse more than search
Performance:
- DOM nodes: Grows with scroll
- Memory: Grows with scroll (can cause issues)
- Scrolling: Seamless experience
Tradeoffs:
- Can't reach footer
- Hard to return to specific position
- Poor SEO (content loads dynamically)
- Growing memory usage
- Scrollbar becomes meaningless
Decision Matrix:
| Scenario | Best Choice | Why |
|---|---|---|
| 10,000 row data table | Virtual Scrolling | Need instant access, constant DOM |
| E-commerce catalog | Pagination | SEO, filtering, stable URLs |
| Social feed | Infinite Scroll | Discovery, engagement |
| Logs/Terminal | Virtual Scrolling | Performance with huge lists |
| Search results | Pagination | Users need to find things |
| Chat history | Virtual Scrolling | Need to jump to any message |
Hybrid Approach (Advanced):
function HybridList() {
const [mode, setMode] = useState('infinite'); // or 'paginated'
// Switch based on data size or user preference
useEffect(() => {
if (totalItems > 10000) {
setMode('virtual');
}
}, [totalItems]);
if (mode === 'virtual') return <VirtualScrollList />;
if (mode === 'infinite') return <InfiniteScrollList />;
return <PaginatedList />;
}
Question 9: Explain the performance impact of React Context and when to use alternatives
How Context Works:
const ThemeContext = createContext();
function App() {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Header />
<Main />
<Footer />
</ThemeContext.Provider>
);
}
function Button() {
const { theme } = useContext(ThemeContext);
return <button className={theme}>Click me</button>;
}
The Performance Problem:
When context value changes, ALL components that use useContext re-render, even if they only use a small part of the value.
Problem Example:
function App() {
const [theme, setTheme] = useState('dark');
const [user, setUser] = useState(null);
// ❌ This causes ALL consumers to re-render when either changes
const value = { theme, setTheme, user, setUser };
return (
<AppContext.Provider value={value}>
<ThemeButton /> {/* Re-renders on user change */}
<UserProfile /> {/* Re-renders on theme change */}
</AppContext.Provider>
);
}
Solution 1: Split Contexts
// ✅ Separate concerns
const ThemeContext = createContext();
const UserContext = createContext();
function App() {
const [theme, setTheme] = useState('dark');
const [user, setUser] = useState(null);
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<UserContext.Provider value={{ user, setUser }}>
<ThemeButton /> {/* Only re-renders on theme change */}
<UserProfile /> {/* Only re-renders on user change */}
</UserContext.Provider>
</ThemeContext.Provider>
);
}
Solution 2: Memoize Context Value
function App() {
const [theme, setTheme] = useState('dark');
// ✅ Prevent unnecessary re-renders of provider
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return (
<ThemeContext.Provider value={value}>
<Children />
</ThemeContext.Provider>
);
}
Solution 3: Use Selectors (Custom Hook)
// Not preventing re-renders, but making them cheap
const AppContext = createContext();
function useTheme() {
const context = useContext(AppContext);
return context.theme; // Only get what you need
}
function useUser() {
const context = useContext(AppContext);
return context.user;
}
// Components still re-render, but can be memoized
const ThemeButton = memo(function ThemeButton() {
const theme = useTheme();
return <button className={theme}>Click</button>;
});
When Context Performance Becomes a Problem:
- Context value changes frequently (multiple times per second)
- Many components (50+) consume the context
- Components consuming context are expensive to render
Alternatives to Consider:
1. Component Composition (Best for UI state)
// Instead of context:
function App() {
const [theme, setTheme] = useState('dark');
return (
<Layout theme={theme}>
<Header theme={theme} onThemeChange={setTheme} />
<Content theme={theme} />
</Layout>
);
}
// Use children to avoid prop drilling:
function App() {
const [theme, setTheme] = useState('dark');
return (
<Layout>
<Header>
<ThemeButton theme={theme} onChange={setTheme} />
</Header>
<Content theme={theme} />
</Layout>
);
}
2. State Management Libraries (For complex state)
// Zustand - only re-renders components that use changed state
import create from 'zustand';
const useStore = create((set) => ({
theme: 'dark',
user: null,
setTheme: (theme) => set({ theme }),
setUser: (user) => set({ user })
}));
function ThemeButton() {
// Only subscribes to theme
const theme = useStore((state) => state.theme);
const setTheme = useStore((state) => state.setTheme);
return <button onClick={() => setTheme('light')}>{theme}</button>;
}
function UserProfile() {
// Only subscribes to user
const user = useStore((state) => state.user);
return <div>{user?.name}</div>;
}
3. Jotai/Recoil (Atomic state)
import { atom, useAtom } from 'jotai';
const themeAtom = atom('dark');
const userAtom = atom(null);
function ThemeButton() {
const [theme, setTheme] = useAtom(themeAtom);
// Only re-renders when themeAtom changes
return <button onClick={() => setTheme('light')}>{theme}</button>;
}
Performance Comparison:
| Approach | Re-renders | Boilerplate | Use Case |
|---|---|---|---|
| Context | All consumers | Low | Simple, infrequent updates |
| Split Context | Only relevant | Medium | Multiple independent values |
| Zustand | Only subscribers | Low | Frequent updates, many consumers |
| Jotai | Only atom users | Medium | Fine-grained reactivity |
| Composition | Only prop recipients | None | Simple component trees |
When Context is Fine:
// ✅ Good use of context: Infrequent changes
const AuthContext = createContext();
function App() {
const [user, setUser] = useState(null);
// User only changes on login/logout
return (
<AuthContext.Provider value={{ user, setUser }}>
<AppContent />
</AuthContext.Provider>
);
}
// ✅ Good use of context: Theme that rarely changes
const ThemeContext = createContext();
When to Use Alternatives:
// ❌ Bad for context: High frequency updates
const MouseContext = createContext();
function App() {
const [pos, setPos] = useState({ x: 0, y: 0 });
// Updates on every mouse move!
useEffect(() => {
const handler = (e) => setPos({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', handler);
return () => window.removeEventListener('mousemove', handler);
}, []);
// This will re-render EVERY consumer on EVERY mouse move
return (
<MouseContext.Provider value={pos}>
<App />
</MouseContext.Provider>
);
}
// ✅ Better: Use Zustand or local state
Part 4: System Design for Frontends
Question 10: Design a frontend architecture for a real-time collaborative document editor (like Google Docs)
Requirements Analysis:
- Multiple users editing simultaneously
- Real-time updates (low latency)
- Conflict resolution
- Offline support
- History/undo functionality
- Scalability (large documents, many users)
High-Level Architecture:
┌─────────────┐
│ Browser │
│ │
│ ┌───────┐ │
│ │ Editor│ │
│ └───┬───┘ │
│ │ │
│ ┌───▼───┐ │
│ │ OT/ │ │
│ │ CRDT │ │
│ └───┬───┘ │
│ │ │
│ ┌───▼───┐ │
│ │ Local │ │
│ │ Store │ │
│ └───┬───┘ │
└──────┼──────┘
│ WebSocket
│
┌──────▼──────┐
│ WebSocket │
│ Gateway │
└──────┬──────┘
│
┌──────▼──────┐
│ Message │
│ Broker │
│ (Redis) │
└──────┬──────┘
│
┌──────▼──────┐
│ Document │
│ Service │
└──────┬──────┘
│
┌──────▼──────┐
│ Database │
│ (PostgreSQL)│
└─────────────┘
Component 1: Editor Layer (Frontend)
// Document editor component
function CollaborativeEditor({ documentId }) {
const [doc, setDoc] = useState(null);
const [users, setUsers] = useState([]);
const editorRef = useRef(null);
const wsRef = useRef(null);
useEffect(() => {
// Initialize CRDT document
const yDoc = new Y.Doc();
const yText = yDoc.getText('content');
// Connect to WebSocket
const ws = new WebSocket(`wss://api.example.com/doc/${documentId}`);
wsRef.current = ws;
// Sync provider for real-time updates
const provider = new WebsocketProvider(
'wss://api.example.com/doc',
documentId,
yDoc
);
// Handle incoming changes
yText.observe(event => {
// Update editor with remote changes
applyRemoteChanges(event.changes);
});
// Handle local changes
editorRef.current.on('change', (delta) => {
// Convert editor delta to CRDT operations
applyLocalChanges(yText, delta);
});
// Awareness for cursor positions
provider.awareness.on('change', changes => {
const states = Array.from(provider.awareness.getStates().values());
setUsers(states);
});
return () => {
provider.destroy();
ws.close();
};
}, [documentId]);
return (
<div className="editor-container">
<Toolbar />
<UserCursors users={users} />
<Editor ref={editorRef} />
</div>
);
}
Component 2: Conflict Resolution (CRDT Implementation)
// Using Yjs CRDT for conflict-free replicated data
class DocumentState {
constructor() {
this.yDoc = new Y.Doc();
this.yText = this.yDoc.getText('content');
this.undoManager = new Y.UndoManager(this.yText);
}
// Apply local operation
insert(index, text) {
this.yText.insert(index, text);
// CRDT automatically handles conflicts
// Each character has a unique ID (clientID + clock)
}
delete(index, length) {
this.yText.delete(index, length);
}
// Get current state
toString() {
return this.yText.toString();
}
// Sync with remote
applyUpdate(update) {
Y.applyUpdate(this.yDoc, update);
}
// Get updates to send
getUpdate() {
return Y.encodeStateAsUpdate(this.yDoc);
}
undo() {
this.undoManager.undo();
}
redo() {
this.undoManager.redo();
}
}
Alternative: Operational Transform (OT)
// OT requires server authority but has smaller payloads
class OTEngine {
constructor() {
this.revision = 0;
this.pendingOps = [];
}
// Transform operation against concurrent operations
transform(op1, op2) {
if (op1.type === 'insert' && op2.type === 'insert') {
if (op1.position < op2.position) {
return op2; // No change needed
} else if (op1.position > op2.position) {
return { ...op2, position: op2.position + op1.text.length };
} else {
// Same position - use client ID to break tie
return op1.clientId < op2.clientId ? op2 :
{ ...op2, position: op2.position + op1.text.length };
}
}
// Handle other operation type combinations...
}
// Apply operation with transformation
applyOperation(op) {
// Transform against pending operations
let transformedOp = op;
for (const pending of this.pendingOps) {
transformedOp = this.transform(transformedOp, pending);
}
// Apply to document
this.applyToDocument(transformedOp);
return transformedOp;
}
}
Component 3: Real-time Sync Layer
// WebSocket manager with reconnection
class DocumentSync {
constructor(documentId, onUpdate) {
this.documentId = documentId;
this.onUpdate = onUpdate;
this.ws = null;
this.reconnectAttempts = 0;
this.maxReconnectDelay = 30000;
this.pendingOperations = [];
this.connect();
}
connect() {
this.ws = new WebSocket(
`wss://api.example.com/doc/${this.documentId}`
);
this.ws.onopen = () => {
console.log('Connected');
this.reconnectAttempts = 0;
// Send any pending operations
this.flushPendingOperations();
// Request current state
this.ws.send(JSON.stringify({
type: 'sync-request',
revision: this.currentRevision
}));
};
this.ws.onmessage = (event) => {
const message = JSON.parse(event.data);
switch (message.type) {
case 'update':
this.handleUpdate(message);
break;
case 'sync-response':
this.handleSyncResponse(message);
break;
case 'acknowledgment':
this.handleAck(message);
break;
}
};
this.ws.onclose = () => {
console.log('Disconnected');
this.reconnect();
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
}
reconnect() {
const delay = Math.min(
1000 * Math.pow(2, this.reconnectAttempts),
this.maxReconnectDelay
);
this.reconnectAttempts++;
setTimeout(() => {
this.connect();
}, delay);
}
sendOperation(operation) {
const message = {
type: 'operation',
documentId: this.documentId,
operation,
clientId: this.clientId,
timestamp: Date.now()
};
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(message));
} else {
// Queue for later
this.pendingOperations.push(message);
}
}
flushPendingOperations() {
while (this.pendingOperations.length > 0) {
const op = this.pendingOperations.shift();
this.ws.send(JSON.stringify(op));
}
}
handleUpdate(message) {
this.onUpdate(message.operation);
}
}
Component 4: Offline Support
// IndexedDB for offline storage
class OfflineStorage {
constructor(documentId) {
this.documentId = documentId;
this.db = null;
this.init();
}
async init() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('CollabEditor', 1);
request.orupgrade = (event) => {
const db = event.target.result;
// Store for document content
if (!db.objectStoreNames.contains('documents')) {
db.createObjectStore('documents', { keyPath: 'id' });
}
// Store for pending operations
if (!db.objectStoreNames.contains('pendingOps')) {
const store = db.createObjectStore('pendingOps', {
keyPath: 'id',
autoIncrement: true
});
store.createIndex('documentId', 'documentId', { unique: false });
store.createIndex('timestamp', 'timestamp', { unique: false });
}
};
request.onsuccess = (event) => {
this.db = event.target.result;
resolve();
};
request.onerror = reject;
});
}
// Save document state
async saveDocument(content) {
const tx = this.db.transaction(['documents'], 'readwrite');
const store = tx.objectStore('documents');
await store.put({
id: this.documentId,
content,
lastModified: Date.now()
});
}
// Load document from cache
async loadDocument() {
const tx = this.db.transaction(['documents'], 'readonly');
const store = tx.objectStore('documents');
return new Promise((resolve, reject) => {
const request = store.get(this.documentId);
request.onsuccess = () => resolve(request.result);
request.onerror = reject;
});
}
// Queue operation for later sync
async queueOperation(operation) {
const tx = this.db.transaction(['pendingOps'], 'readwrite');
const store = tx.objectStore('pendingOps');
await store.add({
documentId: this.documentId,
operation,
timestamp: Date.now()
});
}
// Get all pending operations
async getPendingOperations() {
const tx = this.db.transaction(['pendingOps'], 'readonly');
const store = tx.objectStore('pendingOps');
const index = store.index('documentId');
return new Promise((resolve, reject) => {
const request = index.getAll(this.documentId);
request.onsuccess = () => resolve(request.result);
request.onerror = reject;
});
}
// Clear synced operations
async clearPendingOperations() {
const tx = this.db.transaction(['pendingOps'], 'readwrite');
const store = tx.objectStore('pendingOps');
const index = store.index('documentId');
const request = index.openCursor(this.documentId);
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
cursor.delete();
cursor.continue();
}
};
}
}
Component 5: Presence & Awareness
// Track cursor positions and user presence
class AwarenessManager {
constructor(provider) {
this.provider = provider;
this.awareness = provider.awareness;
this.localState = {
user: null,
cursor: null,
selection: null,
color: this.generateColor()
};
}
setUser(user) {
this.localState.user = user;
this.updateAwareness();
}
updateCursor(position) {
this.localState.cursor = position;
this.updateAwareness();
}
updateSelection(range) {
this.localState.selection = range;
this.updateAwareness();
}
updateAwareness() {
this.awareness.setLocalState(this.localState);
}
// Get all remote user states
getRemoteStates() {
const states = [];
this.awareness.getStates().forEach((state, clientId) => {
if (clientId !== this.awareness.clientID) {
states.push({ ...state, clientId });
}
});
return states;
}
generateColor() {
const colors = [
'#FF6B6B', '#4ECDC4', '#45B7D1',
'#FFA07A', '#98D8C8', '#F7DC6F'
];
return colors[Math.floor(Math.random() * colors.length)];
}
}
// Render remote cursors
function RemoteCursors({ awareness }) {
const [cursors, setCursors] = useState([]);
useEffect(() => {
const updateCursors = () => {
const states = Array.from(awareness.getStates().values())
.filter(state => state.cursor !== null);
setCursors(states);
};
awareness.on('change', updateCursors);
updateCursors();
return () => awareness.off('change', updateCursors);
}, [awareness]);
return (
<>
{cursors.map(state => (
<Cursor
key={state.clientId}
position={state.cursor}
color={state.color}
user={state.user}
/>
))}
</>
);
}
Component 6: Performance Optimizations
// Virtual scrolling for large documents
function LargeDocumentEditor({ doc }) {
const [visibleRange, setVisibleRange] = useState({ start: 0, end: 100 });
const containerRef = useRef();
// Only render visible lines
const visibleLines = useMemo(() => {
return doc.lines.slice(visibleRange.start, visibleRange.end);
}, [doc, visibleRange]);
// Update visible range on scroll
useEffect(() => {
const container = containerRef.current;
const handleScroll = throttle(() => {
const scrollTop = container.scrollTop;
const lineHeight = 20; // pixels
const viewportHeight = container.clientHeight;
const start = Math.floor(scrollTop / lineHeight) - 10; // buffer
const end = Math.ceil((scrollTop + viewportHeight) / lineHeight) + 10;
setVisibleRange({
start: Math.max(0, start),
end: Math.min(doc.lines.length, end)
});
}, 100);
container.addEventListener('scroll', handleScroll);
return () => container.removeEventListener('scroll', handleScroll);
}, [doc.lines.length]);
return (
<div ref={containerRef} style={{ height: '100vh', overflow: 'auto' }}>
<div style={{ height: doc.lines.length * 20 }}>
<div style={{ transform: `translateY(${visibleRange.start * 20}px)` }}>
{visibleLines.map((line, i) => (
<Line key={visibleRange.start + i} content={line} />
))}
</div>
</div>
</div>
);
}
// Debounce sync operations
function useDebouncedSync(syncFn, delay = 300) {
const timeoutRef = useRef();
const pendingOpsRef = useRef([]);
return useCallback((operation) => {
pendingOpsRef.current.push(operation);
clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => {
if (pendingOpsRef.current.length > 0) {
syncFn(pendingOpsRef.current);
pendingOpsRef.current = [];
}
}, delay);
}, [syncFn, delay]);
}
Backend Architecture (Brief Overview):
// WebSocket server (Node.js)
const WebSocket = require('ws');
const Redis = require('redis');
class CollaborationServer {
constructor() {
this.wss = new WebSocket.Server({ port: 8080 });
this.redis = Redis.createClient();
this.rooms = new Map(); // documentId -> Set of clients
this.wss.on('connection', (ws, req) => {
this.handleConnection(ws, req);
});
}
handleConnection(ws, req) {
const documentId = this.extractDocumentId(req.url);
// Add client to room
if (!this.rooms.has(documentId)) {
this.rooms.set(documentId, new Set());
}
this.rooms.get(documentId).add(ws);
// Handle messages
ws.on('message', async (data) => {
const message = JSON.parse(data);
switch (message.type) {
case 'operation':
await this.handleOperation(documentId, message, ws);
break;
case 'sync-request':
await this.handleSyncRequest(documentId, message, ws);
break;
}
});
// Cleanup on disconnect
ws.on('close', () => {
this.rooms.get(documentId).delete(ws);
});
}
async handleOperation(documentId, message, sender) {
// Save to Redis for persistence
await this.redis.lpush(
`doc:${documentId}:ops`,
JSON.stringify(message.operation)
);
// Broadcast to all clients in room except sender
const clients = this.rooms.get(documentId);
clients.forEach(client => {
if (client !== sender && client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify({
type: 'update',
operation: message.operation
}));
}
});
// Send acknowledgment
sender.send(JSON.stringify({
type: 'acknowledgment',
operationId: message.operation.id
}));
}
async handleSyncRequest(documentId, message, client) {
// Get operations since client's revision
const ops = await this.redis.lrange(
`doc:${documentId}:ops`,
message.revision,
-1
);
client.send(JSON.stringify({
type: 'sync-response',
operations: ops.map(op => JSON.parse(op))
}));
}
}
Scalability Considerations:
- Sharding by Document: Different documents on different servers
- Redis Pub/Sub: For broadcasting across multiple server instances
- Operation Compaction: Periodically compress operation history
- Snapshot Storage: Save full document state every N operations
// Load balancing with sticky sessions
// nginx config
upstream collaboration_servers {
ip_hash; # Sticky sessions based on IP
server server1:8080;
server server2:8080;
server server3:8080;
}
// Or use Redis for session sharing
class DistributedCollaboration {
constructor() {
this.pubsub = Redis.createClient();
this.subscriber = Redis.createClient();
// Subscribe to document channels
this.subscriber.on('message', (channel, message) => {
const documentId = channel.replace('doc:', '');
this.broadcastToLocalClients(documentId, message);
});
}
async handleOperation(documentId, operation) {
// Publish to Redis for other servers
await this.pubsub.publish(
`doc:${documentId}`,
JSON.stringify(operation)
);
// Also broadcast to local clients
this.broadcastToLocalClients(documentId, operation);
}
}
Key Design Decisions:
-
CRDT vs OT: CRDT chosen for:
- No server authority needed
- Better offline support
- Simpler conflict resolution
- Trade-off: Larger payload size
-
WebSocket vs HTTP polling:
- WebSocket for real-time, bi-directional
- Fallback to long-polling for old browsers
-
Operation-based vs State-based sync:
- Operation-based for efficiency
- Periodic state snapshots for recovery
-
Client-side vs Server-side rendering:
- Client-side for editor (needs interactivity)
- Server-side for initial load and SEO
Question 11: How would you design a frontend monitoring and observability system?
Requirements:
- Performance metrics (Core Web Vitals)
- Error tracking and reporting
- User session replay
- Real-time alerts
- Custom business metrics
- Privacy compliance
Architecture Overview:
// Core monitoring SDK
class FrontendMonitor {
constructor(config) {
this.config = config;
this.sessionId = this.generateSessionId();
this.userId = null;
this.queue = [];
this.flushInterval = 10000; // 10 seconds
this.initializeMonitoring();
}
initializeMonitoring() {
this.trackPerformance();
this.trackErrors();
this.trackUserInteractions();
this.trackNetworkRequests();
// Flush queue periodically
setInterval(() => this.flush(), this.flushInterval);
// Flush on page unload
window.addEventListener('beforeunload', () => this.flush(true));
}
// 1. Performance Monitoring
trackPerformance() {
// Core Web Vitals
this.trackLCP(); // Largest Contentful Paint
this.trackFID(); // First Input Delay
this.trackCLS(); // Cumulative Layout Shift
this.trackFCP(); // First Contentful Paint
this.trackTTFB(); // Time to First Byte
}
trackLCP() {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
this.record({
type: 'performance',
metric: 'LCP',
value: lastEntry.renderTime || lastEntry.loadTime,
timestamp: Date.now(),
url: window.location.href,
sessionId: this.sessionId
});
});
observer.observe({ entryTypes: ['largest-contentful-paint'] });
}
trackFID() {
const observer = new PerformanceObserver((list) => {
const entry = list.getEntries()[0];
this.record({
type: 'performance',
metric: 'FID',
value: entry.processingStart - entry.startTime,
timestamp: Date.now(),
sessionId: this.sessionId
});
});
observer.observe({ entryTypes: ['first-input'] });
}
trackCLS() {
let clsScore = 0;
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
clsScore += entry.value;
}
}
this.record({
type: 'performance',
metric: 'CLS',
value: clsScore,
timestamp: Date.now(),
sessionId: this.sessionId
});
});
observer.observe({ entryTypes: ['layout-shift'] });
}
// Navigation timing
trackPageLoad() {
window.addEventListener('load', () => {
setTimeout(() => {
const timing = performance.getEntriesByType('navigation')[0];
this.record({
type: 'performance',
metric: 'page-load',
data: {
dns: timing.domainLookupEnd - timing.domainLookupStart,
tcp: timing.connectEnd - timing.connectStart,
ttfb: timing.responseStart - timing.requestStart,
download: timing.responseEnd - timing.responseStart,
domInteractive: timing.domInteractive - timing.fetchStart,
domComplete: timing.domComplete - timing.fetchStart,
loadComplete: timing.loadEventEnd - timing.fetchStart
},
timestamp: Date.now(),
sessionId: this.sessionId
});
}, 0);
});
}
// 2. Error Tracking
trackErrors() {
// JavaScript errors
window.addEventListener('error', (event) => {
this.record({
type: 'error',
subtype: 'javascript',
message: event.message,
stack: event.error?.stack,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
timestamp: Date.now(),
sessionId: this.sessionId,
userId: this.userId,
url: window.location.href,
userAgent: navigator.userAgent
});
});
// Unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
this.record({
type: 'error',
subtype: 'unhandled-rejection',
message: event.reason?.message || String(event.reason),
stack: event.reason?.stack,
timestamp: Date.now(),
sessionId: this.sessionId
});
});
// React error boundaries (if using React)
this.setupReactErrorBoundary();
}
setupReactErrorBoundary() {
// Hook into React error boundary
if (window.React) {
const originalError = console.error;
console.error = (...args) => {
if (args[0]?.includes?.('React')) {
this.record({
type: 'error',
subtype: 'react',
message: args.join(' '),
timestamp: Date.now(),
sessionId: this.sessionId
});
}
originalError.apply(console, args);
};
}
}
// 3. User Interaction Tracking
trackUserInteractions() {
// Click tracking
document.addEventListener('click', (event) => {
const target = event.target;
this.record({
type: 'interaction',
subtype: 'click',
element: this.getElementIdentifier(target),
x: event.clientX,
y: event.clientY,
timestamp: Date.now(),
sessionId: this.sessionId
});
}, true);
// Form submissions
document.addEventListener('submit', (event) => {
const form = event.target;
this.record({
type: 'interaction',
subtype: 'form-submit',
formId: form.id || form.name,
action: form.action,
timestamp: Date.now(),
sessionId: this.sessionId
});
}, true);
// Page visibility
document.addEventListener('visibilitychange', () => {
this.record({
type: 'interaction',
subtype: 'visibility',
hidden: document.hidden,
timestamp: Date.now(),
sessionId: this.sessionId
});
});
}
// 4. Network Request Tracking
trackNetworkRequests() {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.initiatorType === 'fetch' || entry.initiatorType === 'xmlhttprequest') {
this.record({
type: 'network',
url: entry.name,
method: entry.initiatorType,
duration: entry.duration,
transferSize: entry.transferSize,
encodedBodySize: entry.encodedBodySize,
decodedBodySize: entry.decodedBodySize,
timestamp: Date.now(),
sessionId: this.sessionId
});
}
}
});
observer.observe({ entryTypes: ['resource'] });
// Intercept fetch
const originalFetch = window.fetch;
window.fetch = async (...args) => {
const startTime = performance.now();
try {
const response = await originalFetch(...args);
const duration = performance.now() - startTime;
this.record({
type: 'api-call',
url: args[0],
method: args[1]?.method || 'GET',
status: response.status,
duration,
timestamp: Date.now(),
sessionId: this.sessionId
});
return response;
} catch (error) {
const duration = performance.now() - startTime;
this.record({
type: 'api-error',
url: args[0],
method: args[1]?.method || 'GET',
error: error.message,
duration,
timestamp: Date.now(),
sessionId: this.sessionId
});
throw error;
}
};
}
// 5. Custom Metrics
recordCustomMetric(name, value, metadata = {}) {
this.record({
type: 'custom-metric',
name,
value,
metadata,
timestamp: Date.now(),
sessionId: this.sessionId,
userId: this.userId
});
}
// 6. Session Replay (Simplified)
startSessionReplay() {
const events = [];
// Capture DOM mutations
const observer = new MutationObserver((mutations) => {
const simplified = mutations.map(m => ({
type: m.type,
target: this.getElementIdentifier(m.target),
timestamp: Date.now()
}));
events.push(...simplified);
// Limit size
if (events.length > 1000) {
this.sendReplayChunk(events.splice(0, 500));
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeOldValue: false,
characterData: true
});
// Capture initial state
this.record({
type: 'replay-start',
html: this.sanitizeHTML(document.documentElement.outerHTML),
viewport: {
width: window.innerWidth,
height: window.innerHeight
},
sessionId: this.sessionId,
timestamp: Date.now()
});
}
// Helper methods
getElementIdentifier(element) {
if (element.id) return `#${element.id}`;
if (element.className) return `.${element.className.split(' ')[0]}`;
return element.tagName.toLowerCase();
}
sanitizeHTML(html) {
// Remove sensitive data (passwords, tokens, etc.)
return html
.replace(/password="[^"]*"/g, 'password="***"')
.replace(/token="[^"]*"/g, 'token="***"')
.replace(/<input[^>]*type="password"[^>]*>/g, '<input type="password" value="***">');
}
generateSessionId() {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
record(data) {
// Add common fields
const enriched = {
...data,
projectId: this.config.projectId,
environment: this.config.environment,
version: this.config.version,
page: {
url: window.location.href,
referrer: document.referrer,
title: document.title
},
device: {
userAgent: navigator.userAgent,
viewport: {
width: window.innerWidth,
height: window.innerHeight
},
connection: navigator.connection?.effectiveType
}
};
this.queue.push(enriched);
// Immediate flush for critical events
if (data.type === 'error' || data.type === 'api-error') {
this.flush();
}
}
async flush(sync = false) {
if (this.queue.length === 0) return;
const batch = this.queue.splice(0, this.queue.length);
const payload = {
batch,
timestamp: Date.now(),
sessionId: this.sessionId
};
if (sync) {
// Use sendBeacon for reliable delivery on page unload
navigator.sendBeacon(
this.config.endpoint,
JSON.stringify(payload)
);
} else {
try {
await fetch(this.config.endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
keepalive: true
});
} catch (error) {
// Failed to send - could retry or log locally
console.error('Failed to send monitoring data:', error);
}
}
}
// Public API
setUserId(userId) {
this.userId = userId;
}
addContext(key, value) {
this.record({
type: 'context',
key,
value,
timestamp: Date.now()
});
}
}
// Initialize monitoring
const monitor = new FrontendMonitor({
projectId: 'my-app',
environment: 'production',
version: '1.2.3',
endpoint: 'https://monitoring.example.com/ingest'
});
React Integration:
// Error Boundary with monitoring
class MonitoredErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// Send to monitoring system
monitor.record({
type: 'error',
subtype: 'react-boundary',
message: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
timestamp: Date.now()
});
}
render() {
if (this.state.hasError) {
return (
<div>
<h1>Something went wrong</h1>
<button onClick={() => this.setState({ hasError: false })}>
Try again
</button>
</div>
);
}
return this.props.children;
}
}
// Custom hook for tracking
function useMonitoring() {
return {
trackEvent: (name, properties) => {
monitor.recordCustomMetric(name, properties);
},
trackTiming: (name, duration) => {
monitor.recordCustomMetric(`timing.${name}`, duration);
},
setUser: (userId) => {
monitor.setUserId(userId);
}
};
}
// Track component render performance
function useRenderTracking(componentName) {
useEffect(() => {
const startTime = performance.now();
return () => {
const duration = performance.now() - startTime;
monitor.recordCustomMetric('component-render', duration, {
component: componentName
});
};
});
}
Backend Processing Pipeline:
// Ingest service (Node.js)
class MonitoringIngestService {
constructor() {
this.kafka = new Kafka({
brokers: ['kafka:9092']
});
this.producer = this.kafka.producer();
}
async handleIngest(req, res) {
const { batch } = req.body;
// Validate and enrich
const enriched = batch.map(event => ({
...event,
receivedAt: Date.now(),
ip: req.ip,
geo: this.lookupGeo(req.ip)
}));
// Send to Kafka for processing
await this.producer.send({
topic: 'frontend-events',
messages: enriched.map(event => ({
key: event.sessionId,
value: JSON.stringify(event)
}))
});
res.status(200).json({ success: true });
}
lookupGeo(ip) {
// GeoIP lookup
return { country: 'US', city: 'New York' };
}
}
// Processing worker
class EventProcessor {
constructor() {
this.kafka = new Kafka({ brokers: ['kafka:9092'] });
this.consumer = this.kafka.consumer({ groupId: 'event-processor' });
this.timescale = new TimescaleDB();
this.redis = new Redis();
}
async start() {
await this.consumer.subscribe({ topic: 'frontend-events' });
await this.consumer.run({
eachMessage: async ({ message }) => {
const event = JSON.parse(message.value);
// Route by type
switch (event.type) {
case 'error':
await this.processError(event);
break;
case 'performance':
await this.processPerformance(event);
break;
case 'interaction':
await this.processInteraction(event);
break;
}
}
});
}
async processError(event) {
// Store in TimescaleDB
await this.timescale.query(`
INSERT INTO errors (
session_id, user_id, message, stack, url, timestamp
) VALUES ($1, $2, $3, $4, $5, $6)
`, [
event.sessionId,
event.userId,
event.message,
event.stack,
event.url,
event.timestamp
]);
// Check for alerts
const errorCount = await this.redis.incr(
`errors:${event.message}:${Date.now() / 60000}`
);
if (errorCount > 10) {
await this.sendAlert({
type: 'error-spike',
message: `Error spike detected: ${event.message}`,
count: errorCount
});
}
// Group similar errors
const errorHash = this.hashError(event);
await this.redis.hincrby('error-groups', errorHash, 1);
}
async processPerformance(event) {
// Store metric
await this.timescale.query(`
INSERT INTO performance_metrics (
session_id, metric, value, url, timestamp
) VALUES ($1, $2, $3, $4, $5)
`, [
event.sessionId,
event.metric,
event.value,
event.url,
event.timestamp
]);
// Update real-time aggregates
await this.redis.zadd(
`perf:${event.metric}:recent`,
event.timestamp,
JSON.stringify({ value: event.value, url: event.url })
);
// Check thresholds
if (event.metric === 'LCP' && event.value > 2500) {
await this.sendAlert({
type: 'performance-degradation',
metric: 'LCP',
value: event.value,
threshold: 2500,
url: event.url
});
}
}
hashError(event) {
const crypto = require('crypto');
return crypto
.createHash('md5')
.update(event.message + event.stack?.split('\n')[0])
.digest('hex');
}
async sendAlert(alert) {
// Send to alerting system (PagerDuty, Slack, etc.)
await fetch('https://hooks.slack.com/services/XXX', {
method: 'POST',
body: JSON.stringify({
text: `⚠️ ${alert.message}`
})
});
}
}
Dashboard & Query API:
// Query service
class MonitoringQueryService {
constructor() {
this.timescale = new TimescaleDB();
}
// Get performance metrics over time
async getPerformanceMetrics(metric, startTime, endTime) {
const results = await this.timescale.query(`
SELECT
time_bucket('5 minutes', timestamp) AS bucket,
percentile_cont(0.5) WITHIN GROUP (ORDER BY value) AS p50,
percentile_cont(0.75) WITHIN GROUP (ORDER BY value) AS p75,
percentile_cont(0.95) WITHIN GROUP (ORDER BY value) AS p95,
percentile_cont(0.99) WITHIN GROUP (ORDER BY value) AS p99
FROM performance_metrics
WHERE metric = $1
AND timestamp >= $2
AND timestamp <= $3
GROUP BY bucket
ORDER BY bucket
`, [metric, startTime, endTime]);
return results.rows;
}
// Get error rate
async getErrorRate(startTime, endTime) {
const results = await this.timescale.query(`
SELECT
time_bucket('5 minutes', timestamp) AS bucket,
COUNT(*) as error_count
FROM errors
WHERE timestamp >= $1 AND timestamp <= $2
GROUP BY bucket
ORDER BY bucket
`, [startTime, endTime]);
return results.rows;
}
// Get top errors
async getTopErrors(limit = 10) {
const results = await this.timescale.query(`
SELECT
message,
COUNT(*) as count,
COUNT(DISTINCT session_id) as affected_sessions,
MAX(timestamp) as last_seen
FROM errors
WHERE timestamp > NOW() - INTERVAL '24 hours'
GROUP BY message
ORDER BY count DESC
LIMIT $1
`, [limit]);
return results.rows;
}
// User journey analysis
async getUserJourney(sessionId) {
const results = await this.timescale.query(`
SELECT type, subtype, url, timestamp, data
FROM events
WHERE session_id = $1
ORDER BY timestamp
`, [sessionId]);
return results.rows;
}
}
Privacy & Compliance:
// Data sanitization for GDPR/CCPA
class PrivacyManager {
sanitizeEvent(event) {
// Remove PII
const sanitized = { ...event };
// Mask email addresses
if (sanitized.message) {
sanitized.message = sanitized.message.replace(
/[\w.-]+@[\w.-]+\.\w+/g,
'***@***.***'
);
}
// Remove query parameters with sensitive data
if (sanitized.url) {
const url = new URL(sanitized.url);
const sensitiveParams = ['token', 'key', 'password', 'email'];
sensitiveParams.forEach(param => {
if (url.searchParams.has(param)) {
url.searchParams.set(param, '***');
}
});
sanitized.url = url.toString();
}
// Hash IP addresses
if (sanitized.ip) {
sanitized.ip = this.hashIP(sanitized.ip);
}
return sanitized;
}
hashIP(ip) {
const crypto = require('crypto');
return crypto.createHash('sha256').update(ip).digest('hex').substr(0, 16);
}
// GDPR: Delete user data
async deleteUserData(userId) {
await this.timescale.query(`
DELETE FROM events WHERE user_id = $1
`, [userId]);
await this.timescale.query(`
DELETE FROM errors WHERE user_id = $1
`, [userId]);
}
// GDPR: Export user data
async exportUserData(userId) {
const events = await this.timescale.query(`
SELECT * FROM events WHERE user_id = $1
`, [userId]);
return events.rows;
}
}
Key Design Decisions:
- Client-side sampling: Only send 10% of performance metrics to reduce load
- Batching: Group events to reduce network requests
- Priority queuing: Errors flush immediately, metrics batch
- Privacy-first: Sanitize before sending, hash IPs, mask PII
- Time-series DB: TimescaleDB for efficient metric storage
- Real-time processing: Kafka for stream processing
- Retention policies: 90 days raw data, 1 year aggregates
Question 12: Design a micro-frontend architecture for a large e-commerce platform
Requirements:
- Multiple teams working independently
- Different tech stacks per team
- Shared design system
- Performance (no massive bundle)
- Gradual migration from monolith
- SEO-friendly
Architecture Patterns:
Pattern 1: Build-Time Integration (Not Recommended)
// ❌ Compile-time integration - loses team autonomy
import ProductList from '@team-catalog/product-list';
import Checkout from '@team-checkout/checkout';
function App() {
return (
<>
<ProductList />
<Checkout />
</>
);
}
Problems:
- All teams must use same framework
- Coordinated deployments
- Version conflicts
Pattern 2: Run-Time Integration via Module Federation (Recommended)
// Webpack Module Federation
// Host application (shell)
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
productCatalog: 'productCatalog@http://localhost:3001/remoteEntry.js',
checkout: 'checkout@http://localhost:3002/remoteEntry.js',
userProfile: 'userProfile@http://localhost:3003/remoteEntry.js'
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' }
}
})
]
};
// Shell application
import React, { lazy, Suspense } from 'react';
const ProductCatalog = lazy(() => import('productCatalog/ProductList'));
const Checkout = lazy(() => import('checkout/CheckoutFlow'));
const UserProfile = lazy(() => import('userProfile/ProfilePage'));
function App() {
return (
<BrowserRouter>
<Layout>
<Suspense fallback={<Spinner />}>
<Routes>
<Route path="/products" element={<ProductCatalog />} />
<Route path="/checkout" element={<Checkout />} />
<Route path="/profile" element={<UserProfile />} />
</Routes>
</Suspense>
</Layout>
</BrowserRouter>
);
}
// Remote application (Product Catalog)
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'productCatalog',
filename: 'remoteEntry.js',
exposes: {
'./ProductList': './src/components/ProductList',
'./ProductDetail': './src/components/ProductDetail'
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true }
}
})
]
};
Benefits:
- Independent deployments
- Load on demand
- Share dependencies
- Can use same framework
Drawbacks:
- Still tied to webpack
- Shared dependencies must match versions
- Complex configuration
Pattern 3: Web Components (Framework Agnostic)
// Product Catalog (React)
import React from 'react';
import ReactDOM from 'react-dom/client';
class ProductCatalogElement extends HTMLElement {
connectedCallback() {
const mountPoint = document.createElement('div');
this.attachShadow({ mode: 'open' }).appendChild(mountPoint);
const root = ReactDOM.createRoot(mountPoint);
root.render(<ProductCatalog
apiUrl={this.getAttribute('api-url')}
onProductClick={this.handleProductClick}
/>);
}
handleProductClick = (productId) => {
this.dispatchEvent(new CustomEvent('product-selected', {
detail: { productId },
bubbles: true
}));
};
}
customElements.define('product-catalog', ProductCatalogElement);
// Checkout (Vue)
import { createApp } from 'vue';
import CheckoutApp from './Checkout.vue';
class CheckoutElement extends HTMLElement {
connectedCallback() {
const mountPoint = document.createElement('div');
this.attachShadow({ mode: 'open' }).appendChild(mountPoint);
createApp(CheckoutApp, {
cartId: this.getAttribute('cart-id')
}).mount(mountPoint);
}
}
customElements.define('checkout-flow', CheckoutElement);
// Host application (any framework or vanilla)
<html>
<body>
<div id="app">
<navigation-bar></navigation-bar>
<product-catalog
api-url="https://api.example.com/products">
</product-catalog>
<checkout-flow
cart-id="abc123">
</checkout-flow>
</div>
<script src="https://cdn.example.com/product-catalog/1.2.3/bundle.js"></script>
<script src="https://cdn.example.com/checkout/2.0.1/bundle.js"></script>
</body>
</html>
Benefits:
- True framework independence
- Shadow DOM encapsulation
- Standard web platform
Drawbacks:
- No shared React context
- Styling isolation (good and bad)
- Each micro-frontend loads full framework
Pattern 4: iFrame (Simple but Limited)
// Host
function App() {
return (
<div>
<iframe
src="https://products.example.com"
style={{ border: 'none', width: '100%', height: '600px' }}
sandbox="allow-scripts allow-same-origin"
/>
</div>
);
}
// Communication via postMessage
window.parent.postMessage({
type: 'product-selected',
productId: '123'
}, '*');
Benefits:
- Complete isolation
- Simple to implement
- Zero version conflicts
Drawbacks:
- Performance overhead
- SEO challenges
- Complex communication
- Awkward routing
Recommended Hybrid Architecture:
┌─────────────────────────────────────────────┐
│ Shell Application (Host) │
│ - Routing │
│ - Authentication │
│ - Shared UI components │
│ - Global state management │
└─────────────────┬───────────────────────────┘
│
┌─────────┼─────────┐
│ │ │
┌───────▼──┐ ┌───▼─────┐ ┌─▼────────┐
│ Product │ │Checkout │ │ User │
│ Catalog │ │ Flow │ │ Profile │
│ (React) │ │ (Vue) │ │ (Svelte) │
└──────────┘ └─────────┘ └──────────┘
Implementation:
// 1. Shell Application
import { createBrowserRouter } from 'react-router-dom';
const router = createBrowserRouter([
{
path: '/',
element: <RootLayout />,
children: [
{
path: 'products/*',
lazy: () => loadMicroFrontend('productCatalog', '/remoteEntry.js')
},
{
path: 'checkout/*',
lazy: () => loadMicroFrontend('checkout', '/remoteEntry.js')
},
{
path: 'profile/*',
lazy: () => loadMicroFrontend('userProfile', '/remoteEntry.js')
}
]
}
]);
// Dynamic micro-frontend loader
async function loadMicroFrontend(name, entry) {
// Load remote entry
await loadScript(`https://${name}.example.com${entry}`);
// Get the container
const container = window[name];
await container.init(__webpack_share_scopes__.default);
// Get the module
const factory = await container.get('./App');
const Module = factory();
return {
Component: Module.default
};
}
// 2. Shared Event Bus
class MicroFrontendEventBus {
constructor() {
this.listeners = new Map();
}
on(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event).push(callback);
}
off(event, callback) {
const callbacks = this.listeners.get(event);
if (callbacks) {
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}
}
emit(event, data) {
const callbacks = this.listeners.get(event);
if (callbacks) {
callbacks.forEach(callback => callback(data));
}
}
}
// Global event bus
window.microFrontendBus = new MicroFrontendEventBus();
// Usage in micro-frontends
// Product Catalog
window.microFrontendBus.emit('product:added-to-cart', {
productId: '123',
quantity: 1
});
// Checkout
window.microFrontendBus.on('product:added-to-cart', (data) => {
updateCart(data);
});
// 3. Shared State Management
class SharedStateManager {
constructor() {
this.state = {};
this.subscribers = new Map();
}
setState(key, value) {
this.state[key] = value;
this.notify(key, value);
}
getState(key) {
return this.state[key];
}
subscribe(key, callback) {
if (!this.subscribers.has(key)) {
this.subscribers.set(key, []);
}
this.subscribers.get(key).push(callback);
// Return unsubscribe function
return () => {
const callbacks = this.subscribers.get(key);
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
};
}
notify(key, value) {
const callbacks = this.subscribers.get(key);
if (callbacks) {
callbacks.forEach(callback => callback(value));
}
}
}
window.sharedState = new SharedStateManager();
// Usage
// Set user in shell
window.sharedState.setState('user', { id: '123', name: 'John' });
// Subscribe in micro-frontend
const unsubscribe = window.sharedState.subscribe('user', (user) => {
console.log('User updated:', user);
});
// 4. Shared Design System
// Published as npm package and imported by all micro-frontends
import { Button, Input, Card } from '@company/design-system';
// Or loaded as web components
<ds-button variant="primary">Click me</ds-button>
// 5. API Gateway Pattern
class APIClient {
constructor(baseURL) {
this.baseURL = baseURL;
this.token = null;
}
setToken(token) {
this.token = token;
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const headers = {
'Content-Type': 'application/json',
...(this.token && { 'Authorization': `Bearer ${this.token}` }),
...options.headers
};
const response = await fetch(url, {
...options,
headers
});
if (!response.ok) {
throw new Error(`API Error: ${response.statusText}`);
}
return response.json();
}
}
// Shared API client
window.apiClient = new APIClient('https://api.example.com');
// Usage in micro-frontends
const products = await window.apiClient.request('/products');
Performance Optimizations:
// 1. Lazy loading with preloading
function preloadMicroFrontend(name) {
const link = document.createElement('link');
link.rel = 'prefetch';
link.href = `https://${name}.example.com/remoteEntry.js`;
document.head.appendChild(link);
}
// Preload on hover
<Link
to="/products"
onMouseEnter={() => preloadMicroFrontend('productCatalog')}
>
Products
</Link>
// 2. Service Worker for caching
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('remoteEntry.js')) {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request).then(fetchResponse => {
return caches.open('micro-frontends-v1').then(cache => {
cache.put(event.request, fetchResponse.clone());
return fetchResponse;
});
});
})
);
}
});
// 3. Code splitting within micro-frontends
// Product Catalog micro-frontend
const ProductDetail = lazy(() => import('./ProductDetail'));
const ProductList = lazy(() => import('./ProductList'));
// 4. Shared chunk optimization
// webpack config
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'vendor',
chunks: 'all'
}
}
}
}
Deployment Strategy:
// Version management
const MICRO_FRONTEND_VERSIONS = {
productCatalog: {
stable: '1.5.2',
canary: '1.6.0-beta.1'
},
checkout: {
stable: '2.1.0',
canary: '2.2.0-rc.1'
}
};
// Feature flags for gradual rollout
function getMicroFrontendVersion(name, userId) {
const versions = MICRO_FRONTEND_VERSIONS[name];
// 10% canary deployment
if (isInCanaryGroup(userId, 0.1)) {
return versions.canary;
}
return versions.stable;
}
function loadMicroFrontend(name, userId) {
const version = getMicroFrontendVersion(name, userId);
return loadScript(`https://${name}.example.com/${version}/remoteEntry.js`);
}
Testing Strategy:
// 1. Contract testing between shell and micro-frontends
// micro-frontend-contracts.test.js
describe('Product Catalog Contract', () => {
it('should expose ProductList component', async () => {
const module = await import('productCatalog/ProductList');
expect(module.default).toBeDefined();
expect(typeof module.default).toBe('function');
});
it('should emit product-selected event', () => {
const callback = jest.fn();
window.microFrontendBus.on('product:selected', callback);
// Trigger event
window.microFrontendBus.emit('product:selected', { id: '123' });
expect(callback).toHaveBeenCalledWith({ id: '123' });
});
});
// 2. Integration testing
describe('Shell Integration', () => {
it('should load and render micro-frontend', async () => {
render(<Shell />);
await waitFor(() => {
expect(screen.getByTestId('product-catalog')).toBeInTheDocument();
});
});
});
// 3. E2E testing across boundaries
// cypress/integration/checkout-flow.spec.js
describe('Checkout Flow', () => {
it('should complete purchase from product to payment', () => {
cy.visit('/products');
cy.get('[data-testid="product-item"]').first().click();
cy.get('[data-testid="add-to-cart"]').click();
cy.get('[data-testid="checkout-button"]').click();
// Now in checkout micro-frontend
cy.get('[data-testid="payment-form"]').should('be.visible');
cy.get('[data-testid="complete-purchase"]').click();
cy.url().should('include', '/confirmation');
});
});
Monitoring & Observability:
// Track micro-frontend loading performance
window.microFrontendBus.on('mfe:loaded', (data) => {
window.monitor.recordCustomMetric('mfe-load-time', data.duration, {
name: data.name,
version: data.version
});
});
// Track errors per micro-frontend
window.addEventListener('error', (event) => {
const mfeName = identifyMicroFrontend(event.filename);
window.monitor.record({
type: 'error',
microFrontend: mfeName,
message: event.message,
stack: event.error?.stack
});
});
function identifyMicroFrontend(filename) {
if (filename.includes('productCatalog')) return 'productCatalog';
if (filename.includes('checkout')) return 'checkout';
if (filename.includes('userProfile')) return 'userProfile';
return 'shell';
}
Key Design Decisions Summary:
- Module Federation for same framework: Best balance of performance and autonomy
- Web Components for different frameworks: When teams need total independence
- Event bus for communication: Loose coupling between micro-frontends
- Shared state manager: For truly global state (auth, user)
- Design system as shared dependency: Ensures UI consistency
- Version management: Canary deployments, gradual rollouts
- API Gateway: Centralized authentication and routing
- Contract testing: Prevent breaking changes
Part 5: Advanced Questions
Question 13: Explain the differences between memoization strategies: React.memo, useMemo, useCallback, and manual memoization
React.memo (Component Memoization)
// Without memo - re-renders on every parent render
function ExpensiveChild({ data, onAction }) {
console.log('Rendering ExpensiveChild');
return <div>{expensiveCalculation(data)}</div>;
}
// With memo - only re-renders when props change
const MemoizedChild = React.memo(ExpensiveChild);
// Custom comparison function
const MemoizedChildCustom = React.memo(
ExpensiveChild,
(prevProps, nextProps) => {
// Return true if props are equal (skip re-render)
return prevProps.data.id === nextProps.data.id;
}
);
When React.memo helps:
- Component is expensive to render
- Component receives same props frequently
- Component is a pure function of its props
When React.memo doesn't help:
- Props change every render (new objects/functions)
- Component is cheap to render
- Component has children that change
Common Pitfall:
function Parent() {
const [count, setCount] = useState(0);
// ❌ New object every render - memo useless
const data = { value: 'hello' };
// ❌ New function every render - memo useless
const handleClick = () => console.log('clicked');
return <MemoizedChild data={data} onClick={handleClick} />;
}
// ✅ Fix with useMemo and useCallback
function Parent() {
const [count, setCount] = useState(0);
const data = useMemo(() => ({ value: 'hello' }), []);
const handleClick = useCallback(() => console.log('clicked'), []);
return <MemoizedChild data={data} onClick={handleClick} />;
}
useMemo (Value Memoization)
function ProductList({ products, filter }) {
// ❌ Expensive calculation runs every render
const filtered = products
.filter(p => p.category === filter)
.sort((a, b) => b.rating - a.rating);
// ✅ Only recalculates when dependencies change
const filtered = useMemo(() => {
return products
.filter(p => p.category === filter)
.sort((a, b) => b.rating - a.rating);
}, [products, filter]);
return <div>{filtered.map(p => <Product key={p.id} {...p} />)}</div>;
}
When useMemo helps:
- Expensive calculations (filtering, sorting large arrays)
- Creating objects/arrays passed to memoized children
- Avoiding unnecessary re-renders due to referential inequality
When useMemo doesn't help:
- Simple calculations (addition, string concatenation)
- Values that change every render anyway
- Over-optimization without measurement
Cost-Benefit Analysis:
// ❌ Premature optimization
const sum = useMemo(() => a + b, [a, b]);
// Cost of useMemo > cost of calculation
// ✅ Worth it
const sortedAndFiltered = useMemo(() => {
return data
.filter(item => item.active)
.sort((a, b) => a.value - b.value)
.map(item => ({ ...item, computed: heavyFunction(item) }));
}, [data]);
useCallback (Function Memoization)
function ProductGrid({ products }) {
const [selectedId, setSelectedId] = useState(null);
// ❌ New function every render
const handleSelect = (id) => {
setSelectedId(id);
analytics.track('product-selected', { id });
};
// ✅ Same function reference across renders
const handleSelect = useCallback((id) => {
setSelectedId(id);
analytics.track('product-selected', { id });
}, []); // Empty deps - function never changes
// ✅ Updates when selectedId changes
const handleToggle = useCallback((id) => {
setSelectedId(prev => prev === id ? null : id);
}, []); // Can use functional update, so no dep needed
return products.map(p => (
<MemoizedProduct
key={p.id}
product={p}
onSelect={handleSelect}
/>
));
}
When useCallback helps:
- Passing callbacks to memoized children
- Callbacks used as dependencies in useEffect
- Preventing child re-renders
When useCallback doesn't help:
- Callbacks not passed to memoized components
- Callbacks change frequently anyway
- Simple event handlers
Common Pattern:
function SearchBar() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
// ✅ Callback is dependency in useEffect
const fetchResults = useCallback(async (searchQuery) => {
const data = await api.search(searchQuery);
setResults(data);
}, []);
useEffect(() => {
if (query) {
fetchResults(query);
}
}, [query, fetchResults]); // fetchResults won't cause re-fetch
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}
Manual Memoization (useRef + Custom Logic)
function ManualMemoization() {
const cache = useRef(new Map());
function expensiveCalculation(input) {
// Check cache
if (cache.current.has(input)) {
console.log('Cache hit');
return cache.current.get(input);
}
// Compute
console.log('Computing...');
const result = /* expensive computation */ input * 2;
// Store in cache
cache.current.set(input, result);
return result;
}
return <div>{expensiveCalculation(props.value)}</div>;
}
When manual memoization helps:
- Complex caching logic
- LRU cache needed
- Caching across multiple renders with custom eviction
- Fine-grained control over cache invalidation
Advanced: LRU Cache Implementation
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.cache = new Map();
}
get(key) {
if (!this.cache.has(key)) return undefined;
// Move to end (most recently used)
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
set(key, value) {
// Remove if exists
if (this.cache.has(key)) {
this.cache.delete(key);
}
// Add to end
this.cache.set(key, value);
// Evict oldest if over capacity
if (this.cache.size > this.capacity) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
}
}
function ComponentWithLRU() {
const cache = useRef(new LRUCache(100));
function compute(input) {
let result = cache.current.get(input);
if (result === undefined) {
result = expensiveComputation(input);
cache.current.set(input, result);
}
return result;
}
return <div>{compute(props.data)}</div>;
}
Comparison Table:
| Technique | Memoizes | Use Case | Performance Cost |
|---|---|---|---|
| React.memo | Component | Prevent re-renders | Props comparison |
| useMemo | Value | Expensive calculations | Dependency check |
| useCallback | Function | Stable callback references | Dependency check |
| Manual (useRef) | Custom | Complex caching logic | Your implementation |
Decision Tree:
Do you need to prevent a component re-render?
├─ Yes → Use React.memo
│ └─ Are props objects/functions?
│ └─ Yes → Also use useMemo/useCallback for props
└─ No → Is it an expensive calculation?
├─ Yes → Use useMemo
└─ No → Is it a function passed to memoized child?
├─ Yes → Use useCallback
└─ No → Don't memoize
Question 14: How do you handle race conditions in React applications?
Problem: Multiple Async Operations
// ❌ Race condition: Last request wins, not latest request
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
fetch(`/api/search?q=${query}`)
.then(r => r.json())
.then(data => setResults(data));
// If query changes quickly, old responses can overwrite new ones
}, [query]);
return <div>{results.map(r => <Result key={r.id} {...r} />)}</div>;
}
Solution 1: Cleanup Flag
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
let cancelled = false;
fetch(`/api/search?q=${query}`)
.then(r => r.json())
.then(data => {
if (!cancelled) {
setResults(data);
}
});
return () => {
cancelled = true;
};
}, [query]);
return <div>{results.map(r => <Result key={r.id} {...r} />)}</div>;
}
Solution 2: AbortController
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
const controller = new AbortController();
fetch(`/api/search?q=${query}`, {
signal: controller.signal
})
.then(r => r.json())
.then(data => setResults(data))
.catch(err => {
if (err.name !== 'AbortError') {
console.error('Search failed:', err);
}
});
return () => {
controller.abort();
};
}, [query]);
return <div>{results.map(r => <Result key={r.id} {...r} />)}</div>;
}
Solution 3: Request ID/Version
function SearchResults({ query }) {
const [results, setResults] = useState([]);
const requestIdRef = useRef(0);
useEffect(() => {
const requestId = ++requestIdRef.current;
fetch(`/api/search?q=${query}`)
.then(r => r.json())
.then(data => {
// Only update if this is still the latest request
if (requestId === requestIdRef.current) {
setResults(data);
}
});
}, [query]);
return <div>{results.map(r => <Result key={r.id} {...r} />)}</div>;
}
Solution 4: Custom Hook with Race Condition Prevention
function useAsyncData(fetchFn, deps) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
const controller = new AbortController();
async function fetchData() {
setLoading(true);
setError(null);
try {
const result = await fetchFn(controller.signal);
if (!cancelled) {
setData(result);
setLoading(false);
}
} catch (err) {
if (!cancelled && err.name !== 'AbortError') {
setError(err);
setLoading(false);
}
}
}
fetchData();
return () => {
cancelled = true;
controller.abort();
};
}, deps);
return { data, loading, error };
}
// Usage
function SearchResults({ query }) {
const { data, loading, error } = useAsyncData(
async (signal) => {
const response = await fetch(`/api/search?q=${query}`, { signal });
return response.json();
},
[query]
);
if (loading) return <Spinner />;
if (error) return <Error message={error.message} />;
return <div>{data.map(r => <Result key={r.id} {...r} />)}</div>;
}
Race Condition in Optimistic Updates:
function TodoItem({ todo }) {
const [isCompleted, setIsCompleted] = useState(todo.completed);
// ❌ Race condition with rapid clicks
async function toggleComplete() {
setIsCompleted(!isCompleted); // Optimistic update
try {
await api.updateTodo(todo.id, { completed: !isCompleted });
} catch (error) {
setIsCompleted(isCompleted); // Revert
}
}
// ✅ Fixed with request tracking
const pendingRequestRef = useRef(null);
async function toggleComplete() {
// Cancel previous request
if (pendingRequestRef.current) {
pendingRequestRef.current.abort();
}
const controller = new AbortController();
pendingRequestRef.current = controller;
const newState = !isCompleted;
setIsCompleted(newState);
try {
await api.updateTodo(
todo.id,
{ completed: newState },
{ signal: controller.signal }
);
pendingRequestRef.current = null;
} catch (error) {
if (error.name !== 'AbortError') {
setIsCompleted(!newState); // Revert
}
}
}
return (
<div>
<input
type="checkbox"
checked={isCompleted}
onChange={toggleComplete}
/>
{todo.title}
</div>
);
}
Race Conditions in Parallel Requests:
// Problem: Need data from multiple sources
function UserDashboard({ userId }) {
const [profile, setProfile] = useState(null);
const [orders, setOrders] = useState([]);
const [reviews, setReviews] = useState([]);
useEffect(() => {
// ❌ Three separate effects = three race conditions
fetch(`/api/users/${userId}`).then(r => r.json()).then(setProfile);
fetch(`/api/orders/${userId}`).then(r => r.json()).then(setOrders);
fetch(`/api/reviews/${userId}`).then(r => r.json()).then(setReviews);
}, [userId]);
// ✅ Solution: Single effect with Promise.all
useEffect(() => {
let cancelled = false;
Promise.all([
fetch(`/api/users/${userId}`).then(r => r.json()),
fetch(`/api/orders/${userId}`).then(r => r.json()),
fetch(`/api/reviews/${userId}`).then(r => r.json())
]).then(([profileData, ordersData, reviewsData]) => {
if (!cancelled) {
setProfile(profileData);
setOrders(ordersData);
setReviews(reviewsData);
}
});
return () => {
cancelled = true;
};
}, [userId]);
}
Advanced: Debouncing to Prevent Race Conditions
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return debouncedValue;
}
function SearchResults({ query }) {
const debouncedQuery = useDebounce(query, 300);
const [results, setResults] = useState([]);
useEffect(() => {
if (!debouncedQuery) return;
const controller = new AbortController();
fetch(`/api/search?q=${debouncedQuery}`, {
signal: controller.signal
})
.then(r => r.json())
.then(setResults)
.catch(err => {
if (err.name !== 'AbortError') {
console.error(err);
}
});
return () => controller.abort();
}, [debouncedQuery]);
return <div>{results.map(r => <Result key={r.id} {...r} />)}</div>;
}
Conclusion
These interview questions cover the cutting edge of frontend development in 2025:
- React Hooks - Deep understanding of closures, memoization, and rules
- Server Components - New paradigm shifting SSR and data fetching
- Performance - Real tradeoffs in code splitting, scrolling, and Context
- System Design - Architecting complex systems like collaborative editors
- Micro-frontends - Building scalable applications with multiple teams
- Observability - Production monitoring and debugging
- Race Conditions - Handling async complexity correctly
Key Takeaways:
- Understand not just the "what" but the "why" and "when"
- Performance optimization requires measurement, not guessing
- Every architectural decision has tradeoffs
- Real-world complexity requires systematic approaches
- Testing and monitoring are essential, not optional
Further Study:
- React documentation on Server Components
- Web performance APIs (PerformanceObserver, etc.)
- Distributed systems concepts (CRDT, OT)
- Browser internals and rendering pipeline
- Modern JavaScript features (Temporal, Pattern Matching)
Remember: The best engineers don't just know the answers—they understand the tradeoffs and can explain their reasoning clearly.
