RORK LABJP
TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the roundTOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
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.

Rork515Troubleshooting38useState2useEffect2AsyncAPI6Debugging11

"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-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-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.
Dev Tools2026-07-14
Adding a Single Zod Validation Boundary to Rork's Generated Fetch Code
The network code Rork generates implicitly trusts the shape of the response. When the API shifts, the screen quietly goes blank. Here is how to slip a single Zod parse layer between the generated UI and the network to make failures predictable, with numbers from real operation.
📚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 →