React Interview
1. Use of State Management like Redux: How Does It Work?
Redux is a predictable state container for JavaScript apps, commonly used with React to manage global state. It works on three core principles:
- Single Source of Truth: The entire app’s state is stored in a single object (the store).
- State is Read-Only: The only way to change the state is by dispatching actions, which are payloads describing what happened.
- Changes via Pure Reducers: Reducers are pure functions that take the current state and an action, returning a new state.
How it works:
- Store: Holds the app’s state.
- Actions: Plain objects with a
type(and optional payload) that describe changes (e.g.,{ type: 'INCREMENT', payload: 1 }). - Reducers: Functions that specify how the state changes in response to actions (e.g.,
(state, action) => newState). - Dispatch: Sends actions to the store, triggering reducers to update the state.
- Subscriptions: Components subscribe to the store to access state and re-render when it changes.
2. Performance: How to Load Resources Only Necessary for the Current Screen?
To load only necessary resources for the current screen (lazy loading):
- Code Splitting: Use
React.lazy()andSuspenseto load components dynamically only when needed.const LazyComponent = React.lazy(() => import('./LazyComponent')); function App() { return ( <Suspense fallback={<div>Loading...</div>}> <LazyComponent /> </Suspense> ); } - Dynamic Imports: Load JavaScript modules only when required (e.g., for a specific route).
- Route-Based Splitting: Use React Router to load components for specific routes.
- Image Lazy Loading: Use the
loading="lazy"attribute for images or libraries likereact-lazyload. - Server-Side Rendering (SSR): Fetch only the data needed for the initial render, deferring others.
- Tree Shaking: Ensure bundlers (e.g., Webpack) eliminate unused code during builds.
3. How to Cache Data (Header-Based and State-Based)?
- Header-Based Caching:
- Use HTTP headers like
Cache-Control,ETag, orLast-Modifiedto control browser or CDN caching. - Example:
Cache-Control: max-age=3600caches responses for 1 hour. - Use libraries like
axiosto respect cache headers or implement custom caching logic withfetch.
- Use HTTP headers like
- State-Based Caching:
- Store API responses in Redux or Context to avoid redundant calls.
- Use libraries like
react-queryorswrfor automatic caching and revalidation. - Example with
react-query:import { useQuery } from 'react-query'; const fetchData = async () => (await fetch('/api/data')).json(); function Component() { const { data } = useQuery('key', fetchData); return <div>{data}</div>; } - Persist state in
localStorageorsessionStoragefor client-side caching.
4. Difference Between Using Redux and Context
| Aspect | Redux | Context |
|---|---|---|
| Purpose | Global state management with predictable updates via actions/reducers. | Share state across components without prop drilling. |
| Complexity | More complex (store, reducers, actions, middleware). | Simpler, built into React. |
| Use Case | Large apps with complex state, frequent updates, or async operations. | Small to medium apps with simple shared state (e.g., theme, user data). |
| Scalability | Scales well for large apps with many components and state changes. | Can cause unnecessary re-renders in large apps if not optimized. |
| Middleware | Supports middleware (e.g., redux-thunk) for async logic. | No built-in middleware; requires custom solutions. |
| Debugging | DevTools for time-travel debugging and action logging. | No built-in debugging tools. |
When to Use:
- Use Redux for complex state logic, large-scale apps, or when middleware is needed.
- Use Context for lightweight, localized state (e.g., theme, auth status).
5. What is Prop Drilling and How to Avoid It?
Prop Drilling: Passing props through multiple layers of components, even if intermediate components don’t use them. This makes code harder to maintain.
How to Avoid:
- Context API: Share state globally without passing props.
const MyContext = React.createContext(); function App() { return ( <MyContext.Provider value={{ data: 'Hello' }}> <Child /> </MyContext.Provider> ); } function Child() { const { data } = useContext(MyContext); return <div>{data}</div>; } - Redux: Manage state centrally and access it via hooks.
- Component Composition: Use children props or render props to pass data directly to components that need it.
- Custom Hooks: Encapsulate state logic and share it across components.
6. How to Avoid Unwanted Re-renders of Components (useEffect Parameters)?
Unwanted re-renders occur when a component re-renders unnecessarily due to state or prop changes.
Avoiding Re-renders:
- useEffect Dependencies: Ensure the dependency array includes only necessary variables to prevent excessive runs.
useEffect(() => { fetchData(); }, [id]); // Only re-run if `id` changes - Memoize Values:
- Use
useMemofor expensive computations.const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); - Use
useCallbackfor functions passed as props.const memoizedCallback = useCallback(() => doSomething(a, b), [a, b]);
- Use
- React.memo: Prevent re-rendering of functional components if props haven’t changed.
const MyComponent = React.memo(({ prop }) => <div>{prop}</div>); - Optimize State Updates: Avoid creating new objects/arrays unnecessarily (e.g., use immutable updates).
7. Controlled vs. Uncontrolled Components
| Aspect | Controlled Components | Uncontrolled Components |
|---|---|---|
| Definition | Input elements whose value is controlled by React state. | Input elements that manage their own state via the DOM. |
| Example | <input value={state} onChange={(e) => setState(e.target.value)} /> | <input defaultValue="initial" ref={inputRef} /> |
| State Management | State is managed in React (single source of truth). | State is managed by the DOM; accessed via refs. |
| Use Case | Forms with validation, dynamic updates, or conditional logic. | Simple forms or when you need direct DOM access. |
| Pros | Predictable, easier to validate and manipulate. | Simpler for basic forms, less boilerplate. |
| Cons | More code for state management. | Harder to control or validate dynamically. |
Example:
- Controlled:
function Form() { const [value, setValue] = useState(''); return <input value={value} onChange={(e) => setValue(e.target.value)} />; } - Uncontrolled:
function Form() { const inputRef = useRef(); return <input defaultValue="initial" ref={inputRef} />; }
8. Layers of Safety and the Test Pyramid
The test pyramid prioritizes writing more low-level tests (faster, cheaper) and fewer high-level tests (slower, more complex).
Layers:
- Unit Tests:
- Test individual functions or components in isolation.
- React Testing Library (RTL): Renders components, simulates user interactions, and queries the DOM.
import { render, screen } from '@testing-library/react'; test('renders button', () => { render(<Button>Click</Button>); expect(screen.getByText('Click')).toBeInTheDocument(); }); - Enzyme: Older library for shallow rendering and DOM manipulation (less recommended now).
- Difference: RTL encourages testing user behavior (queries like
getByText), while Enzyme focuses on component internals (state/props). RTL is preferred for modern React apps.
- Difference: RTL encourages testing user behavior (queries like
- Integration Tests:
- Test interactions between components or modules (e.g., a form submitting data to an API).
- Example: Test a form component calling an API mock and updating the UI.
- Regression Suite:
- Automated tests to ensure new changes don’t break existing functionality.
- Run unit, integration, and end-to-end tests after code changes.
- Smoke Tests:
- Quick tests to verify core functionality (e.g., app loads, main features work).
- Example: Check if the login page renders and submits.
Test Pyramid Structure:
- Base: Many unit tests (fast, isolated).
- Middle: Fewer integration tests (test component interactions).
- Top: Few end-to-end or smoke tests (test full workflows).
9. Good Practices to Follow
- Component Thinking (Storybook/Styleguidist):
- Build reusable, isolated components and document them in tools like Storybook.
- Example: Create a
Buttoncomponent with stories for different variants.
- ESLint:
- Enforce code quality and consistency (e.g., no unused variables, consistent formatting).
- Example: Configure ESLint with
eslint-plugin-reactandeslint-plugin-react-hooks.
- Tests in Pipeline:
- Run unit/integration tests in CI/CD pipelines (e.g., GitHub Actions, Jenkins) to catch issues early.
- Trunk-Based Development:
- Developers work on short-lived branches, merging frequently into a single trunk to reduce merge conflicts.
- Feature Toggles:
- Use toggles to enable/disable features without deploying new code (e.g., using
unleashor custom flags).
- Use toggles to enable/disable features without deploying new code (e.g., using
10. Sample Code: Integrating Redux with React
1. Create Redux Store and Reducer
// store.js
import { createStore } from 'redux';
// Reducer
const initialState = { count: 0 };
const counterReducer = (state = initialState, action) => {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'DECREMENT':
return { ...state, count: state.count - 1 };
default:
return state;
}
};
// Store
const store = createStore(counterReducer);
export default store;2. Connect Store to React Components
// App.js
import { Provider } from 'react-redux';
import store from './store';
import Counter from './Counter';
function App() {
return (
<Provider store={store}>
<Counter />
</Provider>
);
}
export default App;3. Dispatch Actions Using useDispatch
// Counter.js
import { useSelector, useDispatch } from 'react-redux';
function Counter() {
const count = useSelector((state) => state.count); // Access state
const dispatch = useDispatch();
return (
<div>
<p>Count: {count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button>
</div>
);
}
export default Counter;11. Importance of Immutability in React/Redux
- React: Immutability ensures React detects changes via reference comparison (
===). Updating state immutably (e.g., using spread operators) triggers re-renders only when necessary.setState((prev) => ({ ...prev, key: newValue })); - Redux: Reducers must be pure and return new state objects. Mutating state directly breaks Redux’s predictability and time-travel debugging.
// Correct return { ...state, count: state.count + 1 }; // Incorrect state.count += 1; // Mutates state
12. Handling Asynchronous Tasks in Redux (e.g., API Calls)
Use middleware like redux-thunk to handle async operations.
- Thunk: Allows dispatching functions instead of plain action objects, enabling async logic.
- Setup:
// store.js import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; const store = createStore(reducer, applyMiddleware(thunk)); - Async Action:
// actions.js export const fetchData = () => async (dispatch) => { dispatch({ type: 'FETCH_START' }); try { const response = await fetch('/api/data'); const data = await response.json(); dispatch({ type: 'FETCH_SUCCESS', payload: data }); } catch (error) { dispatch({ type: 'FETCH_ERROR', payload: error.message }); } }; - Usage:
function Component() { const dispatch = useDispatch(); useEffect(() => { dispatch(fetchData()); }, [dispatch]); }
13. Improving Performance and Preventing Unnecessary Re-renders
- React.memo: Prevents re-rendering if props are unchanged.
const MyComponent = React.memo(({ data }) => <div>{data}</div>); - useMemo: Memoizes expensive calculations.
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); - useCallback: Memoizes functions to prevent re-creation.
const memoizedCallback = useCallback(() => doSomething(a, b), [a, b]); - Optimize State: Avoid unnecessary state updates or deep object mutations.
- Lazy Loading: Load components or data only when needed (see Q2).
14. Testing with Jest and React Testing Library
- Jest: A test runner that provides assertions, mocks, and test suites.
- React Testing Library (RTL): Focuses on testing user interactions, not implementation details.
- Example:
import { render, screen, fireEvent } from '@testing-library/react'; import Button from './Button'; test('button click updates text', () => { render(<Button />); const button = screen.getByRole('button', { name: /click me/i }); fireEvent.click(button); expect(screen.getByText('Clicked!')).toBeInTheDocument(); });
15. Debugging Issues in an Application
- React DevTools: Inspect component hierarchy, props, and state.
- Browser DevTools: Check network requests, console logs, and performance.
- Debugging Tools:
- Use
console.logordebuggerto trace code execution. - Redux DevTools for action/state history.
- Use
- Steps:
- Reproduce the issue consistently.
- Check console for errors or warnings.
- Use breakpoints to inspect variables.
- Isolate the issue (e.g., test components in Storybook).
- Verify props/state changes and API responses.
16. Past Projects, Use Cases, Challenges
Since I don’t have access to your past projects, here’s how to approach this:
- Use Cases: Describe key features (e.g., real-time chat, form validation, or API-driven dashboards).
- Challenges:
- Managing complex state (solved with Redux or Context).
- Performance issues (optimized with lazy loading or memoization).
- Cross-browser compatibility (used polyfills or feature detection).
- Example Answer: “In a recent e-commerce project, I implemented a cart system using Redux for state management. A challenge was handling async API calls for stock updates, which I solved using
redux-thunkand optimistic updates.”
17. Restricting Sensitive Content and Permissions
- Role-Based Access Control (RBAC):
- Store user roles (e.g., admin, viewer) in state (Redux/Context) or JWT.
- Conditionally render components based on roles.
const { role } = useSelector((state) => state.user); return role === 'admin' ? <EditButton /> : <ViewOnly />;
- Edit vs. View:
- Use separate components or disable inputs for viewers.
- Example:
function Content({ user }) { return ( <div> {user.role === 'editor' ? ( <input type="text" onChange={handleEdit} /> ) : ( <p>View-only content</p> )} </div> ); }
- Backend Validation: Always validate permissions on the server to prevent unauthorized access.
18. Building a Secure Authentication System
- Frontend:
- Use a form with controlled inputs for username/password.
- Validate inputs (e.g., email format, password strength).
- Use HTTPS for secure communication.
- Backend:
- Hash passwords with
bcryptbefore storing. - Use JWT or OAuth for token-based authentication.
- Implement refresh tokens for session management.
- Hash passwords with
- Security Practices:
- Prevent XSS by sanitizing inputs (e.g., use
DOMPurify). - Prevent CSRF with tokens or
SameSitecookies. - Rate-limit login attempts to prevent brute-force attacks.
- Prevent XSS by sanitizing inputs (e.g., use
- Example:
function Login() { const [credentials, setCredentials] = useState({ email: '', password: '' }); const handleSubmit = async (e) => { e.preventDefault(); const response = await fetch('/api/login', { method: 'POST', body: JSON.stringify(credentials), }); const { token } = await response.json(); localStorage.setItem('token', token); // Store JWT }; return ( <form onSubmit={handleSubmit}> <input type="email" value={credentials.email} onChange={(e) => setCredentials({ ...credentials, email: e.target.value })} /> <input type="password" value={credentials.password} onChange={(e) => setCredentials({ ...credentials, password: e.target.value })} /> <button type="submit">Login</button> </form> ); }
19. Debugging Slow Form Input
- Steps:
- Profile Performance: Use React DevTools Profiler orstages the component.
- Check for Re-renders: Ensure the component isn’t re-rendering excessively (use
React.memooruseCallback). - Optimize State Updates: Avoid unnecessary state updates or complex computations in
onChange. - Debounce Input: Use a debouncing library (e.g.,
lodash.debounce) to reduce event frequency.
const debouncedOnChange = debounce((value) => setState(value), 300);- Simplify Logic: Move expensive logic out of the render cycle using
useMemo.
20. Handling File Uploads in a Form
- Use the
inputelement withtype="file". - Example:
function FileUpload() { const handleFileChange = (e) => { const file = e.target.files[0]; const formData = new FormData(); formData.append('file', file); fetch('/api/upload', { method: 'POST', body: formData, }); }; return <input type="file" onChange={handleFileChange} />; } - Handling Data:
- Use
FormDatato send files to the server. - Validate file size/type client-side before uploading.
- Display upload progress using
onUploadProgressinaxios. - Store file metadata in state or Redux for UI updates.
- Use
21. Unit vs. Integration Tests
- Unit Tests: Test a single unit (e.g., a function or component) in isolation.
- Example: Test a button’s
onClickhandler.
- Example: Test a button’s
- Integration Tests: Test interactions between components or systems (e.g., form submission and API response).
- Test Cases Example (for a login form):
- Unit: Test input validation, button click handler.
- Integration: Test form submission, API response handling, and UI updates.
22. Jest vs. React Testing Library
- Jest: A test runner providing assertions, mocks, and test organization.
- React Testing Library (RTL): A library for testing React components by simulating user interactions.
- Difference:
- Jest is a general-purpose testing framework; RTL is specific to React, focusing on user-centric testing.
- Jest provides the environment; RTL provides utilities for rendering and querying components.
- Which is Better?: They’re complementary. Use Jest with RTL for React apps to leverage Jest’s test runner and RTL’s user-focused testing approach.
23. Ternary Operator vs. && for Conditional Rendering
- Ternary Operator (
condition ? <ComponentA /> : <ComponentB />):- Pros: Explicitly defines both outcomes, clearer for complex conditions.
- Cons: More verbose.
- Logical AND (
condition && <Component />):- Pros: Concise for rendering something or nothing.
- Cons: Less explicit, can lead to subtle bugs (e.g., rendering
0orfalse).
- Preference: Use
&&for simple “render or nothing” cases; use ternary for mutually exclusive components.
24. Transforming Any Value to Boolean
Use the double NOT operator (!!) or Boolean():
const value = 'hello';
const bool = !!value; // true
const bool2 = Boolean(value); // true!!coerces any value to a boolean (null,undefined,0,''→false; others →true).
25. Scenario Causing an Infinite Loop
An infinite loop in React can occur due to:
- Incorrect
useEffectDependencies:useEffect(() => { setState(state + 1); // Updates state every render }, [state]);- Fix: Remove
statefrom dependencies or use a condition to prevent updates.
- Fix: Remove
- State Updates in Render: Calling
setStatedirectly in the render function.function Component() { const [state, setState] = useState(0); setState(state + 1); // Infinite loop return <div>{state}</div>; }
26. Checking Accessibility of an Application
- Tools:
- Lighthouse: Run accessibility audits in Chrome DevTools.
- axe DevTools: Browser extension for accessibility testing.
- ESLint Plugins: Use
eslint-plugin-jsx-a11yto catch accessibility issues in code.
- Practices:
- Ensure semantic HTML (e.g., use
<button>instead of<div>for buttons). - Add ARIA attributes for screen readers (e.g.,
aria-label). - Test with screen readers (e.g., NVDA, VoiceOver).
- Use tools like
react-axeto log accessibility violations during development.
- Ensure semantic HTML (e.g., use
If you need further clarification or more detailed examples for any of these topics, let me know!