Frontend Engineer Interview Preparation — Complete Guide
Frontend Engineer Interview Preparation — Complete Guide
Hey everyone! Welcome back! This is a complete interview preparation guide for Frontend Engineers covering JavaScript, React, Next.js, System Design, and Behavioral rounds.
This covers 3 rounds with real questions and polished answers that you can use in your next interview!
Rounds:
- Round 1 — Technical (JavaScript + React fundamentals)
- Round 2 — Advanced Technical / System Design
- Round 3 — Managerial / Behavioral
Let's dive in!
Round 1 — Technical (JavaScript + React)
Q1: Explain event loop and how asynchronous tasks are executed in JavaScript.
Answer:
JavaScript is a single-threaded language — it has one call stack and can do one thing at a time. But it can handle async operations like API calls, setTimeout, and DOM events using the Event Loop.
The key components are:
- Call Stack — where JavaScript executes code, one function at a time (LIFO)
- Web APIs — browser provides these (setTimeout, fetch, DOM events). When we call an async function, it goes to Web APIs
- Callback Queue (Macrotask Queue) — callbacks from setTimeout, setInterval, DOM events wait here
- Microtask Queue — Promise callbacks (.then, .catch, .finally) and MutationObserver wait here
- Event Loop — continuously checks: "Is call stack empty? If yes, pick from microtask queue first, then macrotask queue"
How it works:
==============
console.log("Start");
setTimeout(() => {
console.log("setTimeout"); // Macrotask
}, 0);
Promise.resolve().then(() => {
console.log("Promise"); // Microtask
});
console.log("End");
OUTPUT:
Start
End
Promise
setTimeout
Why this order?
================
1. "Start" → Goes to call stack → executes immediately
2. setTimeout → Goes to Web API → after 0ms → callback goes to MACROTASK queue
3. Promise.then → Goes to MICROTASK queue
4. "End" → Goes to call stack → executes immediately
5. Call stack is now EMPTY!
6. Event Loop checks: Microtask queue first → "Promise" executes
7. Event Loop checks: Macrotask queue → "setTimeout" executes
RULE: Microtasks ALWAYS run before Macrotasks!
The Event Loop Flow:
=====================
┌──────────────┐
│ Call Stack │ ← Executes synchronous code
└──────┬───────┘
│
▼
┌──────────────┐ ┌──────────────┐
│ Web APIs │ ──→ │ Callback │
│ (setTimeout, │ │ Queues │
│ fetch, DOM) │ │ │
└──────────────┘ │ Microtask: ⚡│ ← Promises (.then)
│ (HIGH priority)│
│ │
│ Macrotask: 🐌│ ← setTimeout, setInterval
│ (LOW priority) │
└──────┬───────┘
│
┌──────┴───────┐
│ Event Loop │
│ "Is stack │
│ empty?" │
│ │
│ Yes → Pick │
│ microtask │
│ first, then │
│ macrotask │
└──────────────┘
Priority: Microtask > Macrotask
Microtasks: Promise.then, Promise.catch, Promise.finally,
queueMicrotask, MutationObserver
Macrotasks: setTimeout, setInterval, setImmediate,
requestAnimationFrame, I/O, UI rendering
Real example that interviewers love:
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve()
.then(() => console.log("3"))
.then(() => console.log("4"));
setTimeout(() => console.log("5"), 0);
console.log("6");
OUTPUT:
1
6
3
4
2
5
Step by step:
1. "1" → call stack → prints immediately
2. setTimeout(cb, 0) → Web API → macrotask queue
3. Promise.then → microtask queue
4. setTimeout(cb, 0) → Web API → macrotask queue
5. "6" → call stack → prints immediately
6. Stack empty → microtask: "3" prints
7. .then chain → microtask: "4" prints
8. Microtask queue empty → macrotask: "2" prints
9. macrotask: "5" prints
Key Points: ============ - JavaScript is single-threaded but NON-BLOCKING (thanks to event loop) - Call stack executes synchronous code - Web APIs handle async operations - Microtasks (Promises) have HIGHER priority than Macrotasks (setTimeout) - Event Loop picks tasks only when call stack is EMPTY - Each macrotask is followed by ALL pending microtasks
Q2: Implement a custom hook in React that debounces input value.
Answer:
A debounce hook delays updating the value until the user stops typing for a specified time. This is very useful for search inputs where we don't want to fire API calls on every keystroke.
// useDebounce.js — Custom Hook
import { useState, useEffect } from 'react';
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
// Set a timer to update debounced value after delay
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Cleanup: clear timer if value changes before delay
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return debouncedValue;
}
export default useDebounce;
Usage in a Search Component:
import { useState, useEffect } from 'react';
import useDebounce from './useDebounce';
function SearchComponent() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearch = useDebounce(searchTerm, 300);
useEffect(() => {
if (debouncedSearch) {
// This API call only fires 300ms after user STOPS typing
fetch(`/api/search?q=${debouncedSearch}`)
.then(res => res.json())
.then(data => console.log(data));
}
}, [debouncedSearch]);
return (
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
);
}
// User types "JavaScript":
// J → a → v → a → S → c → r → i → p → t
// Only ONE API call after 300ms of no typing!
How it works: ============== 1. User types → searchTerm updates on every keystroke 2. useDebounce receives the new value 3. useEffect sets a setTimeout for 300ms 4. If user types again before 300ms → cleanup clears the timer 5. Only when user STOPS typing for 300ms → debouncedValue updates 6. The search useEffect fires → API call happens ONCE! Why this is a good custom hook: - Reusable across any component - Handles cleanup automatically - Follows React patterns (useState + useEffect) - No external library needed
Q3: Create a reusable dropdown component that supports search and multi-select.
Answer:
I would build a Dropdown component that takes options as props, supports searching/filtering, and allows selecting multiple items.
// MultiSelectDropdown.jsx
import { useState, useRef, useEffect } from 'react';
function MultiSelectDropdown({ options, placeholder = "Select...", onChange }) {
const [isOpen, setIsOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [selected, setSelected] = useState([]);
const dropdownRef = useRef(null);
// Close dropdown when clicking outside
useEffect(() => {
function handleClickOutside(e) {
if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
setIsOpen(false);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
// Filter options based on search
const filteredOptions = options.filter(opt =>
opt.label.toLowerCase().includes(searchTerm.toLowerCase())
);
// Toggle selection
const toggleOption = (option) => {
const newSelected = selected.includes(option.value)
? selected.filter(v => v !== option.value) // Remove
: [...selected, option.value]; // Add
setSelected(newSelected);
onChange?.(newSelected);
};
return (
<div ref={dropdownRef} className="dropdown">
<div className="dropdown-trigger" onClick={() => setIsOpen(!isOpen)}>
{selected.length > 0
? `${selected.length} selected`
: placeholder}
</div>
{isOpen && (
<div className="dropdown-menu">
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
className="dropdown-search"
/>
<ul className="dropdown-list">
{filteredOptions.map(opt => (
<li
key={opt.value}
onClick={() => toggleOption(opt)}
className={selected.includes(opt.value) ? 'selected' : ''}
>
<input
type="checkbox"
checked={selected.includes(opt.value)}
readOnly
/>
{opt.label}
</li>
))}
</ul>
</div>
)}
</div>
);
}
// Usage:
const options = [
{ value: 'react', label: 'React' },
{ value: 'vue', label: 'Vue' },
{ value: 'angular', label: 'Angular' },
{ value: 'svelte', label: 'Svelte' },
];
<MultiSelectDropdown
options={options}
placeholder="Select frameworks..."
onChange={(selected) => console.log(selected)}
/>
Key Design Decisions: ====================== - useRef + clickOutside listener → closes dropdown when clicking outside - Controlled search input → filters options in real-time - Checkbox UI → clear visual for multi-select - onChange callback → parent component gets notified - Reusable → just pass different options and placeholder For production, also consider: - Keyboard navigation (arrow keys, Enter, Escape) - Accessibility (ARIA roles, screen reader support) - Virtualization for 1000+ options (react-window) - Loading state for async options
Q4: How would you optimize React rendering performance in large lists (e.g., 10k+ rows)?
Answer:
Rendering 10,000+ items in the DOM is extremely slow — the browser struggles to create, layout, and paint that many DOM nodes. Here's how I would optimize:
1. Virtualization (Most Important!)
The Problem: ============= 10,000 rows → 10,000 DOM nodes → Browser crashes! 😱 The Solution: VIRTUALIZATION ============================== Only render what's VISIBLE in the viewport! If viewport shows 20 rows at a time: - Render only ~25 rows (20 visible + buffer) - As user scrolls → swap out old rows, render new ones - DOM always has ~25 nodes instead of 10,000! Before (No Virtualization): DOM: [Row 1][Row 2][Row 3]...[Row 10000] → 10,000 DOM nodes 😱 After (Virtualized): DOM: [Row 45][Row 46]...[Row 70] → Only ~25 DOM nodes! 🚀 Libraries: - react-window (lightweight, recommended) - react-virtuoso (feature-rich) - TanStack Virtual (framework-agnostic)
// Using react-window
import { FixedSizeList } from 'react-window';
function VirtualizedList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>
{items[index].name}
</div>
);
return (
<FixedSizeList
height={600} // Viewport height
itemCount={items.length} // Total items (10,000)
itemSize={50} // Each row height
width="100%"
>
{Row}
</FixedSizeList>
);
}
// Only ~15-20 DOM nodes at any time, no matter list size!
2. Other Optimization Techniques:
React.memo — Prevent unnecessary re-renders:
const Row = React.memo(({ data }) => <div>{data.name}</div>);
useMemo — Memoize expensive computations:
const sortedList = useMemo(() => list.sort(...), [list]);
useCallback — Stable function references:
const handleClick = useCallback((id) => {...}, []);
Pagination — Load 50 items at a time instead of 10,000
Infinite Scroll — Load more as user scrolls down
Key prop — Always use stable, unique keys (NOT array index!)
✅ key={item.id}
❌ key={index}
Summary — Large List Optimization: ==================================== 1. Virtualization (react-window) → #1 most impactful 2. React.memo on row components → prevent re-renders 3. useMemo for filtered/sorted data → avoid recalculation 4. Stable keys (item.id, not index) → efficient reconciliation 5. Pagination or infinite scroll → load less data 6. Debounce search/filter inputs → fewer re-renders
Q5: Explain the difference between controlled and uncontrolled components with examples.
Answer:
In React, form elements can be handled in two ways — controlled (React controls the value) or uncontrolled (DOM controls the value).
// ✅ CONTROLLED COMPONENT
// React state is the "single source of truth"
function ControlledForm() {
const [name, setName] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
console.log(name); // Already have the value!
};
return (
<form onSubmit={handleSubmit}>
<input
value={name} // React controls value
onChange={(e) => setName(e.target.value)} // Every keystroke updates state
/>
<button type="submit">Submit</button>
</form>
);
}
// ✅ UNCONTROLLED COMPONENT
// DOM is the "single source of truth"
function UncontrolledForm() {
const inputRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
console.log(inputRef.current.value); // Read from DOM directly
};
return (
<form onSubmit={handleSubmit}>
<input
ref={inputRef} // DOM controls value
defaultValue="" // Initial value only
/>
<button type="submit">Submit</button>
</form>
);
}
| Feature | Controlled | Uncontrolled |
|---|---|---|
| Source of truth | React state | DOM |
| Value access | Via state variable | Via ref (inputRef.current.value) |
| Re-renders | On every keystroke | Only on submit |
| Validation | Real-time (on every change) | On submit only |
| Best for | Dynamic forms, instant validation | Simple forms, file inputs |
| React recommendation | Preferred approach | Use when needed |
When to use which: =================== Controlled → When you need: - Real-time validation - Conditional rendering based on input - Format input as user types (phone number, currency) - Disable submit button until form is valid Uncontrolled → When you need: - Simple form with no real-time validation - File input (<input type="file" /> is ALWAYS uncontrolled) - Integration with non-React code - Better performance (no re-render per keystroke)
Round 2 — Advanced Technical / System Design
Q1: How would you design a scalable React application for a dashboard with 100+ pages?
Answer:
For a dashboard with 100+ pages, I would focus on modular architecture, code splitting, and consistent patterns.
Architecture: ============== src/ ├── app/ # App-level config (routes, providers) │ ├── routes.jsx # Centralized route config │ └── providers.jsx # All context providers │ ├── features/ # Feature-based modules (NOT page-based!) │ ├── dashboard/ │ │ ├── components/ # Dashboard-specific components │ │ ├── hooks/ # Dashboard-specific hooks │ │ ├── api/ # Dashboard API calls │ │ └── index.jsx # Dashboard page (lazy loaded) │ │ │ ├── users/ │ │ ├── components/ │ │ ├── hooks/ │ │ ├── api/ │ │ └── index.jsx │ │ │ └── analytics/ │ ├── components/ │ ├── hooks/ │ ├── api/ │ └── index.jsx │ ├── shared/ # Reusable across all features │ ├── components/ # Button, Modal, Table, etc. │ ├── hooks/ # useDebounce, useFetch, etc. │ ├── utils/ # Helper functions │ └── layouts/ # Sidebar, Header, PageLayout │ └── services/ # API client, auth, storage
Key Decisions:
===============
1. FEATURE-BASED structure (not page-based)
- Each feature is independent and self-contained
- Easy to find code: "users feature → users folder"
- Teams can own entire features
2. LAZY LOADING every route
const Dashboard = React.lazy(() => import('./features/dashboard'));
- 100 pages but user only loads what they visit
- Initial bundle stays small
3. SHARED component library
- Consistent UI across 100+ pages
- Button, Table, Modal, Form components
- Design system with Storybook
4. CENTRALIZED state management
- Server state: TanStack Query (React Query)
- Global UI state: Zustand (lightweight)
- Avoid prop drilling with Context for themes/auth
5. ROUTE-BASED code splitting
- Each route is a separate chunk
- Prefetch next likely routes on hover
Q2: Explain how code splitting and lazy loading improve performance.
Answer:
Without code splitting, the browser downloads the entire app as one big JavaScript bundle before showing anything. For large apps, this can be 2-5 MB — users see a blank screen while it loads!
Without Code Splitting: ======================== User visits homepage → Browser downloads ENTIRE app [Home + Dashboard + Users + Analytics + Settings + ...] Bundle size: 3MB 😱 Load time: 5+ seconds on 3G With Code Splitting: ===================== User visits homepage → Browser downloads ONLY homepage code [Home] Bundle size: 200KB ✅ Load time: <1 second! User clicks Dashboard → Downloads dashboard chunk [Dashboard] Additional: 150KB (loaded on demand)
// React.lazy + Suspense — The main way to code split
import { lazy, Suspense } from 'react';
// Instead of:
// import Dashboard from './Dashboard'; ← loaded immediately!
// Do this:
const Dashboard = lazy(() => import('./Dashboard')); // ← loaded on demand!
const Analytics = lazy(() => import('./Analytics'));
const Settings = lazy(() => import('./Settings'));
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/analytics" element={<Analytics />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
// Each route becomes a separate JS chunk
// Downloaded only when user navigates to that route!
Types of Code Splitting:
=========================
1. Route-based splitting → Split by pages (most common)
2. Component-based splitting → Heavy components loaded on demand
const HeavyChart = lazy(() => import('./HeavyChart'));
3. Library splitting → Load heavy libraries only when needed
const { PDFViewer } = await import('react-pdf');
Performance Impact:
====================
- Initial load: 3MB → 300KB (10x smaller!)
- TTI (Time to Interactive): 5s → 1s
- Users see content faster
- Unused code is never downloaded
Q3: How do you handle API rate limits gracefully on the frontend?
Answer:
API rate limiting means the server restricts how many requests a client can make in a given time window. If we exceed it, we get 429 Too Many Requests. Here's how I handle it:
Strategies: ============ 1. DEBOUNCE user actions (search, autocomplete) - Don't fire API on every keystroke - Wait 300ms after user stops typing 2. THROTTLE frequent events (scroll, resize) - Limit to 1 API call per second 3. REQUEST QUEUE with concurrency limit - Maximum 3 API calls in parallel - Queue the rest 4. RETRY with exponential backoff - If 429 received → wait 1s → retry - If still 429 → wait 2s → retry - Then 4s → 8s → give up 5. CACHING - Cache API responses (TanStack Query does this!) - Don't re-fetch data that hasn't changed - Use stale-while-revalidate pattern
// Exponential Backoff Implementation
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (response.status === 429) {
// Rate limited! Check Retry-After header
const retryAfter = response.headers.get('Retry-After');
const delay = retryAfter
? parseInt(retryAfter) * 1000
: Math.pow(2, attempt) * 1000; // 1s, 2s, 4s, 8s
console.log(`Rate limited. Retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
return response;
} catch (error) {
if (attempt === maxRetries) throw error;
}
}
}
// With TanStack Query (built-in retry!)
const { data } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
retry: 3,
retryDelay: (attempt) => Math.pow(2, attempt) * 1000,
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
});
Key Points: ============ - Debounce/throttle → prevent excessive requests - Exponential backoff → graceful retry on 429 - Caching → avoid unnecessary requests - Show user-friendly message: "Too many requests, please wait..." - Never bombard the server — respect rate limits!
Q4: What is hydration in Next.js and when can it cause UI mismatches?
Answer:
Hydration is the process where React takes the server-rendered HTML and attaches JavaScript to it, making it interactive.
How Hydration Works: ===================== Step 1: Server renders HTML Server → Generates static HTML → Sends to browser User sees content IMMEDIATELY (but it's not interactive yet) Step 2: Browser downloads JavaScript bundle React JS code loads in the background Step 3: HYDRATION happens React "hydrates" the static HTML: - Attaches event listeners (onClick, onChange, etc.) - Connects state management - Makes everything interactive! Timeline: Server HTML received → User sees content (fast!) JS bundle loaded → Hydration → Page becomes interactive Think of it like: Server HTML = a paper menu (you can read it) Hydration = turning it into a touchscreen (now you can interact!)
When does hydration mismatch happen?
Hydration mismatch = Server HTML ≠ Client render
React expects the first client render to EXACTLY MATCH
the server HTML. If they don't match → hydration error!
Common causes:
===============
1. Using Date/Time:
// ❌ Server time ≠ Client time!
return <p>{new Date().toLocaleString()}</p>
// ✅ Fix: Use useEffect for client-only values
const [time, setTime] = useState('');
useEffect(() => setTime(new Date().toLocaleString()), []);
2. Using window/localStorage:
// ❌ window doesn't exist on server!
return <p>Width: {window.innerWidth}</p>
// ✅ Fix: Check if client-side
const [width, setWidth] = useState(0);
useEffect(() => setWidth(window.innerWidth), []);
3. Using Math.random():
// ❌ Random value differs on server vs client!
return <div id={Math.random()}>...</div>
4. Conditional rendering based on auth/cookies:
// ❌ Server doesn't have access to browser cookies
return isLoggedIn ? <Dashboard /> : <Login />
5. Browser extensions modifying DOM:
// Extensions add elements → React sees unexpected HTML
How to fix:
============
- Use useEffect for client-only values (runs only on client)
- Use 'suppressHydrationWarning' for intentional mismatches
- Use dynamic import with { ssr: false } for client-only components
- Keep server and client renders identical!
Q5: How would you build a component library used by multiple teams?
Answer:
Component Library Architecture:
================================
1. DESIGN SYSTEM first
- Design tokens (colors, spacing, typography)
- Consistent naming conventions
- Figma ↔ Code alignment
2. MONOREPO structure (using Turborepo or Nx)
packages/
├── ui/ # Core components
│ ├── Button/
│ ├── Modal/
│ ├── Table/
│ └── package.json
├── tokens/ # Design tokens
│ ├── colors.ts
│ ├── spacing.ts
│ └── package.json
└── utils/ # Shared utilities
└── package.json
3. DOCUMENTATION with Storybook
- Every component has stories
- Interactive playground
- Props documentation
- Accessibility guidelines
4. KEY PRINCIPLES:
- Composable (small, focused components)
- Accessible (ARIA labels, keyboard navigation)
- Themeable (dark mode, custom brands)
- Tree-shakeable (import only what you use)
- Well-tested (unit + visual regression tests)
5. PUBLISHING:
- Semantic versioning (1.0.0, 1.1.0, 2.0.0)
- Changelog for every release
- npm private registry or GitHub Packages
- CI/CD auto-publishes on merge to main
Q6: Explain CSR, SSR, and SSG — when would you use each?
Answer:
| Feature | CSR (Client-Side Rendering) | SSR (Server-Side Rendering) | SSG (Static Site Generation) |
|---|---|---|---|
| When HTML is generated | In browser (runtime) | On server (per request) | At build time |
| Initial load | Blank → then content loads | Full HTML immediately | Full HTML immediately |
| SEO | ❌ Poor | ✅ Great | ✅ Great |
| Performance | Slow first load | Fast first load, server cost | Fastest (served from CDN) |
| Dynamic data | ✅ Yes | ✅ Yes (fresh every request) | ❌ Stale until rebuild |
| Best for | Dashboards, admin panels | E-commerce, social media | Blogs, docs, marketing |
When to use which:
===================
CSR (React SPA):
→ Internal dashboards, admin panels
→ Apps behind login (SEO doesn't matter)
→ Highly interactive apps (chat, editors)
SSR (Next.js getServerSideProps):
→ E-commerce product pages (SEO + fresh data)
→ Social media feeds (personalized + SEO)
→ Pages that need fresh data on every request
SSG (Next.js getStaticProps):
→ Blog posts, documentation
→ Marketing/landing pages
→ Pages where data rarely changes
ISR (Incremental Static Regeneration — best of both!):
→ SSG + revalidate every X seconds
→ Fresh enough + fast (CDN cached)
→ getStaticProps with { revalidate: 60 }
Q7: How would you design a frontend architecture that can handle 1M+ users daily?
Answer:
Frontend Architecture for Scale: ================================= 1. CDN for Static Assets - Serve JS, CSS, images from CDN (CloudFront, Cloudflare) - Users download from nearest edge server - 90% of traffic never hits your origin server 2. Code Splitting + Lazy Loading - Initial bundle < 200KB - Route-based splitting - Load heavy libraries on demand 3. Caching Strategy - HTTP cache headers (Cache-Control, ETag) - Service Worker for offline support - TanStack Query for API response caching - stale-while-revalidate pattern 4. Optimized Rendering - SSR/SSG for SEO pages - CSR for interactive dashboards - Virtualized lists for large datasets - Image optimization (WebP, lazy loading, srcset) 5. Performance Monitoring - Web Vitals (LCP, FID, CLS) - Error tracking (Sentry) - Real User Monitoring (RUM) - Performance budgets in CI/CD 6. API Layer - GraphQL or BFF (Backend for Frontend) - Request deduplication - Debounce/throttle user actions - Retry with backoff on failures 7. Bundle Optimization - Tree shaking (remove unused code) - Compression (gzip/brotli) - Dynamic imports - Analyze bundle with webpack-bundle-analyzer
Rule of thumb: =============== - First load < 3 seconds on 3G - LCP (Largest Contentful Paint) < 2.5s - TTI (Time to Interactive) < 5s - Bundle size < 200KB initial - Cache everything you can - Monitor everything in production
Round 3 — Managerial / Behavioral
Q1: Tell me about a time when a critical production bug broke the UI — how did you handle it?
Answer:
In the logistics platform, we had a production bug where the dashboard stopped loading for all users after a deployment. The entire tracking page was showing a white screen.
How I handled it:
Step 1 — Immediate Response: I first checked the browser console and found a JavaScript error — one of the API responses had changed its format and our code was trying to access a property on undefined. This caused the entire React component tree to crash because we didn't have an Error Boundary.
Step 2 — Quick Fix: I added a null check on the API response and deployed a hotfix within 30 minutes. I also added an Error Boundary component so that if one section crashes, the rest of the page still works.
Step 3 — Root Cause: The backend team had changed the API response structure without informing the frontend team. I set up a shared API contract (using TypeScript interfaces) so both teams agree on the data shape before any changes.
Step 4 — Prevention: I added Sentry for error monitoring so we get alerts before users report issues. I also added integration tests that validate API response shapes.
Key Takeaways: =============== 1. Stay calm — panicking doesn't fix bugs 2. Identify → Fix → Prevent (in that order) 3. Add Error Boundaries for graceful degradation 4. Monitoring (Sentry) catches issues before users report them 5. API contracts between frontend and backend teams 6. Post-mortem: document what happened and how to prevent it
Q2: How do you prioritize technical debt vs. new features?
Answer:
I believe technical debt and new features should not be treated as opposites — they need to coexist.
My approach:
1. Categorize the debt:
- Critical debt — causes bugs, security issues, or blocks new features → Fix immediately
- High debt — slows down development significantly → Plan in next sprint
- Low debt — annoying but not blocking anything → Boy Scout Rule (clean up as you touch it)
2. The 80/20 rule: I typically allocate about 80% of sprint capacity to new features and 20% to technical debt. This keeps the codebase healthy without stopping feature delivery.
3. "Boy Scout Rule": Leave the code better than you found it. When working on a feature, if I see messy code nearby, I clean it up in the same PR. This way, tech debt gets addressed naturally without dedicated "cleanup sprints."
4. Make it visible: I track technical debt in the backlog alongside features so the team and managers can see it. When debt is invisible, it gets ignored until it becomes an emergency.
Real Example: ============== We had a feature request for real-time notifications. But our API layer was a mess — duplicated calls, no caching. Instead of building notifications on top of bad code: 1. I spent 2 days refactoring the API layer (tech debt) 2. Then built notifications on the clean foundation (feature) 3. The feature took 3 days instead of the estimated 7! Result: Fixing debt FIRST actually made feature delivery FASTER.
Q3: Describe how you handle conflicts with designers or backend teams.
Answer:
Conflicts with cross-functional teams usually happen because of different perspectives — designers think about user experience, backend thinks about system constraints, and frontend is in the middle.
My approach:
1. Listen first, then explain constraints: When a designer gives me a complex animation or interaction, I don't immediately say "that's too hard." I listen to the intent behind the design. Then I explain the technical constraint: "This infinite scroll with 3D animations will drop to 15fps on mobile. Can we achieve the same user goal with a simpler approach?"
2. Propose alternatives, not rejections: Instead of "we can't do this," I say "here are two alternatives that achieve the same goal within our performance budget."
3. With backend teams: When API response structure doesn't match what the frontend needs, instead of arguing, I suggest a BFF (Backend for Frontend) pattern or we agree on a shared API contract before implementation starts. I also use TypeScript interfaces as the common language between frontend and backend.
4. Shared goals: I always bring it back to the user — "What's the best experience we can ship within our timeline?" This aligns everyone.
Key Points: ============ - Listen to understand the intent, not just the requirement - Propose alternatives, never just reject - Use data to support your points (performance metrics, UX research) - Shared language: TypeScript interfaces, Figma prototypes - Common goal: best user experience within constraints
Q4: How do you communicate complex technical decisions to non-technical stakeholders?
Answer:
The key is to translate technical concepts into business impact. Non-technical people don't care about "we need to refactor the state management" — they care about "this will reduce bugs and ship features faster."
My approach:
1. Use analogies: "Our current codebase is like a house where every room's electricity is connected to one switch. If one room has a problem, the whole house goes dark. Code splitting is like giving each room its own switch."
2. Focus on business outcomes:
- Instead of "We need to implement caching" → "This will make the app 3x faster and reduce server costs by 40%"
- Instead of "We need to add Error Boundaries" → "This prevents the entire page from crashing when one small section has an issue"
- Instead of "We need TypeScript" → "This will catch bugs before they reach users and reduce QA time by 30%"
3. Visual aids: I use simple diagrams, before/after screenshots, and performance metrics graphs. A chart showing "page load: 5s → 1.5s" speaks louder than any technical explanation.
4. Decision framework: When presenting options, I frame it as: "Option A costs X, delivers by Y, and gives us Z benefit." This lets stakeholders make informed decisions without needing to understand the code.
Formula: ========= "If we [technical action], then [business outcome]" Examples: - "If we add monitoring, we'll catch issues before users complain" - "If we migrate to Next.js, our pages will load 3x faster, improving conversion" - "If we write tests, we'll reduce production bugs by 40%" Always answer: "So what?" → Why should they care?
Key Takeaways — Interview Cheat Sheet
| Round | Focus | What They're Testing |
|---|---|---|
| Round 1 | JavaScript + React fundamentals | Do you KNOW the basics deeply? |
| Round 2 | System Design + Architecture | Can you THINK at scale? |
| Round 3 | Behavioral + Communication | Can you WORK with a team? |
Top Tips: ========== 1. Don't just say WHAT — explain WHY 2. Use real examples from your experience 3. Mention trade-offs (nothing is perfect) 4. For system design: think about scale, caching, monitoring 5. For behavioral: use STAR format (Situation, Task, Action, Result) 6. Ask clarifying questions — shows maturity 7. It's OK to say "I don't know, but here's how I'd approach it"
Keep coding, keep learning! See you in the next one!
Post a Comment