RORK LABJP
PLAY — Google Play's target API level 36 requirement took effect yesterday, August 31. From today, new apps and updates must target Android 16VISIBILITY — Apps still on API 35 stay listed but disappear for users on newer Android versions. No error is raised; new installs simply fade, which makes the change easy to missEXTENSION — If you missed the deadline, an extension through November 1, 2026 can be requested in Play Console — best filed alongside a concrete migration planAPPLE — On the Apple side, the event lands September 9 and iOS 27 is reported to ship September 14. Testing generated apps on iOS 27 hardware before release week is time well spentEXPO — Expo released expo-paste-input on August 28, a native module that brings image, GIF, and sticker paste to React Native TextInputEAS — EAS Observe reached general availability on August 20, putting crash and performance monitoring on the same EAS platform as builds and updatesPLAY — Google Play's target API level 36 requirement took effect yesterday, August 31. From today, new apps and updates must target Android 16VISIBILITY — Apps still on API 35 stay listed but disappear for users on newer Android versions. No error is raised; new installs simply fade, which makes the change easy to missEXTENSION — If you missed the deadline, an extension through November 1, 2026 can be requested in Play Console — best filed alongside a concrete migration planAPPLE — On the Apple side, the event lands September 9 and iOS 27 is reported to ship September 14. Testing generated apps on iOS 27 hardware before release week is time well spentEXPO — Expo released expo-paste-input on August 28, a native module that brings image, GIF, and sticker paste to React Native TextInputEAS — EAS Observe reached general availability on August 20, putting crash and performance monitoring on the same EAS platform as builds and updates
Articles/Dev Tools
Dev Tools/2026-04-19Intermediate

Rork App Loading Spinner Never Stops — Fixing Async State Update Issues

Learn why Rork app loading spinners get stuck and data never appears. Covers try/catch/finally patterns, useEffect dependency array issues, and response structure mismatches with practical code fixes.

Rork548Troubleshooting39useState2useEffect2AsyncAPI7Debugging11

"The API is clearly returning data — I can see it in the logs — but the spinner just keeps spinning." If you've built an app with Rork and done real-device testing, you've probably run into this exact situation at least once.

No error message. Logs look fine. But the screen doesn't update. And when you ask the AI to fix it, the same problem keeps coming back. That's what makes this category of bug so frustrating — it hides well.

This article breaks down the most common reasons loading states get stuck in Rork-generated React Native code, and shows you exactly how to fix each one. Even if you're not comfortable reading the code yourself, you'll know what to tell Rork to change.

Why This Happens

Rork generates async/await code for data fetching. The flow looks like this:

  1. Screen mounts
  2. isLoading is set to true, spinner appears
  3. Data is fetched from an API or database
  4. isLoading is set to false, spinner disappears and data renders

The problem is step 4. If something goes wrong during step 3 — a network error, an empty response, an unexpected data structure — step 4 might never execute. When isLoading never gets set back to false, the spinner runs forever.

Pattern 1: Loading Never Clears on Error

This is the most common cause. The code uses try/catch, but doesn't include a finally block.

// Problem: if an error is thrown, setIsLoading(false) is never reached
const fetchData = async () => {
  setIsLoading(true);
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    setItems(data);
    setIsLoading(false); // skipped entirely if an error occurs above
  } catch (error) {
    console.error(error);
    // setIsLoading(false) is missing here
  }
};

When an error is thrown, execution jumps to catch, and setIsLoading(false) inside try is never called. The fix is a finally block.

// Fix: finally runs whether try succeeds or catch fires
const fetchData = async () => {
  setIsLoading(true);
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    setItems(data);
  } catch (error) {
    console.error('Fetch error:', error);
    setError('Failed to load data');
  } finally {
    setIsLoading(false); // always executes, success or failure
  }
};

finally runs no matter what — whether try completes successfully or catch handles an error. Always put your loading reset here.

To ask Rork to fix this: "Update the fetchData function to move setIsLoading(false) into a finally block so it always runs, even when an error occurs."

Pattern 2: Conditional Branches That Skip the Loading Reset

When you branch on the fetched data, some code paths can miss the loading reset entirely.

// Problem: when data is empty, setIsLoading(false) is never called
const fetchUserData = async (userId: string) => {
  setIsLoading(true);
  const data = await getUserData(userId);
 
  if (data && data.length > 0) {
    setUserList(data);
    setIsLoading(false); // only called if data has items
  }
  // if data is empty, loading never clears
};

Move the loading reset outside the conditional, or wrap everything in try/finally.

// Fix: loading clears regardless of what data looks like
const fetchUserData = async (userId: string) => {
  setIsLoading(true);
  try {
    const data = await getUserData(userId);
    if (data && data.length > 0) {
      setUserList(data);
    } else {
      setEmptyMessage('No users found');
    }
  } catch (error) {
    setError('Failed to load users');
  } finally {
    setIsLoading(false);
  }
};

Pattern 3: Infinite Loop from useEffect Dependency Array

If loading starts, stops, then immediately starts again — you probably have a useEffect dependency issue.

// Problem: lastFetched is updated inside the effect, triggering it again endlessly
useEffect(() => {
  fetchData();
  setLastFetched(new Date());
}, [lastFetched]); // every time lastFetched changes, this fires again

When a value in the dependency array gets modified inside the effect itself, it creates an infinite loop.

// Run only once when the component mounts
useEffect(() => {
  fetchData();
}, []);
 
// Run only when a specific value changes (that isn't modified inside the effect)
useEffect(() => {
  if (userId) {
    fetchData(userId);
  }
}, [userId]); // userId changes trigger a re-fetch, but userId isn't set inside this effect

Check your dependency array: if any value listed there is also being updated inside the same useEffect, that's your infinite loop.

Pattern 4: Response Structure Doesn't Match What the Code Expects

Sometimes the API returns 200, data arrives, and there's still nothing on screen. The fetched data's shape doesn't match what the code is trying to access.

// API response shape: { success: true, result: { items: [...] } }
 
// Wrong — data.items is undefined
const data = await response.json();
setItems(data.items);
 
// Correct
setItems(data.result.items);

The fastest way to diagnose this is console.log.

const fetchData = async () => {
  setIsLoading(true);
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
 
    // Log the actual shape so you can see what's coming back
    console.log('API response:', JSON.stringify(data, null, 2));
 
    // Then update the accessor to match what you see
    setItems(data.result.items);
  } catch (error) {
    console.error(error);
  } finally {
    setIsLoading(false);
  }
};

Check the output in the Rork Companion console or your Expo dev server terminal. Once you know the actual structure, you can tell Rork: "Update the setItems call to use data.result.items instead of data.items."

Pattern 5: setState Called After Component Unmounts

If you navigate away from a screen quickly, you might see: "Can't perform a React state update on an unmounted component." This means an async operation completed after the component was already gone.

In modern React/Expo, this is often just a warning rather than a hard failure, but it can cause memory leaks and subtle state bugs in complex apps.

// Fix: track mount state and skip setState if already unmounted
useEffect(() => {
  let isMounted = true;
 
  const fetchData = async () => {
    setIsLoading(true);
    try {
      const data = await getData();
      if (isMounted) {
        setItems(data);
      }
    } finally {
      if (isMounted) {
        setIsLoading(false);
      }
    }
  };
 
  fetchData();
 
  return () => {
    isMounted = false; // cleanup runs when component unmounts
  };
}, []);

To request this from Rork: "Add an isMounted flag to the useEffect so that setState calls are skipped if the component has already unmounted. Return a cleanup function that sets isMounted to false."

How to Tell Rork What to Fix

A few prompts that reliably get the right changes:

For a stuck loading spinner: "The loading state never clears when an error occurs. Add a finally block to the fetchData function and move setIsLoading(false) into it."

For infinite re-fetching: "The useEffect is running in a loop. Check the dependency array and remove any values that are being modified inside the effect. Use an empty array if the fetch should only run once."

For missing data: "Add console.log(JSON.stringify(data, null, 2)) inside fetchData after the await, then tell me what the API response structure looks like."

Debugging Checklist

When loading won't stop or UI won't update, work through these in order.

First, look for setIsLoading(false) calls that only exist inside try, not in finally. Moving them to finally fixes the majority of stuck-spinner bugs.

Second, add a console.log to see the actual API response. Mismatched data structures are more common than you'd expect, and the log reveals them immediately.

Third, audit your useEffect dependency arrays. Any value in the array that's also being set inside the effect is a potential infinite loop.

If none of those find it, ask Rork to show you the entire fetchData function so you can trace the data flow from fetch to render.

Async bugs appear in AI-generated code and hand-written code alike. "It's not erroring, it just doesn't work" almost always comes down to one of these patterns. Work through them one at a time and you'll find it.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $15 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Dev Tools2026-04-22
FlatList Renders Blank in Rork? A Calm, Ordered Way to Debug Empty Lists
When a FlatList in your Rork-generated app shows nothing at all, the cause is usually a handful of very specific things. Here's the order I check them in, based on bugs I've actually shipped and fixed.
Dev Tools2026-07-24
Collapsing Duplicate Requests Into One: A Reference-Counted Single-Flight Layer
When several components fire the same API call at launch, you get a burst of identical requests. Here is a single-flight layer that shares one in-flight promise instead: how to build the key that decides the folding, the trap of handing out a failed promise forever, the trap of one caller's abort cancelling everyone, and the test that keeps it all from regressing, with the real network numbers alongside.
Dev Tools2026-07-15
A Yellow Warning in Dev, a Crash on Resume — Functions in Route Params
Rork generated navigation code that put a function and a Date into route params. In development it was only a warning. After the OS reclaimed the process, resuming the app crashed with params.onFavorite is not a function. Here is the cause and the fix.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →