logo
React Hooks - Interview Questions and Answers
How do you handle asynchronous operations with Hooks?
Handling Asynchronous Operations with Hooks in React

React Hooks don't have built-in support for asynchronous operations, but you can handle them using async functions, Promises, or libraries like axios and fetch. Here’s how you can manage async operations effectively.


1. Using useEffect for Side Effects (Fetching Data)

Since useEffect runs after render, it’s a great place to fetch data from an API.

Example: Fetching Data with fetch
import { useState, useEffect } from "react";

function FetchData() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");
        if (!response.ok) throw new Error("Failed to fetch");
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, []); // Empty dependency array → runs once

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;
  return <p>Title: {data?.title}</p>;
}
* Key Takeaways :
  • Fetch inside useEffect to run the request when the component mounts.
  • Use async/await inside a function (not directly in useEffect).
  • Handle loading and error states properly.

2. Handling Asynchronous State Updates

State updates using useState are synchronous in React, but you can use useEffect to react to state changes asynchronously.

import { useState, useEffect } from "react";

function AsyncCounter() {
  const [count, setCount] = useState(0);
  const [doubleCount, setDoubleCount] = useState(0);

  useEffect(() => {
    const timer = setTimeout(() => {
      setDoubleCount(count * 2);
    }, 500);

    return () => clearTimeout(timer); // Cleanup
  }, [count]); // Runs when count changes

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <p>Count: {count}</p>
      <p>Double (delayed): {doubleCount}</p>
    </div>
  );
}
* Why use useEffect?
  • Prevents race conditions by cleaning up previous timers before setting a new one.
  • Ensures derived state updates asynchronously.

3. Using useCallback for Asynchronous Functions

If you're passing an async function as a prop to child components, wrap it in useCallback to prevent unnecessary re-renders.

import { useState, useCallback } from "react";

function FetchButton({ fetchData }) {
  return <button onClick={fetchData}>Fetch Data</button>;
}

function ParentComponent() {
  const [data, setData] = useState(null);

  const fetchData = useCallback(async () => {
    const response = await fetch("https://jsonplaceholder.typicode.com/posts/2");
    const result = await response.json();
    setData(result);
  }, []);

  return (
    <div>
      <FetchButton fetchData={fetchData} />
      <p>Data: {data?.title}</p>
    </div>
  );
}
* Why use useCallback?
  • Prevents unnecessary re-renders of child components that depend on fetchData.

4. Using useReducer for Complex Async State Logic

For managing complex async state (e.g., loading, success, failure states), use useReducer.

import { useReducer, useEffect } from "react";

const initialState = {
  loading: true,
  data: null,
  error: null,
};

function reducer(state, action) {
  switch (action.type) {
    case "SUCCESS":
      return { loading: false, data: action.payload, error: null };
    case "ERROR":
      return { loading: false, data: null, error: action.payload };
    default:
      return state;
  }
}

function FetchWithReducer() {
  const [state, dispatch] = useReducer(reducer, initialState);

  useEffect(() => {
    async function fetchData() {
      try {
        const response = await fetch("https://jsonplaceholder.typicode.com/posts/3");
        const result = await response.json();
        dispatch({ type: "SUCCESS", payload: result });
      } catch (err) {
        dispatch({ type: "ERROR", payload: err.message });
      }
    }
    fetchData();
  }, []);

  if (state.loading) return <p>Loading...</p>;
  if (state.error) return <p>Error: {state.error}</p>;
  return <p>Title: {state.data?.title}</p>;
}
* Why useReducer?
  • Better for handling complex async state transitions (loading, success, failure).
  • Makes state updates more predictable.

5. Using useContext for Asynchronous Global State

For fetching data once and sharing it globally, use useContext with a provider.

import { createContext, useContext, useState, useEffect } from "react";

const DataContext = createContext();

function DataProvider({ children }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    async function fetchData() {
      const response = await fetch("https://jsonplaceholder.typicode.com/posts/4");
      const result = await response.json();
      setData(result);
    }
    fetchData();
  }, []);

  return <DataContext.Provider value={data}>{children}</DataContext.Provider>;
}

function DisplayData() {
  const data = useContext(DataContext);
  return <p>Title: {data?.title || "Loading..."}</p>;
}

function App() {
  return (
    <DataProvider>
      <DisplayData />
    </DataProvider>
  );
}
* Why useContext?
  • Avoids prop drilling when sharing async data across multiple components.

Best Practices for Handling Async in Hooks :
Best Practice Do This
Use useEffect for API calls Fetch data inside useEffect
Use async inside functions, not useEffect directly Define async functions inside useEffect
Handle errors properly Use try/catch and setError states
Cleanup side effects Return a cleanup function inside useEffect
Use useCallback for async functions in props Prevent unnecessary re-renders
Use useReducer for complex state updates Manage loading, success, and error states