- Published on
Frontend Live Coding Interview: React Component Challenges & Solutions
Frontend Live Coding Interview: React Component Challenges & Solutions
Live coding interviews are where rubber meets the road in frontend interviews. You'll need to demonstrate not just theoretical knowledge, but practical problem-solving skills under pressure. This guide covers the most common React component challenges you'll face, with step-by-step implementations and optimization techniques. 🚀
Table of Contents
- Common Live Coding Scenarios
- Data Fetching Components
- Infinite Scroll Implementation
- Form Handling and Validation
- Search and Filtering
- Modal and Dialog Components
- Performance Optimization Techniques
- Interview Tips and Best Practices
Common Live Coding Scenarios
What Interviewers Look For
During live coding sessions, interviewers evaluate:
- Problem-solving approach: How you break down complex problems
- Code organization: Clean, readable, and maintainable code structure
- React patterns: Proper use of hooks, state management, and lifecycle
- Error handling: Graceful handling of edge cases and errors
- Performance awareness: Understanding of optimization techniques
- Testing mindset: Writing testable and robust components
Typical Time Constraints
- 30-45 minutes: Basic component implementation
- 45-60 minutes: Complex component with optimization
- 60+ minutes: Full feature with multiple components
Data Fetching Components
Challenge 1: User List with Loading States
Scenario: Build a component that fetches and displays a list of users with proper loading and error states.
// Basic implementation
import React, { useState, useEffect } from 'react'
interface User {
id: number
name: string
email: string
company: { name: string }
}
interface ApiResponse {
data: User[]
loading: boolean
error: string | null
}
const UserList: React.FC = () => {
const [users, setUsers] = useState<User[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
const fetchUsers = async () => {
try {
setLoading(true)
setError(null)
const response = await fetch('https://jsonplaceholder.typicode.com/users')
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const userData = await response.json()
setUsers(userData)
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred')
} finally {
setLoading(false)
}
}
fetchUsers()
}, [])
if (loading) {
return (
<div className="loading-container">
<div className="spinner" />
<p>Loading users...</p>
</div>
)
}
if (error) {
return (
<div className="error-container">
<p>Error: {error}</p>
<button onClick={() => window.location.reload()}>Try Again</button>
</div>
)
}
return (
<div className="user-list">
<h2>Users ({users.length})</h2>
<div className="user-grid">
{users.map((user) => (
<div key={user.id} className="user-card">
<h3>{user.name}</h3>
<p>{user.email}</p>
<p>{user.company.name}</p>
</div>
))}
</div>
</div>
)
}
export default UserList
Advanced Implementation with Custom Hook
// Custom hook for data fetching
const useApi = <T>(url: string) => {
const [data, setData] = useState<T | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const refetch = useCallback(async () => {
try {
setLoading(true)
setError(null)
const response = await fetch(url)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const result = await response.json()
setData(result)
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred')
} finally {
setLoading(false)
}
}, [url])
useEffect(() => {
refetch()
}, [refetch])
return { data, loading, error, refetch }
}
// Enhanced component using the custom hook
const EnhancedUserList: React.FC = () => {
const { data: users, loading, error, refetch } = useApi<User[]>(
'https://jsonplaceholder.typicode.com/users'
)
if (loading) return <LoadingSpinner />
if (error) return <ErrorMessage error={error} onRetry={refetch} />
if (!users?.length) return <EmptyState message="No users found" />
return (
<div className="user-list">
<header className="list-header">
<h2>Users ({users.length})</h2>
<button onClick={refetch} className="refresh-btn">
Refresh
</button>
</header>
<UserGrid users={users} />
</div>
)
}
// Reusable components
const LoadingSpinner: React.FC = () => (
<div className="loading-container">
<div className="spinner" />
<p>Loading...</p>
</div>
)
const ErrorMessage: React.FC<{ error: string; onRetry: () => void }> = ({
error,
onRetry
}) => (
<div className="error-container">
<p>Error: {error}</p>
<button onClick={onRetry}>Try Again</button>
</div>
)
const EmptyState: React.FC<{ message: string }> = ({ message }) => (
<div className="empty-state">
<p>{message}</p>
</div>
)
const UserGrid: React.FC<{ users: User[] }> = ({ users }) => (
<div className="user-grid">
{users.map(user => (
<UserCard key={user.id} user={user} />
))}
</div>
)
const UserCard: React.FC<{ user: User }> = ({ user }) => (
<div className="user-card">
<h3>{user.name}</h3>
<p>{user.email}</p>
<p>{user.company.name}</p>
</div>
)
Infinite Scroll Implementation
Challenge 2: Infinite Scrolling Product List
Scenario: Build a product list that loads more items as the user scrolls to the bottom.
// Basic infinite scroll implementation
import React, { useState, useEffect, useCallback, useRef } from 'react'
interface Product {
id: number
title: string
price: number
image: string
category: string
}
const InfiniteProductList: React.FC = () => {
const [products, setProducts] = useState<Product[]>([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [hasMore, setHasMore] = useState(true)
const [page, setPage] = useState(1)
const observer = useRef<IntersectionObserver | null>(null)
const lastProductElementRef = useCallback(
(node: HTMLDivElement | null) => {
if (loading) return
if (observer.current) observer.current.disconnect()
observer.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasMore) {
setPage((prevPage) => prevPage + 1)
}
})
if (node) observer.current.observe(node)
},
[loading, hasMore]
)
const fetchProducts = useCallback(async (pageNum: number) => {
try {
setLoading(true)
setError(null)
const response = await fetch(`https://fakestoreapi.com/products?limit=6&page=${pageNum}`)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const newProducts = await response.json()
if (newProducts.length === 0) {
setHasMore(false)
return
}
setProducts((prevProducts) =>
pageNum === 1 ? newProducts : [...prevProducts, ...newProducts]
)
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred')
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
fetchProducts(page)
}, [page, fetchProducts])
if (error && products.length === 0) {
return (
<div className="error-container">
<p>Error: {error}</p>
<button onClick={() => fetchProducts(1)}>Try Again</button>
</div>
)
}
return (
<div className="infinite-product-list">
<h2>Products</h2>
<div className="product-grid">
{products.map((product, index) => {
const isLast = index === products.length - 1
return (
<div
key={product.id}
ref={isLast ? lastProductElementRef : null}
className="product-card"
>
<img src={product.image} alt={product.title} />
<h3>{product.title}</h3>
<p className="price">${product.price}</p>
<p className="category">{product.category}</p>
</div>
)
})}
</div>
{loading && (
<div className="loading-more">
<div className="spinner" />
<p>Loading more products...</p>
</div>
)}
{!hasMore && products.length > 0 && (
<div className="end-message">
<p>You've reached the end!</p>
</div>
)}
{error && products.length > 0 && (
<div className="error-inline">
<p>Error loading more: {error}</p>
<button onClick={() => fetchProducts(page)}>Retry</button>
</div>
)}
</div>
)
}
export default InfiniteProductList
Advanced Infinite Scroll with Custom Hook
// Advanced infinite scroll hook
interface UseInfiniteScrollProps<T> {
fetchData: (page: number) => Promise<T[]>
itemsPerPage?: number
}
const useInfiniteScroll = <T extends { id: number | string }>({
fetchData,
itemsPerPage = 10,
}: UseInfiniteScrollProps<T>) => {
const [items, setItems] = useState<T[]>([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [hasMore, setHasMore] = useState(true)
const [page, setPage] = useState(1)
const observer = useRef<IntersectionObserver | null>(null)
const lastElementRef = useCallback(
(node: HTMLElement | null) => {
if (loading) return
if (observer.current) observer.current.disconnect()
observer.current = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasMore) {
setPage((prevPage) => prevPage + 1)
}
},
{
threshold: 1.0,
rootMargin: '100px', // Load before reaching the bottom
}
)
if (node) observer.current.observe(node)
},
[loading, hasMore]
)
const loadData = useCallback(
async (pageNum: number, reset = false) => {
try {
setLoading(true)
setError(null)
const newItems = await fetchData(pageNum)
if (newItems.length < itemsPerPage) {
setHasMore(false)
}
setItems((prevItems) => {
if (reset || pageNum === 1) {
return newItems
}
// Remove duplicates based on id
const existingIds = new Set(prevItems.map((item) => item.id))
const uniqueNewItems = newItems.filter((item) => !existingIds.has(item.id))
return [...prevItems, ...uniqueNewItems]
})
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred')
} finally {
setLoading(false)
}
},
[fetchData, itemsPerPage]
)
const refresh = useCallback(() => {
setPage(1)
setHasMore(true)
setItems([])
loadData(1, true)
}, [loadData])
useEffect(() => {
loadData(page)
}, [page, loadData])
useEffect(() => {
return () => {
if (observer.current) {
observer.current.disconnect()
}
}
}, [])
return {
items,
loading,
error,
hasMore,
lastElementRef,
refresh,
}
}
// Enhanced infinite scroll component
const AdvancedInfiniteList: React.FC = () => {
const fetchProducts = useCallback(async (page: number): Promise<Product[]> => {
const response = await fetch(`https://fakestoreapi.com/products?limit=6&page=${page}`)
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
return response.json()
}, [])
const {
items: products,
loading,
error,
hasMore,
lastElementRef,
refresh,
} = useInfiniteScroll({ fetchData: fetchProducts, itemsPerPage: 6 })
if (error && products.length === 0) {
return <ErrorMessage error={error} onRetry={refresh} />
}
return (
<div className="advanced-infinite-list">
<header className="list-header">
<h2>Products ({products.length})</h2>
<button onClick={refresh} disabled={loading}>
Refresh
</button>
</header>
<div className="product-grid">
{products.map((product, index) => (
<ProductCard
key={product.id}
product={product}
ref={index === products.length - 1 ? lastElementRef : null}
/>
))}
</div>
<InfiniteScrollIndicator
loading={loading}
hasMore={hasMore}
error={error}
onRetry={refresh}
itemCount={products.length}
/>
</div>
)
}
// Optimized product card with forwardRef
const ProductCard = React.forwardRef<HTMLDivElement, { product: Product }>(({ product }, ref) => (
<div ref={ref} className="product-card">
<img
src={product.image}
alt={product.title}
loading="lazy" // Lazy load images
/>
<h3>{product.title}</h3>
<p className="price">${product.price}</p>
<p className="category">{product.category}</p>
</div>
))
const InfiniteScrollIndicator: React.FC<{
loading: boolean
hasMore: boolean
error: string | null
onRetry: () => void
itemCount: number
}> = ({ loading, hasMore, error, onRetry, itemCount }) => {
if (loading) {
return (
<div className="loading-more">
<div className="spinner" />
<p>Loading more products...</p>
</div>
)
}
if (error && itemCount > 0) {
return (
<div className="error-inline">
<p>Error loading more: {error}</p>
<button onClick={onRetry}>Retry</button>
</div>
)
}
if (!hasMore && itemCount > 0) {
return (
<div className="end-message">
<p>You've reached the end! ({itemCount} items total)</p>
</div>
)
}
return null
}
Form Handling and Validation
Challenge 3: Contact Form with Real-time Validation
Scenario: Build a contact form with real-time validation and submission handling.
// Form validation utilities
interface ValidationRule {
required?: boolean
minLength?: number
maxLength?: number
pattern?: RegExp
custom?: (value: string) => string | null
}
interface ValidationRules {
[key: string]: ValidationRule
}
const validateField = (value: string, rules: ValidationRule): string | null => {
if (rules.required && !value.trim()) {
return 'This field is required'
}
if (rules.minLength && value.length < rules.minLength) {
return `Minimum length is ${rules.minLength} characters`
}
if (rules.maxLength && value.length > rules.maxLength) {
return `Maximum length is ${rules.maxLength} characters`
}
if (rules.pattern && !rules.pattern.test(value)) {
return 'Invalid format'
}
if (rules.custom) {
return rules.custom(value)
}
return null
}
// Custom form hook
const useForm = <T extends Record<string, string>>(
initialValues: T,
validationRules: ValidationRules
) => {
const [values, setValues] = useState<T>(initialValues)
const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({})
const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({})
const [isSubmitting, setIsSubmitting] = useState(false)
const validateSingle = useCallback(
(name: keyof T, value: string) => {
const rules = validationRules[name as string]
if (!rules) return null
return validateField(value, rules)
},
[validationRules]
)
const validateAll = useCallback(() => {
const newErrors: Partial<Record<keyof T, string>> = {}
let isValid = true
Object.keys(values).forEach((key) => {
const error = validateSingle(key, values[key])
if (error) {
newErrors[key] = error
isValid = false
}
})
setErrors(newErrors)
return isValid
}, [values, validateSingle])
const handleChange = useCallback(
(name: keyof T, value: string) => {
setValues((prev) => ({ ...prev, [name]: value }))
// Real-time validation
if (touched[name]) {
const error = validateSingle(name, value)
setErrors((prev) => ({ ...prev, [name]: error || undefined }))
}
},
[touched, validateSingle]
)
const handleBlur = useCallback(
(name: keyof T) => {
setTouched((prev) => ({ ...prev, [name]: true }))
const error = validateSingle(name, values[name])
setErrors((prev) => ({ ...prev, [name]: error || undefined }))
},
[values, validateSingle]
)
const handleSubmit = useCallback(
async (onSubmit: (values: T) => Promise<void>) => {
setIsSubmitting(true)
// Mark all fields as touched
const allTouched = Object.keys(values).reduce((acc, key) => {
acc[key] = true
return acc
}, {} as Record<keyof T, boolean>)
setTouched(allTouched)
const isValid = validateAll()
if (isValid) {
try {
await onSubmit(values)
} catch (error) {
console.error('Form submission error:', error)
}
}
setIsSubmitting(false)
},
[values, validateAll]
)
const reset = useCallback(() => {
setValues(initialValues)
setErrors({})
setTouched({})
setIsSubmitting(false)
}, [initialValues])
const isValid = Object.keys(errors).length === 0 && Object.keys(touched).length > 0
return {
values,
errors,
touched,
isSubmitting,
isValid,
handleChange,
handleBlur,
handleSubmit,
reset,
validateAll,
}
}
// Contact form component
interface ContactFormData {
name: string
email: string
subject: string
message: string
}
const ContactForm: React.FC = () => {
const [submitStatus, setSubmitStatus] = useState<'idle' | 'success' | 'error'>('idle')
const validationRules: ValidationRules = {
name: {
required: true,
minLength: 2,
maxLength: 50,
},
email: {
required: true,
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
custom: (value) => {
if (value && !value.includes('.')) {
return 'Please enter a valid email address'
}
return null
},
},
subject: {
required: true,
minLength: 5,
maxLength: 100,
},
message: {
required: true,
minLength: 10,
maxLength: 500,
},
}
const {
values,
errors,
touched,
isSubmitting,
isValid,
handleChange,
handleBlur,
handleSubmit,
reset,
} = useForm<ContactFormData>({ name: '', email: '', subject: '', message: '' }, validationRules)
const onSubmit = async (formData: ContactFormData) => {
try {
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 2000))
// Simulate random success/failure
if (Math.random() > 0.3) {
console.log('Form submitted:', formData)
setSubmitStatus('success')
reset()
} else {
throw new Error('Submission failed')
}
} catch (error) {
setSubmitStatus('error')
}
}
const handleFormSubmit = (e: React.FormEvent) => {
e.preventDefault()
handleSubmit(onSubmit)
}
return (
<div className="contact-form-container">
<h2>Contact Us</h2>
{submitStatus === 'success' && (
<div className="alert alert-success">
Thank you for your message! We'll get back to you soon.
</div>
)}
{submitStatus === 'error' && (
<div className="alert alert-error">Something went wrong. Please try again.</div>
)}
<form onSubmit={handleFormSubmit} className="contact-form">
<FormField
label="Name"
name="name"
type="text"
value={values.name}
error={errors.name}
touched={touched.name}
onChange={(value) => handleChange('name', value)}
onBlur={() => handleBlur('name')}
required
/>
<FormField
label="Email"
name="email"
type="email"
value={values.email}
error={errors.email}
touched={touched.email}
onChange={(value) => handleChange('email', value)}
onBlur={() => handleBlur('email')}
required
/>
<FormField
label="Subject"
name="subject"
type="text"
value={values.subject}
error={errors.subject}
touched={touched.subject}
onChange={(value) => handleChange('subject', value)}
onBlur={() => handleBlur('subject')}
required
/>
<FormField
label="Message"
name="message"
type="textarea"
value={values.message}
error={errors.message}
touched={touched.message}
onChange={(value) => handleChange('message', value)}
onBlur={() => handleBlur('message')}
required
rows={5}
/>
<div className="form-actions">
<button
type="button"
onClick={reset}
disabled={isSubmitting}
className="btn btn-secondary"
>
Reset
</button>
<button type="submit" disabled={!isValid || isSubmitting} className="btn btn-primary">
{isSubmitting ? 'Sending...' : 'Send Message'}
</button>
</div>
</form>
</div>
)
}
// Reusable form field component
interface FormFieldProps {
label: string
name: string
type: 'text' | 'email' | 'textarea'
value: string
error?: string
touched?: boolean
onChange: (value: string) => void
onBlur: () => void
required?: boolean
placeholder?: string
rows?: number
}
const FormField: React.FC<FormFieldProps> = ({
label,
name,
type,
value,
error,
touched,
onChange,
onBlur,
required,
placeholder,
rows = 3,
}) => {
const hasError = touched && error
return (
<div className={`form-field ${hasError ? 'has-error' : ''}`}>
<label htmlFor={name} className="form-label">
{label}
{required && <span className="required">*</span>}
</label>
{type === 'textarea' ? (
<textarea
id={name}
name={name}
value={value}
onChange={(e) => onChange(e.target.value)}
onBlur={onBlur}
placeholder={placeholder}
rows={rows}
className="form-input"
aria-invalid={hasError}
aria-describedby={hasError ? `${name}-error` : undefined}
/>
) : (
<input
id={name}
name={name}
type={type}
value={value}
onChange={(e) => onChange(e.target.value)}
onBlur={onBlur}
placeholder={placeholder}
className="form-input"
aria-invalid={hasError}
aria-describedby={hasError ? `${name}-error` : undefined}
/>
)}
{hasError && (
<span id={`${name}-error`} className="error-message" role="alert">
{error}
</span>
)}
</div>
)
}
export default ContactForm
Search and Filtering
Challenge 4: Real-time Search with Debouncing
Scenario: Build a search component with real-time filtering and performance optimization.
// Debounce hook
const useDebounce = <T>(value: T, delay: number): T => {
const [debouncedValue, setDebouncedValue] = useState<T>(value)
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => {
clearTimeout(handler)
}
}, [value, delay])
return debouncedValue
}
// Search and filter component
interface Item {
id: number
title: string
category: string
description: string
price: number
tags: string[]
}
interface SearchFilters {
query: string
category: string
minPrice: number
maxPrice: number
sortBy: 'title' | 'price' | 'category'
sortOrder: 'asc' | 'desc'
}
const SearchableProductList: React.FC = () => {
const [items, setItems] = useState<Item[]>([])
const [loading, setLoading] = useState(true)
const [filters, setFilters] = useState<SearchFilters>({
query: '',
category: '',
minPrice: 0,
maxPrice: 1000,
sortBy: 'title',
sortOrder: 'asc'
})
// Debounce search query to avoid excessive API calls
const debouncedQuery = useDebounce(filters.query, 300)
const debouncedPriceFilters = useDebounce(
{ min: filters.minPrice, max: filters.maxPrice },
500
)
// Fetch initial data
useEffect(() => {
const fetchItems = async () => {
try {
setLoading(true)
const response = await fetch('https://fakestoreapi.com/products')
const data = await response.json()
// Simulate additional properties
const enhancedData = data.map((item: any) => ({
...item,
tags: item.category.split(' '),
description: item.description.substring(0, 100) + '...'
}))
setItems(enhancedData)
} catch (error) {
console.error('Failed to fetch items:', error)
} finally {
setLoading(false)
}
}
fetchItems()
}, [])
// Filter and sort items
const filteredItems = useMemo(() => {
let filtered = [...items]
// Text search
if (debouncedQuery) {
const query = debouncedQuery.toLowerCase()
filtered = filtered.filter(item =>
item.title.toLowerCase().includes(query) ||
item.description.toLowerCase().includes(query) ||
item.category.toLowerCase().includes(query) ||
item.tags.some(tag => tag.toLowerCase().includes(query))
)
}
// Category filter
if (filters.category) {
filtered = filtered.filter(item => item.category === filters.category)
}
// Price range filter
filtered = filtered.filter(item =>
item.price >= debouncedPriceFilters.min &&
item.price <= debouncedPriceFilters.max
)
// Sorting
filtered.sort((a, b) => {
let comparison = 0
switch (filters.sortBy) {
case 'title':
comparison = a.title.localeCompare(b.title)
break
case 'price':
comparison = a.price - b.price
break
case 'category':
comparison = a.category.localeCompare(b.category)
break
}
return filters.sortOrder === 'desc' ? -comparison : comparison
})
return filtered
}, [items, debouncedQuery, filters.category, debouncedPriceFilters, filters.sortBy, filters.sortOrder])
// Get unique categories for filter dropdown
const categories = useMemo(() => {
return Array.from(new Set(items.map(item => item.category)))
}, [items])
const updateFilter = <K extends keyof SearchFilters>(
key: K,
value: SearchFilters[K]
) => {
setFilters(prev => ({ ...prev, [key]: value }))
}
const clearFilters = () => {
setFilters({
query: '',
category: '',
minPrice: 0,
maxPrice: 1000,
sortBy: 'title',
sortOrder: 'asc'
})
}
if (loading) {
return <LoadingSpinner />
}
return (
<div className="searchable-product-list">
{/* Search and Filter Controls */}
<div className="search-controls">
<div className="search-bar">
<input
type="text"
placeholder="Search products..."
value={filters.query}
onChange={(e) => updateFilter('query', e.target.value)}
className="search-input"
/>
<SearchIcon />
</div>
<div className="filters">
<select
value={filters.category}
onChange={(e) => updateFilter('category', e.target.value)}
className="filter-select"
>
<option value="">All Categories</option>
{categories.map(category => (
<option key={category} value={category}>
{category}
</option>
))}
</select>
<div className="price-range">
<label>
Min Price: ${filters.minPrice}
<input
type="range"
min="0"
max="1000"
step="10"
value={filters.minPrice}
onChange={(e) => updateFilter('minPrice', Number(e.target.value))}
/>
</label>
<label>
Max Price: ${filters.maxPrice}
<input
type="range"
min="0"
max="1000"
step="10"
value={filters.maxPrice}
onChange={(e) => updateFilter('maxPrice', Number(e.target.value))}
/>
</label>
</div>
<div className="sort-controls">
<select
value={filters.sortBy}
onChange={(e) => updateFilter('sortBy', e.target.value as any)}
className="sort-select"
>
<option value="title">Sort by Title</option>
<option value="price">Sort by Price</option>
<option value="category">Sort by Category</option>
</select>
<button
onClick={() => updateFilter('sortOrder', filters.sortOrder === 'asc' ? 'desc' : 'asc')}
className="sort-order-btn"
>
{filters.sortOrder === 'asc' ? '↑' : '↓'}
</button>
</div>
<button onClick={clearFilters} className="clear-filters-btn">
Clear Filters
</button>
</div>
</div>
{/* Results Summary */}
<div className="results-summary">
<p>
Showing {filteredItems.length} of {items.length} products
{debouncedQuery && ` for "${debouncedQuery}"`}
</p>
</div>
{/* Results Grid */}
{filteredItems.length === 0 ? (
<div className="no-results">
<p>No products found matching your criteria.</p>
<button onClick={clearFilters}>Clear all filters</button>
</div>
) : (
<div className="results-grid">
{filteredItems.map(item => (
<ProductCard key={item.id} product={item} />
))}
</div>
)}
</div>
)
}
// Highlight search terms in text
const HighlightedText: React.FC<{ text: string; query: string }> = ({
text,
query
}) => {
if (!query) return <>{text}</>
const parts = text.split(new RegExp(`(${query})`, 'gi'))
return (
<>
{parts.map((part, index) =>
part.toLowerCase() === query.toLowerCase() ? (
<mark key={index}>{part}</mark>
) : (
part
)
)}
</>
)
}
// Enhanced product card with search highlighting
const SearchableProductCard: React.FC<{
product: Item
searchQuery?: string
}> = ({ product, searchQuery = '' }) => (
<div className="product-card">
<img src={product.image} alt={product.title} />
<h3>
<HighlightedText text={product.title} query={searchQuery} />
</h3>
<p className="description">
<HighlightedText text={product.description} query={searchQuery} />
</p>
<p className="price">${product.price}</p>
<p className="category">{product.category}</p>
</div>
)
const SearchIcon: React.FC = () => (
<svg className="search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.35-4.35" />
</svg>
)
export default SearchableProductList
Modal and Dialog Components
Challenge 5: Accessible Modal with Focus Management
Scenario: Build an accessible modal component with proper focus management and keyboard navigation.
// Modal component with accessibility features
import React, { useEffect, useRef, useCallback } from 'react'
import { createPortal } from 'react-dom'
interface ModalProps {
isOpen: boolean
onClose: () => void
title: string
children: React.ReactNode
size?: 'small' | 'medium' | 'large'
closeOnOverlayClick?: boolean
closeOnEscape?: boolean
}
const Modal: React.FC<ModalProps> = ({
isOpen,
onClose,
title,
children,
size = 'medium',
closeOnOverlayClick = true,
closeOnEscape = true,
}) => {
const modalRef = useRef<HTMLDivElement>(null)
const previousActiveElement = useRef<HTMLElement | null>(null)
// Focus management
useEffect(() => {
if (isOpen) {
// Store the previously focused element
previousActiveElement.current = document.activeElement as HTMLElement
// Focus the modal
setTimeout(() => {
modalRef.current?.focus()
}, 0)
} else {
// Restore focus when modal closes
previousActiveElement.current?.focus()
}
}, [isOpen])
// Trap focus within modal
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (!isOpen || !modalRef.current) return
if (e.key === 'Escape' && closeOnEscape) {
onClose()
return
}
if (e.key === 'Tab') {
const focusableElements = modalRef.current.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)
const firstElement = focusableElements[0] as HTMLElement
const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement
if (e.shiftKey) {
if (document.activeElement === firstElement) {
lastElement.focus()
e.preventDefault()
}
} else {
if (document.activeElement === lastElement) {
firstElement.focus()
e.preventDefault()
}
}
}
},
[isOpen, onClose, closeOnEscape]
)
useEffect(() => {
document.addEventListener('keydown', handleKeyDown)
return () => document.removeEventListener('keydown', handleKeyDown)
}, [handleKeyDown])
// Prevent body scroll when modal is open
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = 'unset'
}
return () => {
document.body.style.overflow = 'unset'
}
}, [isOpen])
const handleOverlayClick = (e: React.MouseEvent) => {
if (e.target === e.currentTarget && closeOnOverlayClick) {
onClose()
}
}
if (!isOpen) return null
const modalContent = (
<div
className="modal-overlay"
onClick={handleOverlayClick}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
>
<div ref={modalRef} className={`modal-content modal-${size}`} tabIndex={-1}>
<div className="modal-header">
<h2 id="modal-title" className="modal-title">
{title}
</h2>
<button onClick={onClose} className="modal-close-btn" aria-label="Close modal">
×
</button>
</div>
<div className="modal-body">{children}</div>
</div>
</div>
)
return createPortal(modalContent, document.body)
}
// Modal hook for easier state management
const useModal = (initialState = false) => {
const [isOpen, setIsOpen] = useState(initialState)
const open = useCallback(() => setIsOpen(true), [])
const close = useCallback(() => setIsOpen(false), [])
const toggle = useCallback(() => setIsOpen((prev) => !prev), [])
return { isOpen, open, close, toggle }
}
// Confirmation modal component
interface ConfirmationModalProps {
isOpen: boolean
onClose: () => void
onConfirm: () => void
title: string
message: string
confirmText?: string
cancelText?: string
variant?: 'danger' | 'warning' | 'info'
}
const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
isOpen,
onClose,
onConfirm,
title,
message,
confirmText = 'Confirm',
cancelText = 'Cancel',
variant = 'info',
}) => {
const handleConfirm = () => {
onConfirm()
onClose()
}
return (
<Modal isOpen={isOpen} onClose={onClose} title={title} size="small">
<div className="confirmation-modal">
<p className="confirmation-message">{message}</p>
<div className="confirmation-actions">
<button onClick={onClose} className="btn btn-secondary">
{cancelText}
</button>
<button onClick={handleConfirm} className={`btn btn-${variant}`} autoFocus>
{confirmText}
</button>
</div>
</div>
</Modal>
)
}
// Example usage component
const ModalExamples: React.FC = () => {
const basicModal = useModal()
const confirmModal = useModal()
const [items, setItems] = useState(['Item 1', 'Item 2', 'Item 3'])
const [itemToDelete, setItemToDelete] = useState<number | null>(null)
const handleDeleteItem = (index: number) => {
setItemToDelete(index)
confirmModal.open()
}
const confirmDelete = () => {
if (itemToDelete !== null) {
setItems((prev) => prev.filter((_, i) => i !== itemToDelete))
setItemToDelete(null)
}
}
return (
<div className="modal-examples">
<h2>Modal Examples</h2>
<div className="actions">
<button onClick={basicModal.open} className="btn btn-primary">
Open Basic Modal
</button>
</div>
<div className="item-list">
<h3>Items</h3>
{items.map((item, index) => (
<div key={index} className="item">
<span>{item}</span>
<button onClick={() => handleDeleteItem(index)} className="btn btn-danger btn-small">
Delete
</button>
</div>
))}
</div>
{/* Basic Modal */}
<Modal
isOpen={basicModal.isOpen}
onClose={basicModal.close}
title="Basic Modal"
size="medium"
>
<div className="modal-content-example">
<p>This is a basic modal with some content.</p>
<form className="modal-form">
<div className="form-group">
<label htmlFor="modal-input">Example Input:</label>
<input id="modal-input" type="text" placeholder="Type something..." />
</div>
<div className="form-group">
<label htmlFor="modal-select">Example Select:</label>
<select id="modal-select">
<option>Option 1</option>
<option>Option 2</option>
<option>Option 3</option>
</select>
</div>
</form>
<div className="modal-actions">
<button onClick={basicModal.close} className="btn btn-secondary">
Cancel
</button>
<button className="btn btn-primary">Save Changes</button>
</div>
</div>
</Modal>
{/* Confirmation Modal */}
<ConfirmationModal
isOpen={confirmModal.isOpen}
onClose={confirmModal.close}
onConfirm={confirmDelete}
title="Confirm Deletion"
message={`Are you sure you want to delete "${
itemToDelete !== null ? items[itemToDelete] : ''
}"? This action cannot be undone.`}
confirmText="Delete"
cancelText="Cancel"
variant="danger"
/>
</div>
)
}
export default ModalExamples
Performance Optimization Techniques
Challenge 6: Optimized Component with Memoization
Scenario: Optimize a component that renders large lists with expensive calculations.
// Expensive calculation simulation
const expensiveCalculation = (value: number): number => {
// Simulate expensive operation
let result = 0
for (let i = 0; i < 1000000; i++) {
result += Math.sin(value + i) * Math.cos(value - i)
}
return result
}
// Unoptimized component (for comparison)
const UnoptimizedList: React.FC<{ items: Item[] }> = ({ items }) => {
const [filter, setFilter] = useState('')
// This will recalculate on every render!
const processedItems = items
.filter((item) => item.name.includes(filter))
.map((item) => ({
...item,
expensiveValue: expensiveCalculation(item.value),
}))
return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter items..."
/>
<div>
{processedItems.map((item) => (
<div key={item.id}>
{item.name}: {item.expensiveValue.toFixed(2)}
</div>
))}
</div>
</div>
)
}
// Optimized component with memoization
interface OptimizedItem {
id: number
name: string
value: number
category: string
}
const OptimizedList: React.FC<{ items: OptimizedItem[] }> = ({ items }) => {
const [filter, setFilter] = useState('')
const [sortBy, setSortBy] = useState<'name' | 'value'>('name')
// Memoize expensive calculations
const expensiveValues = useMemo(() => {
const cache = new Map<number, number>()
return items.reduce((acc, item) => {
if (!cache.has(item.value)) {
cache.set(item.value, expensiveCalculation(item.value))
}
acc[item.id] = cache.get(item.value)!
return acc
}, {} as Record<number, number>)
}, [items])
// Memoize filtered and sorted items
const processedItems = useMemo(() => {
return items
.filter((item) => item.name.toLowerCase().includes(filter.toLowerCase()))
.sort((a, b) => {
if (sortBy === 'name') {
return a.name.localeCompare(b.name)
}
return a.value - b.value
})
.map((item) => ({
...item,
expensiveValue: expensiveValues[item.id],
}))
}, [items, filter, sortBy, expensiveValues])
// Debounce filter input
const debouncedFilter = useDebounce(filter, 300)
// Memoize handlers to prevent unnecessary re-renders
const handleFilterChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setFilter(e.target.value)
}, [])
const handleSortChange = useCallback((newSortBy: 'name' | 'value') => {
setSortBy(newSortBy)
}, [])
return (
<div className="optimized-list">
<div className="list-controls">
<input
value={filter}
onChange={handleFilterChange}
placeholder="Filter items..."
className="filter-input"
/>
<div className="sort-controls">
<button
onClick={() => handleSortChange('name')}
className={sortBy === 'name' ? 'active' : ''}
>
Sort by Name
</button>
<button
onClick={() => handleSortChange('value')}
className={sortBy === 'value' ? 'active' : ''}
>
Sort by Value
</button>
</div>
</div>
<div className="items-count">
Showing {processedItems.length} of {items.length} items
</div>
<VirtualizedItemList items={processedItems} />
</div>
)
}
// Memoized item component
interface ItemProps {
item: OptimizedItem & { expensiveValue: number }
onClick?: (item: OptimizedItem) => void
}
const OptimizedItem = React.memo<ItemProps>(({ item, onClick }) => {
const handleClick = useCallback(() => {
onClick?.(item)
}, [item, onClick])
return (
<div className="list-item" onClick={handleClick}>
<div className="item-name">{item.name}</div>
<div className="item-value">{item.value}</div>
<div className="item-expensive">{item.expensiveValue.toFixed(2)}</div>
<div className="item-category">{item.category}</div>
</div>
)
})
// Virtualized list for large datasets
interface VirtualizedItemListProps {
items: (OptimizedItem & { expensiveValue: number })[]
itemHeight?: number
containerHeight?: number
}
const VirtualizedItemList: React.FC<VirtualizedItemListProps> = ({
items,
itemHeight = 60,
containerHeight = 400,
}) => {
const [scrollTop, setScrollTop] = useState(0)
const containerRef = useRef<HTMLDivElement>(null)
const visibleCount = Math.ceil(containerHeight / itemHeight)
const startIndex = Math.floor(scrollTop / itemHeight)
const endIndex = Math.min(startIndex + visibleCount + 1, items.length)
const visibleItems = items.slice(startIndex, endIndex)
const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
setScrollTop(e.currentTarget.scrollTop)
}, [])
const totalHeight = items.length * itemHeight
const offsetY = startIndex * itemHeight
return (
<div
ref={containerRef}
className="virtualized-list"
style={{ height: containerHeight, overflow: 'auto' }}
onScroll={handleScroll}
>
<div style={{ height: totalHeight, position: 'relative' }}>
<div
style={{
transform: `translateY(${offsetY}px)`,
position: 'absolute',
top: 0,
left: 0,
right: 0,
}}
>
{visibleItems.map((item, index) => (
<div key={item.id} style={{ height: itemHeight }}>
<OptimizedItem item={item} />
</div>
))}
</div>
</div>
</div>
)
}
// Performance monitoring component
const PerformanceMonitor: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const renderCount = useRef(0)
const startTime = useRef(performance.now())
renderCount.current++
useEffect(() => {
const endTime = performance.now()
const renderTime = endTime - startTime.current
console.log(`Render #${renderCount.current} took ${renderTime.toFixed(2)}ms`)
startTime.current = endTime
})
return <>{children}</>
}
// Example usage with performance comparison
const PerformanceComparison: React.FC = () => {
const [items] = useState(() =>
Array.from({ length: 1000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
value: Math.random() * 100,
category: `Category ${i % 5}`,
}))
)
const [showOptimized, setShowOptimized] = useState(true)
return (
<div className="performance-comparison">
<div className="controls">
<button onClick={() => setShowOptimized(!showOptimized)} className="toggle-btn">
Show {showOptimized ? 'Unoptimized' : 'Optimized'} Version
</button>
</div>
<PerformanceMonitor>
{showOptimized ? <OptimizedList items={items} /> : <UnoptimizedList items={items} />}
</PerformanceMonitor>
</div>
)
}
export default PerformanceComparison
Interview Tips and Best Practices
Approach Strategy
Clarify Requirements
"Let me make sure I understand the requirements: - Should this handle loading states? - Do we need error handling? - Are there any accessibility requirements? - What browsers do we need to support?"
Start Simple, Then Optimize
// Start with basic implementation const SimpleComponent = () => { const [data, setData] = useState([]) // Basic functionality first } // Then add optimizations const OptimizedComponent = () => { const [data, setData] = useState([]) const memoizedData = useMemo(() => processData(data), [data]) // Add performance optimizations }
Think Out Loud
"I'm going to use useEffect for the API call because... Now I'm adding error handling because users need feedback when... I'm memoizing this calculation because it's expensive and..."
Common Patterns to Remember
// 1. Data fetching with cleanup
useEffect(() => {
const controller = new AbortController()
const fetchData = async () => {
try {
const response = await fetch(url, {
signal: controller.signal,
})
// Handle response
} catch (error) {
if (error.name !== 'AbortError') {
// Handle error
}
}
}
fetchData()
return () => controller.abort()
}, [url])
// 2. Conditional rendering patterns
const Component = () => {
if (loading) return <LoadingSpinner />
if (error) return <ErrorMessage error={error} />
if (!data?.length) return <EmptyState />
return <DataDisplay data={data} />
}
// 3. Form handling pattern
const useFormField = (initialValue: string) => {
const [value, setValue] = useState(initialValue)
const [error, setError] = useState<string | null>(null)
const onChange = (newValue: string) => {
setValue(newValue)
setError(null) // Clear error on change
}
return { value, error, onChange, setError }
}
Performance Considerations
// Always consider these optimizations:
// 1. Memoization for expensive calculations
const expensiveValue = useMemo(() => heavyCalculation(data), [data])
// 2. Callback memoization
const handleClick = useCallback(
(id: string) => {
onItemClick(id)
},
[onItemClick]
)
// 3. Component memoization
const ExpensiveComponent = React.memo(({ data }) => {
// Only re-renders when data changes
})
// 4. Virtual scrolling for large lists
// 5. Debouncing for search inputs
// 6. Lazy loading for images
Testing Considerations
// Write testable components:
// 1. Separate business logic
const useBusinessLogic = (data) => {
// Business logic here
return processedData
}
// 2. Accept dependencies as props
const Component = ({ apiClient, onSuccess }) => {
// Easier to mock in tests
}
// 3. Use data attributes for testing
;<button data-testid="submit-button" onClick={handleSubmit}>
Submit
</button>
Common Mistakes to Avoid
- Missing dependency arrays:
useEffect(() => {}, [])
vsuseEffect(() => {})
- Infinite loops:
useEffect(() => setData({}), [data])
- Memory leaks: Not cleaning up subscriptions/timeouts
- Accessibility issues: Missing ARIA labels, focus management
- Performance issues: Not memoizing expensive operations
- Error boundaries: Not handling edge cases gracefully
Conclusion
Live coding interviews test your ability to write clean, functional code under pressure while explaining your thought process. The key is to:
- Start simple and build incrementally
- Think about edge cases and error handling
- Consider performance implications
- Write readable, maintainable code
- Communicate clearly throughout the process
Practice these patterns regularly, and you'll be well-prepared for any React live coding challenge! 🚀