Understanding RTK Query: A Real-World Implementation in React Native Application

Table of contents

Introduction
I've been developing a React Native app called Geomusic that allows users to pin Spotify playlists to geographical locations. For efficient data fetching from Spotify's API, I chose RTK Query—a powerful data fetching and caching solution built on Redux Toolkit that eliminates the need to write data fetching logic by hand.
What is RTK Query?
RTK Query provides:
Automatic caching with intelligent cache invalidation
Background refetching capabilities
Error handling and loading states
TypeScript support out of the box
Minimal boilerplate code
The Implementation
Here's my complete Spotify playlist query implementation with detailed comments:
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { TOKEN_STORAGE_KEY } from '@/features/authentication/providers/SpotifyAuthProvider';
export const spotifyPlaylistsQuery = createApi({
// Unique key for this API slice in the Redux store
reducerPath: 'spotifyPlaylistsQuery',
// Configure the base query with Spotify API settings
baseQuery: fetchBaseQuery({
// Base URL for all Spotify API endpoints
baseUrl: 'https://api.spotify.com/v1/',
// Function that runs before every request to add headers
prepareHeaders: async (headers) => {
// Retrieve stored Spotify access token from AsyncStorage
const token = await AsyncStorage.getItem(TOKEN_STORAGE_KEY);
if (token) {
// Add Bearer token for authentication
headers.set('Authorization', `Bearer ${token}`);
// Set content type for JSON requests
headers.set('Content-Type', 'application/json');
}
return headers;
},
}),
// Define the API endpoints
endpoints: (builder) => ({
// Endpoint to fetch user's playlists
getPlaylists: builder.query<PlaylistItem[], void>({
// API endpoint path (gets appended to baseUrl)
query: () => 'me/playlists',
// Transform the response to extract only the items array
transformResponse: (response: SpotifyPlaylistsResponse) => response.items,
}),
// Endpoint to fetch a specific playlist by ID
getPlaylistById: builder.query<SpotifyPlaylistDetailResponse, string>({
// Dynamic endpoint path using the playlistId parameter
query: (playlistId) => `playlists/${playlistId}`,
}),
}),
});
// Export auto-generated hooks for use in components
export const { useGetPlaylistsQuery, useGetPlaylistByIdQuery } = spotifyPlaylistsQuery;
Store Setup
import { configureStore } from '@reduxjs/toolkit';
import { setupListeners } from '@reduxjs/toolkit/query';
import { spotifyPlaylistsQuery } from '@/redux/queries/spotifyPlaylistQuery';
export const store = configureStore({
reducer: {
// Add the API slice reducer to the store
// The key must match the reducerPath from the API definition
[spotifyPlaylistsQuery.reducerPath]: spotifyPlaylistsQuery.reducer,
// Add other reducers here as needed
// geoPlaylists: playlistsReducer,
},
// Add RTK Query middleware to enable caching, invalidation, polling, etc.
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
// Configure serializable check to ignore RTK Query actions
serializableCheck: {
ignoredActions: ['persist/PERSIST', 'persist/REHYDRATE'],
ignoredPaths: [`${spotifyPlaylistsQuery.reducerPath}.queries`],
},
}).concat(spotifyPlaylistsQuery.middleware), // Add the API middleware
});
// Enable useful features like refetchOnFocus and refetchOnReconnect
setupListeners(store.dispatch);
// Export types for TypeScript usage
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
Provider Setup
To use RTK Query in your React Native app, wrap your app with the Redux Provider:
import React from 'react';
import { Provider } from 'react-redux';
import { store } from '@/redux/store';
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
// Provide the Redux store to all child components
<Provider store={store}>
<Stack>
<Stack.Screen name="playlists" options={{ headerShown: false }} />
{/* Add other screens here */}
</Stack>
</Provider>
);
}
The Provider component makes the Redux store (and RTK Query) available to all components in your app, enabling them to use the generated hooks like useGetPlaylistsQuery
.
Real-World Usage
Here's how RTK Query is used in the actual PlaylistsScreen component:
import React from 'react';
import { SafeAreaView } from 'react-native';
import { useGetPlaylistsQuery } from '@/redux/queries/spotifyPlaylistQuery';
const PlaylistsScreen = () => {
const { error, data, isFetching, isLoading } = useGetPlaylistsQuery();
if (isFetching || isLoading) {
return (
<SafeAreaView>
<ThemedText>Loading</ThemedText>
</SafeAreaView>
);
}
return (
<SafeAreaView>
<Header headerText="Select a playlist to pin" />
<Playlists playlists={data} />
</SafeAreaView>
);
};
Key Benefits
1. Automatic Caching and Performance
When users browse different playlists, RTK Query automatically caches results. Returning to the PlaylistsScreen loads data instantly from cache rather than making another API call.
2. Built-in Loading States
The app gracefully handles loading states while data is being fetched, with automatic state management.
3. Background Refetching
RTK Query automatically refetches data when:
The app regains focus
Network connection is restored
Cache becomes stale
Caching in Action
First Request:
Component calls
useGetPlaylistByIdQuery('playlist-id')
RTK Query checks cache - no data found
Makes network request to Spotify API
Stores response in cache
Second Request (same ID):
Component calls
useGetPlaylistByIdQuery('playlist-id')
againRTK Query finds cached data
No network request - returns cached data immediately
This automatic caching makes the app faster and reduces API calls, which is crucial for rate-limited APIs like Spotify's.
Before vs After RTK Query
Before (Traditional Approach):
const [playlists, setPlaylists] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const fetchPlaylists = async () => {
setLoading(true);
try {
const token = await AsyncStorage.getItem(TOKEN_STORAGE_KEY);
const response = await fetch('https://api.spotify.com/v1/me/playlists', {
headers: { Authorization: `Bearer ${token}` }
});
const data = await response.json();
setPlaylists(data.items);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
After RTK Query:
const { error, data, isFetching, isLoading } = useGetPlaylistsQuery();
Conclusion
RTK Query transformed Geomusic's data fetching from complex manual state management to simple, declarative hooks. The results:
Automatic caching - instant loading for previously fetched data
Built-in loading states - no more manual loading/error state management
Better performance - request deduplication and background refetching
TypeScript integration - full type safety out of the box
For any React Native app that consumes APIs, RTK Query eliminates the tedious parts of data fetching while delivering enterprise-grade features automatically.
Subscribe to my newsletter
Read articles from Musango Wope directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
