◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
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.

Rork585Troubleshooting43useStateuseEffectAsyncAPI7Debugging11

"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 $10 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-10-15
When EXPO_PUBLIC_ Values Read Fine in Dev but Come Back undefined in Production: Telling the Spellings Apart
The API URL reads fine while developing and turns undefined once the app is built. The cause was how process.env was written. I lined up babel-preset-expo's output for dev and production and tabulated which spellings get replaced and which are left behind.
⬡ Dev Tools2026-09-21
A build that stops with exit code 0 — the flag that silenced the log, and the check that let an empty file through
Two reports landed in the same week: eas build --local stopping with exit code 0 but produced no further output, and a zero-byte stub that never got embedded. Both came down to settings that reduce output. Here is how to audit your own scripts.
⬡ 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.