Skip to content

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() and Suspense to 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 like react-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, or Last-Modified to control browser or CDN caching.
    • Example: Cache-Control: max-age=3600 caches responses for 1 hour.
    • Use libraries like axios to respect cache headers or implement custom caching logic with fetch.
  • State-Based Caching:
    • Store API responses in Redux or Context to avoid redundant calls.
    • Use libraries like react-query or swr for 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 localStorage or sessionStorage for client-side caching.

4. Difference Between Using Redux and Context

AspectReduxContext
PurposeGlobal state management with predictable updates via actions/reducers.Share state across components without prop drilling.
ComplexityMore complex (store, reducers, actions, middleware).Simpler, built into React.
Use CaseLarge apps with complex state, frequent updates, or async operations.Small to medium apps with simple shared state (e.g., theme, user data).
ScalabilityScales well for large apps with many components and state changes.Can cause unnecessary re-renders in large apps if not optimized.
MiddlewareSupports middleware (e.g., redux-thunk) for async logic.No built-in middleware; requires custom solutions.
DebuggingDevTools 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 useMemo for expensive computations.
      const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
    • Use useCallback for functions passed as props.
      const memoizedCallback = useCallback(() => doSomething(a, b), [a, b]);
  • 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

AspectControlled ComponentsUncontrolled Components
DefinitionInput 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 ManagementState is managed in React (single source of truth).State is managed by the DOM; accessed via refs.
Use CaseForms with validation, dynamic updates, or conditional logic.Simple forms or when you need direct DOM access.
ProsPredictable, easier to validate and manipulate.Simpler for basic forms, less boilerplate.
ConsMore 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:

  1. 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.
  2. 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.
  3. Regression Suite:
    • Automated tests to ensure new changes don’t break existing functionality.
    • Run unit, integration, and end-to-end tests after code changes.
  4. 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

  1. Component Thinking (Storybook/Styleguidist):
    • Build reusable, isolated components and document them in tools like Storybook.
    • Example: Create a Button component with stories for different variants.
  2. ESLint:
    • Enforce code quality and consistency (e.g., no unused variables, consistent formatting).
    • Example: Configure ESLint with eslint-plugin-react and eslint-plugin-react-hooks.
  3. Tests in Pipeline:
    • Run unit/integration tests in CI/CD pipelines (e.g., GitHub Actions, Jenkins) to catch issues early.
  4. Trunk-Based Development:
    • Developers work on short-lived branches, merging frequently into a single trunk to reduce merge conflicts.
  5. Feature Toggles:
    • Use toggles to enable/disable features without deploying new code (e.g., using unleash or custom flags).

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.log or debugger to trace code execution.
    • Redux DevTools for action/state history.
  • Steps:
    1. Reproduce the issue consistently.
    2. Check console for errors or warnings.
    3. Use breakpoints to inspect variables.
    4. Isolate the issue (e.g., test components in Storybook).
    5. 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-thunk and 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 bcrypt before storing.
    • Use JWT or OAuth for token-based authentication.
    • Implement refresh tokens for session management.
  • Security Practices:
    • Prevent XSS by sanitizing inputs (e.g., use DOMPurify).
    • Prevent CSRF with tokens or SameSite cookies.
    • Rate-limit login attempts to prevent brute-force attacks.
  • 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:
    1. Profile Performance: Use React DevTools Profiler orstages the component.
    2. Check for Re-renders: Ensure the component isn’t re-rendering excessively (use React.memo or useCallback).
    3. Optimize State Updates: Avoid unnecessary state updates or complex computations in onChange.
    4. Debounce Input: Use a debouncing library (e.g., lodash.debounce) to reduce event frequency.
    const debouncedOnChange = debounce((value) => setState(value), 300);
    1. Simplify Logic: Move expensive logic out of the render cycle using useMemo.

20. Handling File Uploads in a Form

  • Use the input element with type="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 FormData to send files to the server.
    • Validate file size/type client-side before uploading.
    • Display upload progress using onUploadProgress in axios.
    • Store file metadata in state or Redux for UI updates.

21. Unit vs. Integration Tests

  • Unit Tests: Test a single unit (e.g., a function or component) in isolation.
    • Example: Test a button’s onClick handler.
  • 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 0 or false).
  • 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 useEffect Dependencies:
    useEffect(() => {
      setState(state + 1); // Updates state every render
    }, [state]);
    • Fix: Remove state from dependencies or use a condition to prevent updates.
  • State Updates in Render: Calling setState directly 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-a11y to 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-axe to log accessibility violations during development.

If you need further clarification or more detailed examples for any of these topics, let me know!

On this page