skip to Main Content

I am trying to resolve the issue with the useTransition hook. I have a button and onClick of it fires a function that makes an API call. The issue hee is that the isPending from the useTransition hook is set to false before completion of the API call.

function App() {
  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
  const [isPending, startTransition] = useTransition();
  const [data, setData] = useState(null);
  useEffect(()=>{
    console.log("Is Pending changed", isPending)
  },[isPending])
  const fetchData = async () => {
    startTransition(async () => {
      await delay(2000);
      const response = await fetch('https://jsonplaceholder.typicode.com/todos/1'); // Replace with your API endpoint
      const result = await response.json();
      setData(result);
    });
  };

  return (
    <div>
      <button onClick={fetchData}>Fetch Data</button>
      {isPending ? (
        <p>Fetching data...</p>
      ) : (
        data && <pre>{JSON.stringify(data, null, 2)}</pre>
      )}
    </div>
  );

}

I expect that the useTransisition should wait for the completion of the API call as explained in the documentation. Am I doing something wrong?

2

Answers


  1. useTransition didn’t support asynchronous functions before React 19. Make sure to use React 19 if you want to use asynchronous functions as your transition. Skip to the end of the answer if you are using an older React version.


    React 19

    From the useTransition docs:

    Parameters

    action: A function that updates some state by calling one or more set functions. React calls action immediately with no parameters and marks all state updates scheduled synchronously during the action function call as Transitions. Any async calls that are awaited in the action will be included in the Transition, but currently require wrapping any set functions after the await in an additional startTransition (see Troubleshooting). State updates marked as Transitions will be non-blocking and will not display unwanted loading indicators.

    And further down in the Troubleshooting section:

    React doesn’t treat my state update after await as a Transition

    When you use await inside a startTransition function, the state updates that happen after the await are not marked as Transitions. You must wrap state updates after each await in a startTransition call:

    startTransition(async () => {
      await someAsyncFunction();
      // ❌ Not using startTransition after await
      setPage('/about');
    });
    

    However, this works instead:

    startTransition(async () => {
      await someAsyncFunction();
      // ✅ Using startTransition *after* await
      startTransition(() => {
        setPage('/about');
      });
    });
    

    This is a JavaScript limitation due to React losing the scope of the
    async context. In the future, when AsyncContext is available, this
    limitation will be removed.

    So to get your state updated after the transition, change your code to:

    const fetchData = () => {
      startTransition(async () => {
        await delay(2000);
        const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
        const result = await response.json();
        startTransition(() => {
          setData(result);
        });
      });
    };
    

    (Note that you don’t need the outer async keyword; fetchData itself does not await anything)


    React 18

    With older versions of React, useTransition didn’t support asynchronous functions. See the docs for useTransition in React 18:

    The function you pass to startTransition must be synchronous. React immediately executes this function, marking all state updates that happen while it executes as Transitions. If you try to perform more state updates later (for example, in a timeout), they won’t be marked as Transitions.

    And later in the troubleshooting section:

    React doesn’t treat my state update as a Transition

    When you wrap a state update in a Transition, make sure that it
    happens during the startTransition call:

    startTransition(() => {
      // ✅ Setting state *during* startTransition call
      setPage('/about');
    });
    

    The function you pass to startTransition must be synchronous.

    You can’t mark an update as a Transition like this:

    startTransition(() => {
      // ❌ Setting state *after* startTransition call
      setTimeout(() => {
        setPage('/about');
      }, 1000);
    });
    

    Instead, you could do this:

    setTimeout(() => {
      startTransition(() => {
        // ✅ Setting state *during* startTransition call
        setPage('/about');
      });
    }, 1000);
    

    Similarly, you can’t mark an update as a Transition like this:

    startTransition(async () => {
      await someAsyncFunction();
      // ❌ Setting state *after* startTransition call
      setPage('/about');
    });
    

    However, this works instead:

    await someAsyncFunction();
    startTransition(() => {
      // ✅ Setting state *during* startTransition call
      setPage('/about');
    });
    

    The latter won’t help in your case, so you are left with managing the pending state yourself:

    const [loading, setLoading] = useState(false);
    const fetchData = async () => {
      try {
        setLoading(true);
        await delay(2000);
        const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
        const result = await response.json();
        setData(result);
      } finally {
        setLoading(false);
      }
    };
    
    Login or Signup to reply.
  2. useTransition doesn’t directly depend on how long (time) a function takes to run, like a delay or waiting time. Instead, it depends on how complex or lengthy a render is. If that state update leads to expensive or complex rendering, React will show isPending as true while it’s working on that render.

    import React, { useTransition, useState, useEffect } from "react";
    
    export default function App() {
      const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
      
      const [isPending, startTransition] = useTransition();
      const [data, setData] = useState(null);
    
      useEffect(() => {
        console.log("Is Pending changed:", isPending);
      }, [isPending]);
    
      const fetchData =  () => {
        
           startTransition (async () => {
                const response = await fetch('https://jsonplaceholder.typicode.com/todos/'); // Replace with your API endpoint
                      startTransition(async () => {
                          const result = await response.json();
                          startTransition(() => {
                               setData(result);
                         })
                      }) 
         
           }) 
         
         };
    
      return (
        <div>
          <button onClick={fetchData}>Fetch Data</button>
          
          {isPending ? (
            <p>Fetching data...</p>
          ) : data ? (
            <div>
             
            
              {data.map((item, i) => 
                <div>
                <div key={i}>Item {i} </div>
                       <div key={item.id}>{item.title }</div>
                    <div key={i}>Item {i} </div>
                       <div key={item.id}>{item.title }</div>
                    <div key={i}>Item {i} </div>
                       <div key={item.id}>{item.title }</div>
                    <div key={i}>Item {i} </div>
                       <div key={item.id}>{item.title }</div>
                    </div>
            )
                
              }
            </div>
          ) : (
            <p>No data yet. Click "Fetch Data" to start.</p>
          )}
        </div>
      );
    }
    

    here’s an example of your code that can lead to isPending to be true, i rendered the same value multiple times from the Api call so the rendering process will be more expensive

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search