Home / Articles / How React Query Cut 500 Lines From a React Native App's API Layer

This article is published in English.

How React Query Cut 500 Lines From a React Native App's API Layer

A real-world React Native case study showing how switching from manual useEffect data-fetching to React Query eliminated repetitive code and improved caching and offline handling.

1364 words

Introduction

Some time back, a React Native codebase you might recognize ran into a familiar issue.

Nearly every screen that needed remote data followed the same pattern:

  • Data-fetching calls tucked inside useEffect
  • Several separate loading flags
  • Custom error-handling blocks
  • Hand-rolled pull-to-refresh logic
  • Manual retry mechanisms
  • Ad-hoc local caching workarounds

Functionally, none of this was broken. But keeping it consistent across dozens of screens turned into a real maintenance burden.

Switching to React Query (from TanStack) let the team strip out a large amount of repetitive networking code while getting better caching, cleaner loading management, and more reliable offline behavior in return. The library ships with built-in query caching, automatic background refetching, and network-aware logic, which removes most of the need to hand-write that state machinery yourself.

What follows is a side-by-side look at the old manual approach versus the React Query version, using patterns drawn from real production React Native screens.

The Problem With Traditional API Management

A typical screen's setup looked roughly like this:

const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState(null);

useEffect(() => {
  fetchData();
}, []);
const fetchData = async () => {
  try {
    setLoading(true);
    const response = await api.getPosts();
    setData(response);
  } catch (err) {
    setError(err);
  } finally {
    setLoading(false);
  }
};

This same boilerplate showed up again and again throughout the app.

Each screen needed to independently manage:

  • A loading flag
  • An error flag
  • Retry handling
  • Pull-to-refresh logic
  • Its own caching approach
  • Manual refetch triggers

As the app kept growing, this repeated pattern became harder and harder to keep consistent.

Enter React Query

That same screen, rewritten with React Query, collapsed down to this:

const { data, isLoading, error, refetch } = useQuery({
  queryKey: ["posts"],
  queryFn: fetchPosts,
});

That's the whole implementation.

React Query takes care of all of the following automatically:

  • Loading indicators
  • Error states
  • Deduplicating identical in-flight requests
  • Refetching data in the background
  • Managing the cache
  • Retrying failed requests
  • Reacting to network reconnection

Most of this behavior comes for free out of the box, and it can be tuned through options like staleTime, gcTime, retry configuration, and refetch settings.

Comparison #1: Network Requests

Before React Query

Picture three separate screens that all need the same user profile data.

Without any shared caching layer, each one fires its own request:

Profile Screen → API Call
Settings Screen → API Call
Dashboard Screen → API Call

The outcome: three separate network calls for the same data.

With React Query

Profile Screen → API Call
Settings Screen → Cached Data
Dashboard Screen → Cached Data

The outcome this time: just one network call.

React Query stores results under a query key and shares that cached data across every component that asks for it. Any screen that requests the same key afterward gets the cached value instantly, while a background refetch can silently keep it up to date.

Production Result

On screens users visit often, this translates into:

  • Far fewer duplicate requests hitting the API
  • Lower load on backend servers
  • Snappier transitions between screens

Comparison #2: Caching Efficiency

Caching is arguably where React Query delivers the most value.

When the same query is requested again before its cached copy goes stale:

useQuery({
  queryKey: ["products"],
  queryFn: getProducts,
  staleTime: 300000,
});

The interface can show that cached data immediately, with React Query optionally refreshing it quietly in the background. Cached entries are kept around and eventually garbage-collected according to settings you control.

Real Example

Consider an e-commerce app's product list screen.

Without caching, opening the screen means:

Open Products
↓
Network Request
↓
Navigate Back
↓
Open Products Again
↓
Network Request

With React Query in place, the same flow becomes:

Open Products
↓
Network Request
↓
Navigate Back
↓
Open Products Again
↓
Instant Cached Data

The difference is that the app feels noticeably snappier to the person using it.

Comparison #3: Loading States

Before adopting React Query, tracking loading state meant juggling several booleans:

const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [isRetrying, setIsRetrying] = useState(false);

Afterward, a single hook call exposes everything needed:

const {
  isLoading,
  isFetching,
  isRefetching,
} = useQuery(...)

React Query distinguishes between the very first load and any subsequent background fetch, which makes the UI logic around it far simpler to reason about.

Real Benefit

Rather than showing a full-screen spinner on every single fetch, the app can differentiate:

  • Initial load: full-screen loader
  • Background refresh: a small, unobtrusive spinner
  • Data already cached: no visible interruption at all

The result feels considerably more responsive to whoever is using the app.

Comparison #4: Offline Support

Offline behavior is one of those things teams tend to underestimate.

Handling it manually usually looks like this:

Check Connectivity
Pause Requests
Retry Later
Handle Errors
Refetch On Reconnect

That means bespoke connectivity logic sprinkled across the codebase.

React Query instead offers built-in online and offline management, along with refetching that reacts to reconnection events. Inside React Native, you can wire this up using onlineManager together with the platform's network status listeners.

For example:

onlineManager.setEventListener(...)

React Query can also be configured to run in offline-first modes, adjusting its network behavior accordingly.

Real Example

Take a news-reading app as an example.

Over the course of a day, the user's connection might look like:

  • Morning: online
  • Afternoon: offline
  • Evening: back online

Throughout that whole sequence, previously cached articles stay readable, and once the connection returns, fresh content can sync automatically.

Real-World Use Cases

1. News Applications

What this brings:

  • Cached articles
  • Automatic background refresh
  • Lower overall API traffic
  • The ability to keep reading while offline

2. E-Commerce Applications

What this brings:

  • Cached product listings
  • Prefetching categories ahead of time
  • Quicker navigation between sections
  • A noticeably smoother shopping experience

React Query also supports prefetching data before navigation even happens, cutting down on perceived wait times.

3. Dashboard Applications

What this brings:

  • Widgets that refresh themselves automatically
  • A cache shared across multiple screens
  • Reduced network chatter
  • Much simpler state management overall

This pattern fits analytics dashboards and admin panels particularly well.

What We Actually Removed

Once the migration wrapped up:

Removed

  • Custom-built loading state handling
  • Manual retry code
  • Duplicate outgoing API calls
  • Boilerplate for pull-to-refresh
  • Homegrown cache logic
  • Manual refetch management

Added

  • The React Query library itself
  • Query keys
  • A QueryClient instance

The net effect: roughly 500 lines of API-handling code were no longer needed.

When React Query Might Not Be Necessary

Reaching for React Query might be overkill if:

  • Your app makes only a handful of API calls
  • The underlying data barely changes
  • Caching genuinely isn't needed
  • Offline behavior doesn't matter for your use case

For most production apps, though, the benefits tend to outweigh the initial learning curve fairly quickly.

Final Thoughts

React Query is more than just another way to fetch data.

It functions as a full server-state management solution, cutting out repetitive API code while simultaneously improving caching, loading behavior, network efficiency, and offline experience.

The biggest payoff here wasn't raw performance.

It was the reduction in complexity.

Less code. Fewer bugs. A better experience for the people using the app.

That combination is what made the migration worth doing.