Frontend Interview Preparation
Comprehensive guide with frequently asked questions to ace your frontend development interviews
JavaScript & ES6 Fundamentals
Key differences:
• Scope:
• Hoisting: all are hoisted, but
• Re-declaration:
• Reassignment:
Example: In an if block,
• Scope:
var is function-scoped, let and const are block-scoped• Hoisting: all are hoisted, but
let and const can't be used before declaration• Re-declaration:
var allows re-declaration, let and const don't• Reassignment:
var and let can be reassigned, const cannotExample: In an if block,
var variables are accessible outside the block, while let and const are not.Hoisting means variable and function declarations are moved to the top of their scope during compilation. With var, the declaration is hoisted and initialized with undefined. With let and const, they're hoisted but not initialized, creating a Temporal Dead Zone where you can't access them before declaration.
The Temporal Dead Zone is the period between when a variable is hoisted and when it's declared where you can't access it. For example, accessing a let or const variable before its declaration throws a ReferenceError, while var would return undefined.
A Promise represents a future value that will be available later (resolved) or an error (rejected).
Three states:
• Pending: initial state
• Fulfilled: operation completed successfully
• Rejected: operation failed
They help handle asynchronous operations with
Three states:
• Pending: initial state
• Fulfilled: operation completed successfully
• Rejected: operation failed
They help handle asynchronous operations with
.then() for success and .catch() for errors.Promises use .then() chains which can become complex. Async/await makes asynchronous code look synchronous and is easier to read and debug. Async functions always return a Promise, and await pauses execution until the Promise resolves. Error handling is better with try/catch blocks instead of .catch().
Destructuring extracts values from objects and arrays into separate variables. For arrays: const [first, second] = colors extracts first two elements. For objects: const { name, age } = person extracts those properties. You can also rename variables: const { name: fullName } = person.
A closure is a function that has access to variables in its outer (enclosing) scope even after the outer function has returned. This happens because functions in JavaScript form closures over the environment in which they were created, maintaining references to outer variables. Closures are useful for data privacy, creating function factories, and maintaining state in asynchronous operations.
Debouncing delays function execution until after a specified time has passed since the last invocation - useful for search inputs to avoid excessive API calls. Throttling limits function execution to once per specified time interval - useful for scroll events to maintain performance. Debouncing waits for a pause in activity, while throttling ensures regular execution intervals.
React Development
The Virtual DOM is a JavaScript representation of the real DOM kept in memory. React creates a virtual DOM tree, and when state changes, it creates a new virtual DOM tree, compares (diffs) it with the previous one, and updates only the changed elements in the real DOM. This makes updates more efficient by minimizing expensive DOM operations.
React Hooks are functions that let you use state and other React features in functional components. They were introduced to avoid the complexity of class components, enable better code reuse through custom hooks, and provide a more direct API to React concepts. Common hooks include useState, useEffect, useContext, and useReducer.
Custom hooks are JavaScript functions that start with "use" and can call other hooks. They allow you to extract component logic into reusable functions. Example: A useApi hook that manages data, loading, and error states for API calls. It uses useState for state management and useEffect for the API request, returning an object with data, loading, and error values that components can destructure and use.
The dependency array controls when useEffect runs. With an empty array [], it runs once after mount. With dependencies [count], it runs when those values change. With NO dependency array, it runs after every render, which can cause performance issues and infinite loops. Always include a dependency array to control when effects should run.
State can be handled using:
1.
2.
3. Context API for sharing state across components
4. External libraries like Redux, Zustand, or Jotai for global state management
5. Server state libraries like React Query or SWR for API data management
1.
useState hook for local component state2.
useReducer for complex state logic3. Context API for sharing state across components
4. External libraries like Redux, Zustand, or Jotai for global state management
5. Server state libraries like React Query or SWR for API data management
State is mutable and internal to a component, managed by the component itself and can trigger re-renders when changed - used for local component data.
Props are immutable (read-only) and passed from parent components, controlled by the parent and received as function arguments - used for communication between components.
Examples:
State:
Props:
Props are immutable (read-only) and passed from parent components, controlled by the parent and received as function arguments - used for communication between components.
Examples:
State:
const [count, setCount] = useState(0)Props:
function Child({ name, age }) { return <div>{name} is {age} years old</div>; }Class components use ES6 classes with lifecycle methods and this.state for state management. Functional components are simpler functions that use hooks for state and lifecycle. Functional components are modern React's preferred approach - they're shorter, have better performance, are easier to test, and use hooks instead of lifecycle methods.
Props drilling is passing props through multiple component levels even when intermediate components don't need them. Solutions include: 1) Context API for simple state sharing in small-medium apps, 2) Redux for complex applications with frequent updates, and 3) Component composition to avoid unnecessary prop passing.
Use Context API for small to medium applications with simple state sharing and less frequent updates. Use Redux for large, complex applications with frequent state updates, when you need debugging tools (DevTools), complex state logic, or multiple developers working on the project.
Key optimization techniques:
1.
2.
3.
4. Code splitting with
5. Proper
6. Avoiding inline objects and functions in JSX
1.
React.memo to prevent unnecessary re-renders2.
useMemo for memoizing expensive calculations3.
useCallback for memoizing functions4. Code splitting with
React.lazy and Suspense5. Proper
key props in lists6. Avoiding inline objects and functions in JSX
Use useReducer when you have complex state logic or multiple related state values. It works like Redux - you define a reducer function that takes current state and action, then returns new state. Dispatch actions to update state. It's better than multiple useState calls for complex state management.
Refs provide direct access to DOM elements or component instances. Common use cases: focus management, triggering animations, and integrating with third-party libraries. Use useRef() in functional components. forwardRef allows passing refs to child components. Avoid overusing refs - prefer declarative patterns when possible.
Use React Router with key components: BrowserRouter (provides routing context), Routes (container for routes), Route (individual route definitions), Link (navigation), useParams (access route parameters), and useNavigate (programmatic navigation). Define routes with path and element props, and use dynamic segments with colons like /users/:id.
Microfrontends is an architectural pattern that extends microservices to the frontend, allowing teams to develop, deploy, and maintain parts of a web application independently.
Key Concepts:
• Independent deployment: Each microfrontend can be deployed separately
• Technology agnostic: Different teams can use different frameworks
• Team autonomy: Teams own their complete stack (frontend + backend)
• Shared runtime: Multiple apps compose into a single user experience
Implementation approaches:
• Module Federation: Webpack 5 feature for dynamic imports
• Single-SPA: Framework for orchestrating multiple apps
• Web Components: Standard for creating reusable components
• Iframe integration: Simple but limited approach
Benefits: Scalable teams, technology diversity, independent releases
Challenges: Complexity, shared state management, bundle duplication
Key Concepts:
• Independent deployment: Each microfrontend can be deployed separately
• Technology agnostic: Different teams can use different frameworks
• Team autonomy: Teams own their complete stack (frontend + backend)
• Shared runtime: Multiple apps compose into a single user experience
Implementation approaches:
• Module Federation: Webpack 5 feature for dynamic imports
• Single-SPA: Framework for orchestrating multiple apps
• Web Components: Standard for creating reusable components
• Iframe integration: Simple but limited approach
Benefits: Scalable teams, technology diversity, independent releases
Challenges: Complexity, shared state management, bundle duplication
React Architecture
A HOC is a function that takes a component and returns a new component with additional functionality. Used to share logic between components without repeating code.
When to use: For reusing logic like authentication, data handling, or service subscriptions.
Example:
Pros: Logic reusability, separation of concerns
Cons: Can complicate component tree, debugging challenges
When to use: For reusing logic like authentication, data handling, or service subscriptions.
Example:
withAuth(Component) - wraps a component with authentication logic.Pros: Logic reusability, separation of concerns
Cons: Can complicate component tree, debugging challenges
A pattern that involves passing a function as a prop to a component, which handles dynamic content rendering.
When to use: When you need to share rendering logic, like dynamic lists or components that depend on external data.
Example:
Pros: Flexible rendering, avoids HOCs in many cases
Cons: More verbose code, less common since Hooks
When to use: When you need to share rendering logic, like dynamic lists or components that depend on external data.
Example:
<DataFetcher render={(data) => ...} />Pros: Flexible rendering, avoids HOCs in many cases
Cons: More verbose code, less common since Hooks
A pattern where multiple components work together sharing implicit state, usually through a parent component.
When to use: For creating flexible component APIs like menus, tabs, or accordions.
Example:
Pros: Intuitive and flexible API, easy to extend
Cons: Context dependency can complicate logic
When to use: For creating flexible component APIs like menus, tabs, or accordions.
Example:
<Toggle><Toggle.On /><Toggle.Off /><Toggle.Button /></Toggle>Pros: Intuitive and flexible API, easy to extend
Cons: Context dependency can complicate logic
Separates components into two types: containers (handle logic and state) and presentational (handle UI).
When to use: To improve maintainability and reusability of components.
Example:
Container:
Presentational:
Pros: Clear separation of concerns, easy to test
Cons: More files, less common with Hooks
When to use: To improve maintainability and reusability of components.
Example:
Container:
UserContainer (fetches data)Presentational:
UserList (renders UI)Pros: Clear separation of concerns, easy to test
Cons: More files, less common with Hooks
Custom Hooks encapsulate reusable logic in functions that use native React hooks.
When to use: To share logic between components without HOCs or render props.
Example:
Pros: Simplifies logic reuse, cleaner than HOCs/render props
Cons: Requires good design to avoid coupling
Note: Custom hooks have largely replaced HOCs and render props in modern React
When to use: To share logic between components without HOCs or render props.
Example:
useFetch(url) - returns { data, loading }Pros: Simplifies logic reuse, cleaner than HOCs/render props
Cons: Requires good design to avoid coupling
Note: Custom hooks have largely replaced HOCs and render props in modern React
Controlled components have their state managed by React. Uncontrolled components use refs to access values directly from the DOM.
When to use:
• Controlled: When you need to handle form state in React
• Uncontrolled: For simple forms or external library integration
Examples:
Controlled:
Uncontrolled:
Pros/Cons:
• Controlled: More control, more code/renders
• Uncontrolled: Less code, less predictable
When to use:
• Controlled: When you need to handle form state in React
• Uncontrolled: For simple forms or external library integration
Examples:
Controlled:
value={state} onChange={setState}Uncontrolled:
ref={inputRef}Pros/Cons:
• Controlled: More control, more code/renders
• Uncontrolled: Less code, less predictable
Use a global state system to share data between components without manually passing props.
When to use: When multiple components need to access or modify the same state.
Options:
• Context API: Built-in, good for medium apps
• Redux: External library, complex apps
• Zustand/Jotai: Lightweight alternatives
Pros: Avoids prop drilling, ideal for large apps
Cons: Can complicate data flow, overhead for small apps
When to use: When multiple components need to access or modify the same state.
Options:
• Context API: Built-in, good for medium apps
• Redux: External library, complex apps
• Zustand/Jotai: Lightweight alternatives
Pros: Avoids prop drilling, ideal for large apps
Cons: Can complicate data flow, overhead for small apps
SOLID principles guide clean, robust code design in React:
1. Single Responsibility Principle (SRP):
Components and hooks should have one purpose
•
•
2. Open/Closed Principle (OCP):
Components should be extensible without modification
• Use composition or HOCs to add functionality
3. Liskov Substitution Principle (LSP):
Sub-components should be interchangeable with parent types
• A
4. Interface Segregation Principle (ISP):
Avoid forcing components to depend on unused props
• Pass only necessary data via props or context
5. Dependency Inversion Principle (DIP):
Depend on abstractions, not implementations
• Inject data service into hook rather than hardcoding API calls
Result: React apps become modular, reusable, and easier to maintain.
1. Single Responsibility Principle (SRP):
Components and hooks should have one purpose
•
<UserCard> only displays user data•
useFetchUsers handles data retrieval2. Open/Closed Principle (OCP):
Components should be extensible without modification
• Use composition or HOCs to add functionality
3. Liskov Substitution Principle (LSP):
Sub-components should be interchangeable with parent types
• A
<Button> variant should work wherever base <Button> is expected4. Interface Segregation Principle (ISP):
Avoid forcing components to depend on unused props
• Pass only necessary data via props or context
5. Dependency Inversion Principle (DIP):
Depend on abstractions, not implementations
• Inject data service into hook rather than hardcoding API calls
Result: React apps become modular, reusable, and easier to maintain.
Testing & Best Practices
Use React Testing Library for user-centric testing. Key methods: render() to mount components, screen.getByText/getByRole for queries, fireEvent/userEvent for interactions, and waitFor() for async operations. Focus on testing behavior users see rather than implementation details. Example: render( ), find elements with screen.getByRole('button'), simulate clicks with userEvent.click(), and assert with expect().toBeInTheDocument().
Best practices include: 1) Test user behavior, not implementation details, 2) Use semantic queries (getByRole, getByLabelText) over getByTestId, 3) Test components in isolation with mocked dependencies, 4) Use userEvent over fireEvent for more realistic interactions, 5) Test error boundaries and loading states, 6) Keep tests simple and focused on one behavior, 7) Use waitFor() for async operations and avoid act() warnings.
Component Design:
• Keep components small and focused
• Use composition over inheritance
• Prefer functional components with hooks
• Extract custom hooks for reusable logic
Performance:
• Use
• Memoize callbacks and calculations
• Implement code splitting
• Use React DevTools Profiler
State Management:
• Start with local state, lift up when needed
• Use Context for app-wide state (theme, auth)
• Consider Redux for complex interactions
• Keep state minimal
Testing:
• Test behavior, not implementation
• Use React Testing Library
• Mock external dependencies
• Aim for high coverage on critical paths
• Keep components small and focused
• Use composition over inheritance
• Prefer functional components with hooks
• Extract custom hooks for reusable logic
Performance:
• Use
React.memo for expensive components• Memoize callbacks and calculations
• Implement code splitting
• Use React DevTools Profiler
State Management:
• Start with local state, lift up when needed
• Use Context for app-wide state (theme, auth)
• Consider Redux for complex interactions
• Keep state minimal
Testing:
• Test behavior, not implementation
• Use React Testing Library
• Mock external dependencies
• Aim for high coverage on critical paths
Test-Driven Development (TDD) involves writing tests before implementing code, ensuring each piece of functionality is tested and meets requirements from the start.
TDD Process in React:
1. Write a Failing Test: Define expected behavior using Jest with React Testing Library
2. Write Code: Implement minimum code to make the test pass
3. Refactor: Improve code while keeping tests passing
Benefits of TDD in React:
• Reliability: Tests ensure components and hooks behave as expected, catching bugs early
• Maintainability: Encourages modular code (e.g., separating useUser from UserList)
• Confidence in Refactoring: Tests act as a safety net for safe code improvements
• Integration with Clean Architecture: Enforces testable, loosely coupled layers
Result: TDD promotes robust, maintainable, and bug-free React applications through a clear development cycle.
TDD Process in React:
1. Write a Failing Test: Define expected behavior using Jest with React Testing Library
2. Write Code: Implement minimum code to make the test pass
3. Refactor: Improve code while keeping tests passing
Benefits of TDD in React:
• Reliability: Tests ensure components and hooks behave as expected, catching bugs early
• Maintainability: Encourages modular code (e.g., separating useUser from UserList)
• Confidence in Refactoring: Tests act as a safety net for safe code improvements
• Integration with Clean Architecture: Enforces testable, loosely coupled layers
Result: TDD promotes robust, maintainable, and bug-free React applications through a clear development cycle.
Code Snippets
React.memo is a higher-order component that memoizes the result and prevents re-renders if props haven't changed.
Code Example:
Key Points:
• Only re-renders when
• Performs shallow comparison of props by default
• Best for expensive components that receive stable props
• Can provide custom comparison function as second argument
Code Example:
// Memoized component to prevent re-renders if props don't change
const UserCard = React.memo(({ user, onSelect }) => {
console.log(`Rendering UserCard for ${user.name}`);
return (
<div onClick={() => onSelect(user.id)}>
{user.name} ({user.email})
</div>
);
});
Key Points:
• Only re-renders when
user or onSelect props change• Performs shallow comparison of props by default
• Best for expensive components that receive stable props
• Can provide custom comparison function as second argument
useMemo memoizes expensive calculations, while useCallback memoizes function references to prevent unnecessary re-renders.
Code Example:
Key Differences:
• useMemo: Memoizes computed values (expensive calculations)
• useCallback: Memoizes function references (prevents child re-renders)
• Both depend on dependency arrays to determine when to recalculate
• Use when performance profiling shows actual benefits
Code Example:
// Memoize filtered users to avoid recalculating on every render
const filteredUsers = useMemo(() => {
console.log('Filtering users');
return users.filter((user) =>
user.name.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [users, searchTerm]);
// Memoize the onSelect callback to prevent re-creating it
const handleSelectUser = useCallback((userId) => {
console.log(`Selected user with ID: ${userId}`);
}, []); // Empty deps if no dependencies needed
Key Differences:
• useMemo: Memoizes computed values (expensive calculations)
• useCallback: Memoizes function references (prevents child re-renders)
• Both depend on dependency arrays to determine when to recalculate
• Use when performance profiling shows actual benefits
Code Splitting allows you to load components dynamically, reducing initial bundle size and improving performance by loading components only when needed.
Code Example:
Key Points:
• React.lazy(): Dynamically imports components using dynamic import()
• Suspense: Handles loading states while component loads
• Fallback: Shows loading UI during component loading
• Bundle splitting: Creates separate chunks for better performance
• Best for large components or route-based splitting
Code Example:
const LazyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
}Key Points:
• React.lazy(): Dynamically imports components using dynamic import()
• Suspense: Handles loading states while component loads
• Fallback: Shows loading UI during component loading
• Bundle splitting: Creates separate chunks for better performance
• Best for large components or route-based splitting