Common React.js Interview Questions for Mid-Level Frontend Developer
1. What are React Hooks and why were they introduced?
Answer: Hooks are functions that let you use state and other React features in functional components without writing a class. They were introduced in React 16.8 to solve several problems:
- Reusing stateful logic between components was difficult
- Complex components became hard to understand
- Classes confused both people and machines
Code Example:
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, [userId]); // Re-run when userId changes
if (loading) return <div>Loading...</div>;
return <div>{user.name}</div>;
}
2. Explain the difference between useMemo and useCallback
Answer:
useMemomemoizes a computed value and returns the memoized valueuseCallbackmemoizes a function itself and returns the memoized function
Both help optimize performance by preventing unnecessary recalculations or recreations.
Code Example:
import { useMemo, useCallback, useState } from 'react';
function ProductList({ products }) {
const [filter, setFilter] = useState('');
// useMemo - memoizes the result of filtering
const filteredProducts = useMemo(() => {
console.log('Filtering products...');
return products.filter(p =>
p.name.toLowerCase().includes(filter.toLowerCase())
);
}, [products, filter]);
// useCallback - memoizes the function itself
const handleAddToCart = useCallback((productId) => {
console.log('Adding to cart:', productId);
// API call here
}, []); // Function doesn't change
return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
/>
{filteredProducts.map(product => (
<Product
key={product.id}
product={product}
onAddToCart={handleAddToCart} // Same reference
/>
))}
</div>
);
}
3. What is the Context API and when should you use it?
Answer: The Context API allows you to share data across the component tree without passing props through every level (prop drilling). Use it for truly global data like themes, user authentication, or language preferences. Don't overuse it for all state management.
Code Example:
import { createContext, useContext, useState } from 'react';
// Create context
const ThemeContext = createContext();
// Provider component
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Custom hook for consuming context
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
// Usage in component
function Header() {
const { theme, toggleTheme } = useTheme();
return (
<header className={theme}>
<button onClick={toggleTheme}>
Switch to {theme === 'light' ? 'dark' : 'light'} mode
</button>
</header>
);
}
// App setup
function App() {
return (
<ThemeProvider>
<Header />
</ThemeProvider>
);
}
4. Explain controlled vs uncontrolled components
Answer:
- Controlled components: Form data is handled by React state. React controls the input value.
- Uncontrolled components: Form data is handled by the DOM itself. You access values using refs.
Controlled components are recommended for most cases as they provide more control and validation.
Code Example:
import { useState, useRef } from 'react';
// Controlled Component
function ControlledForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
console.log({ email, password });
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
<button type="submit">Login</button>
</form>
);
}
// Uncontrolled Component
function UncontrolledForm() {
const emailRef = useRef();
const passwordRef = useRef();
const handleSubmit = (e) => {
e.preventDefault();
console.log({
email: emailRef.current.value,
password: passwordRef.current.value
});
};
return (
<form onSubmit={handleSubmit}>
<input ref={emailRef} type="email" placeholder="Email" />
<input ref={passwordRef} type="password" placeholder="Password" />
<button type="submit">Login</button>
</form>
);
}
5. What is useReducer and when would you use it over useState?
Answer:
useReducer is an alternative to useState for managing complex state logic. Use it when:
- You have complex state logic with multiple sub-values
- The next state depends on the previous state
- You want to optimize performance by passing dispatch down instead of callbacks
Code Example:
import { useReducer } from 'react';
const initialState = {
items: [],
loading: false,
error: null
};
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
return {
...state,
items: [...state.items, action.payload]
};
case 'REMOVE_ITEM':
return {
...state,
items: state.items.filter(item => item.id !== action.payload)
};
case 'SET_LOADING':
return { ...state, loading: action.payload };
case 'SET_ERROR':
return { ...state, error: action.payload };
case 'CLEAR_CART':
return { ...state, items: [] };
default:
return state;
}
}
function ShoppingCart() {
const [state, dispatch] = useReducer(cartReducer, initialState);
const addItem = (item) => {
dispatch({ type: 'ADD_ITEM', payload: item });
};
const removeItem = (id) => {
dispatch({ type: 'REMOVE_ITEM', payload: id });
};
return (
<div>
<h2>Cart ({state.items.length} items)</h2>
{state.error && <div className="error">{state.error}</div>}
{state.items.map(item => (
<div key={item.id}>
{item.name} - ${item.price}
<button onClick={() => removeItem(item.id)}>Remove</button>
</div>
))}
<button onClick={() => dispatch({ type: 'CLEAR_CART' })}>
Clear Cart
</button>
</div>
);
}
6. Explain React's reconciliation process and keys
Answer: Reconciliation is the process React uses to update the DOM efficiently by comparing the new virtual DOM with the previous one. Keys help React identify which items have changed, been added, or removed in lists. Keys should be stable, unique, and predictable (avoid using array indices for dynamic lists).
Code Example:
// Bad - Using index as key
function BadList({ items }) {
return (
<ul>
{items.map((item, index) => (
<li key={index}>{item.name}</li>
))}
</ul>
);
}
// Good - Using stable unique identifier
function GoodList({ items }) {
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
// Demonstrating why keys matter
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Learn React', completed: false },
{ id: 2, text: 'Build project', completed: false }
]);
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
{todo.text}
</li>
))}
</ul>
);
}
7. What are Higher-Order Components (HOCs)?
Answer: A Higher-Order Component is a function that takes a component and returns a new enhanced component. HOCs are used for reusing component logic, such as authentication checks, data fetching, or adding props.
Code Example:
import { useState, useEffect } from 'react';
// HOC for adding loading state to any component
function withLoading(Component) {
return function WithLoadingComponent({ isLoading, ...props }) {
if (isLoading) {
return <div>Loading...</div>;
}
return <Component {...props} />;
};
}
// HOC for authentication
function withAuth(Component) {
return function WithAuthComponent(props) {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Check authentication
const token = localStorage.getItem('token');
setIsAuthenticated(!!token);
setLoading(false);
}, []);
if (loading) return <div>Checking authentication...</div>;
if (!isAuthenticated) return <div>Please log in</div>;
return <Component {...props} />;
};
}
// Original components
function UserProfile({ user }) {
return <div>Welcome, {user.name}!</div>;
}
function DataDisplay({ data }) {
return <div>{JSON.stringify(data)}</div>;
}
// Enhanced components
const UserProfileWithLoading = withLoading(UserProfile);
const ProtectedProfile = withAuth(UserProfile);
// Usage
function App() {
const [loading, setLoading] = useState(true);
const [user, setUser] = useState(null);
return (
<div>
<UserProfileWithLoading
isLoading={loading}
user={user}
/>
<ProtectedProfile user={user} />
</div>
);
}
8. How do you optimize React performance?
Answer: Performance optimization techniques include:
- Using React.memo for component memoization
- Implementing useMemo and useCallback for expensive calculations
- Code splitting with React.lazy and Suspense
- Virtualizing long lists
- Avoiding inline function definitions in render
Code Example:
import { memo, useState, useMemo, useCallback, lazy, Suspense } from 'react';
// 1. React.memo - Prevents re-renders if props haven't changed
const ExpensiveComponent = memo(({ data, onClick }) => {
console.log('ExpensiveComponent rendered');
return (
<div onClick={onClick}>
{data.map(item => <div key={item.id}>{item.name}</div>)}
</div>
);
});
// 2. Code Splitting
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function OptimizedApp() {
const [count, setCount] = useState(0);
const [items, setItems] = useState([
{ id: 1, name: 'Item 1', price: 100 },
{ id: 2, name: 'Item 2', price: 200 }
]);
// 3. useMemo - Memoize expensive calculations
const totalPrice = useMemo(() => {
console.log('Calculating total...');
return items.reduce((sum, item) => sum + item.price, 0);
}, [items]);
// 4. useCallback - Memoize callback functions
const handleClick = useCallback(() => {
console.log('Clicked');
}, []);
return (
<div>
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
<div>Total: ${totalPrice}</div>
<ExpensiveComponent
data={items}
onClick={handleClick}
/>
{/* 5. Lazy loading */}
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
</div>
);
}
9. Explain custom hooks and create an example
Answer: Custom hooks are JavaScript functions that start with "use" and can call other hooks. They let you extract component logic into reusable functions, promoting code reuse and separation of concerns.
Code Example:
import { useState, useEffect } from 'react';
// Custom hook for fetching data
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url);
const json = await response.json();
if (!cancelled) {
setData(json);
setError(null);
}
} catch (err) {
if (!cancelled) {
setError(err.message);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
};
fetchData();
return () => {
cancelled = true;
};
}, [url]);
return { data, loading, error };
}
// Custom hook for local storage
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// Custom hook for form handling
function useForm(initialValues) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const handleChange = (e) => {
const { name, value } = e.target;
setValues(prev => ({ ...prev, [name]: value }));
};
const resetForm = () => {
setValues(initialValues);
setErrors({});
};
return { values, errors, handleChange, resetForm, setErrors };
}
// Usage
function UserProfile() {
const { data, loading, error } = useFetch('/api/user');
const [theme, setTheme] = useLocalStorage('theme', 'light');
const { values, handleChange, resetForm } = useForm({
username: '',
email: ''
});
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div className={theme}>
<h1>{data.name}</h1>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
<form>
<input
name="username"
value={values.username}
onChange={handleChange}
/>
<input
name="email"
value={values.email}
onChange={handleChange}
/>
<button type="button" onClick={resetForm}>Reset</button>
</form>
</div>
);
}
10. What is React.StrictMode and why use it?
Answer: StrictMode is a development tool that highlights potential problems in an application. It activates additional checks and warnings for its descendants, such as identifying unsafe lifecycles, legacy API usage, and detecting unexpected side effects by intentionally double-invoking certain functions.
Code Example:
import { StrictMode, useState, useEffect } from 'react';
function App() {
return (
<StrictMode>
<ProblemComponent />
<GoodComponent />
</StrictMode>
);
}
// This will show warnings in StrictMode
function ProblemComponent() {
const [count, setCount] = useState(0);
// Side effect without cleanup - StrictMode will help detect this
useEffect(() => {
console.log('Effect running');
const interval = setInterval(() => {
setCount(c => c + 1);
}, 1000);
// Missing cleanup! StrictMode double-invocation helps catch this
}, []);
return <div>Count: {count}</div>;
}
// Properly written component
function GoodComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('Effect running');
const interval = setInterval(() => {
setCount(c => c + 1);
}, 1000);
// Proper cleanup
return () => {
clearInterval(interval);
};
}, []);
return <div>Count: {count}</div>;
}
export default App;
These questions cover fundamental to advanced React concepts that are commonly asked in mid-level frontend developer interviews. Practice implementing these patterns and understand the reasoning behind each approach.