In this guide, we’ll cover:
- CRUD Operations
- Pagination
- Redux Persist with RTK Query
- Multiple Base URL Usage
- Protected and Public Routes
- Cache Management and Invalidation
RTK Query is an advanced data-fetching and caching tool built into Redux Toolkit (RTK). It streamlines API interactions by generating Redux slices and hooks for common tasks like fetching, caching, and updating data. Key features include:
- Automatic Caching: RTK Query caches data and automatically re-fetches it when data is invalidated, ensuring the UI always has the latest data.
- Cache Invalidation: Using tags, RTK Query lets you define when certain data should be re-fetched. This helps keep your cache fresh without manually updating data.
-
Auto-Generated Hooks: RTK Query creates hooks for each API endpoint, allowing you to call APIs using simple React hooks (
useGetPostsQuery
,useCreatePostMutation
, etc.). - Error Handling: Includes custom error handling through middleware, making it easy to catch and display errors.
- Simplified Redux Integration: RTK Query integrates directly with Redux, so you don’t need extra libraries for global state management or caching.
RTK Query vs. React Query
Both React Query and RTK Query provide solutions for data fetching and caching in React applications, but they have different strengths and use cases:
Feature | RTK Query | React Query |
---|---|---|
Purpose | Integrated within Redux for managing server data in Redux state. Best for apps already using Redux or requiring centralized global state. | Dedicated to managing server state with no Redux dependency. Great for apps focused on server state without Redux. |
Caching | Automatic caching with fine-grained cache invalidation through tags. Caches data globally within the Redux store. | Automatic caching with flexible cache control policies. Maintains a separate cache independent of Redux. |
Generated Hooks | Auto-generates hooks for endpoints, allowing mutations and queries using useQuery and useMutation hooks. |
Provides hooks (useQuery , useMutation ) that work independently from Redux, but require manual configuration of queries and mutations. |
DevTools | Integrated into Redux DevTools, making debugging seamless for Redux users. | Provides its own React Query DevTools, with detailed insight into query states and cache. |
Error Handling | Centralized error handling using Redux middleware. | Error handling within individual queries, with some centralized error-handling options. |
Redux Integration | Built directly into Redux, simplifying usage for Redux-based apps. | Not integrated with Redux by default, although Redux and React Query can be combined if needed. |
Choosing Between RTK Query and React Query:
-
Use RTK Query if:
- You’re already using Redux and want an integrated, streamlined solution for data fetching.
- You need centralized error handling and devtools integration within Redux.
-
Use React Query if:
- You want a more lightweight setup without Redux dependency.
- You prefer separate server-state management and don’t need global app state.
In essence, RTK Query excels for Redux-centric applications, while React Query provides flexibility and simplicity for projects without Redux or those with a more localized server state management focus.
1. Store Configuration and Setup
// src/store/store.js
import AsyncStorage from '@react-native-async-storage/async-storage';
import { combineReducers, configureStore, isRejectedWithValue } from '@reduxjs/toolkit';
import { setupListeners } from '@reduxjs/toolkit/query';
import { FLUSH, PAUSE, PERSIST, persistReducer, PURGE, REGISTER, REHYDRATE } from 'redux-persist';
import { authApi } from '../api/authApi';
import { postsApi } from '../api/postsApi';
import { usersApi } from '../api/usersApi';
import authSlice from '../features/auth/authSlice';
const persistConfig = {
key: 'root',
version: 1,
storage: AsyncStorage,
blacklist: ['auth', postsApi.middleware, usersApi.middleware, authApi.middleware], // these reduce will not persist data (NOTE: blacklist rtk api slices so that to use tags)
// whitelist: ['users'], //these reduce will persist data
};
const getEnhancers = (getDefaultEnhancers) => {
if (process.env.NODE_ENV === 'development') {
const reactotron = require('../reactotronConfig/ReactotronConfig').default;
return getDefaultEnhancers().concat(reactotron.createEnhancer());
}
return getDefaultEnhancers();
};
/**
* On api error this will be called
*/
export const rtkQueryErrorLogger = (api) => (next) => (action) => {
// RTK Query uses `createAsyncThunk` from redux-toolkit under the hood, so we're able to utilize these matchers!
if (isRejectedWithValue(action)) {
console.log('isRejectedWithValue', action.error, action.payload);
alert(JSON.stringify(action)); // This is just an example. You can replace it with your preferred method for displaying notifications.
}
return next(action);
};
const reducer = combineReducers({
auth: authSlice,
[postsApi.reducerPath]: postsApi.reducer,
[usersApi.reducerPath]: usersApi.reducer,
[authApi.reducerPath]: authApi.reducer,
});
const persistedReducer = persistReducer(persistConfig, reducer);
const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}).concat(postsApi.middleware, usersApi.middleware, authApi.middleware, rtkQueryErrorLogger),
enhancers: getEnhancers,
});
setupListeners(store.dispatch);
export default store;
Redux Store (
src/store/store.js
): The Redux store is the main structure holding the application's state. In your setup, it’s enhanced withredux-persist
to save certain parts of the Redux state locally, so they persist even when the app restarts.-
redux-persist
:- Purpose: Helps keep parts of the Redux state persistent across app sessions.
-
Configuration: A
persistConfig
object specifies thatauth
,postsApi
, andusersApi
should not be persisted (blacklisted), meaning their data resets on app restart. -
persistReducer
combines the reducer configuration with persistence functionality.
Enhancers: Custom enhancers are used to integrate Reactotron in development mode, a helpful tool for debugging Redux actions, state, and network requests. This only activates in development, making debugging easier without affecting production.
-
Middleware:
- RTK Query middlewares (
postsApi.middleware
,usersApi.middleware
,authApi.middleware
) add functionality for automatic cache management, making data fetching efficient. -
rtkQueryErrorLogger
: A custom middleware logs errors when API calls fail. It uses RTK Query’sisRejectedWithValue
function to catch and handle errors, allowing you to alert users about issues or take other actions.
- RTK Query middlewares (
setupListeners
: This function enables automatic re-fetching of data when certain events occur, like when the app regains focus or resumes from the background, providing users with fresh data without manual refresh.
2. API Definitions with RTK Query
RTK Query simplifies API calls by auto-generating Redux slices, hooks, and caching. Here’s a breakdown of the APIs you defined:
// src/api/authApi.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { setToken } from '../features/auth/authSlice';
export const authApi = createApi({
reducerPath: 'authApi',
baseQuery: fetchBaseQuery({
baseUrl: 'https://dummyjson.com/auth/',
}),
endpoints: (builder) => ({
login: builder.mutation({
query: (credentials) => ({
url: 'login',
method: 'POST',
body: credentials,
}),
async onQueryStarted(arg, { dispatch, queryFulfilled }) {
try {
const { data } = await queryFulfilled;
dispatch(setToken(data.accessToken)); // Store the token in Redux
} catch (error) {
console.error('Login error:', error);
}
},
}),
}),
});
export const { useLoginMutation } = authApi;
-
authApi
(src/api/authApi.js
):- Defines a
login
mutation that sends user credentials (e.g., username, password) to the server to authenticate. -
onQueryStarted: Once login is successful, it stores the returned token in Redux using the
setToken
action. This enables secure, authenticated requests to other endpoints.
- Defines a
// src/api/postsApi.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
// Define the postsApi slice with RTK Query
export const postsApi = createApi({
// Unique key for the API slice in Redux state
reducerPath: 'postsApi',
// Configure base query settings, including the base URL for all requests
baseQuery: fetchBaseQuery({
baseUrl: 'https://jsonplaceholder.typicode.com',
}),
// Define cache tag types for automatic cache invalidation
tagTypes: ['Posts'],
// Define API endpoints (queries and mutations)
endpoints: (builder) => ({
// Query to fetch a paginated list of posts
getPosts: builder.query({
// URL and parameters for paginated posts
query: ({ page = 1, limit = 10 }) => `/posts?_page=${page}&_limit=${limit}`,
// Tagging posts to automatically refresh this cache when needed
providesTags: (result) =>
result
? [...result.map(({ id }) => ({ type: 'Posts', id })), { type: 'Posts', id: 'LIST' }]
: [{ type: 'Posts', id: 'LIST' }],
}),
// Query to fetch a single post by its ID
getPostById: builder.query({
// Define query with post ID in the URL path
query: (id) => `/posts/${id}`,
// Tag individual post by ID for selective cache invalidation
providesTags: (result, error, id) => [{ type: 'Posts', id }],
}),
// Mutation to create a new post
createPost: builder.mutation({
// Configure the POST request details and payload
query: (newPost) => ({
url: '/posts',
method: 'POST',
body: newPost,
}),
// Invalidate all posts (paginated list) to refresh after creating a post
invalidatesTags: [{ type: 'Posts', id: 'LIST' }],
}),
// Mutation to update an existing post by its ID
updatePost: builder.mutation({
// Define the PUT request with post ID and updated data in the payload
query: ({ id, ...updatedData }) => ({
url: `/posts/${id}`,
method: 'PUT',
body: updatedData,
}),
// Invalidate cache for both the updated post and the paginated list
invalidatesTags: (result, error, { id }) => [
{ type: 'Posts', id },
{ type: 'Posts', id: 'LIST' },
],
}),
// Mutation to delete a post by its ID
deletePost: builder.mutation({
// Define the DELETE request with post ID in the URL path
query: (id) => ({
url: `/posts/${id}`,
method: 'DELETE',
}),
// Invalidate cache for the deleted post and the paginated list
invalidatesTags: (result, error, id) => [
{ type: 'Posts', id },
{ type: 'Posts', id: 'LIST' },
],
}),
}),
});
// Export generated hooks for each endpoint to use them in components
export const {
useGetPostsQuery, // Use this when you want data to be fetched automatically as the component mounts or when the query parameters change.
useLazyGetPostsQuery, // Use this when you need more control over when the query runs, such as in response to a user action (e.g., clicking a button), conditional fetching, or specific events.
useGetPostByIdQuery,
useCreatePostMutation,
useUpdatePostMutation,
useDeletePostMutation,
} = postsApi;
-
postsApi
(src/api/postsApi.js
):-
CRUD Operations: The posts API contains multiple endpoints to interact with posts (fetch, create, update, delete).
-
getPosts
: Fetches paginated posts, meaning it retrieves data in smaller chunks (pages), improving performance and load times. -
createPost
,updatePost
, anddeletePost
: Each of these performs a different action (create, update, or delete a post).
-
-
Tags for Caching: Each endpoint uses tags (e.g.,
{ type: 'Posts', id }
) to automatically manage cache invalidation and refreshing. For instance, creating or deleting a post invalidates the cache, promptinggetPosts
to fetch fresh data without manual intervention.
-
CRUD Operations: The posts API contains multiple endpoints to interact with posts (fetch, create, update, delete).
// src/api/usersApi.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const usersApi = createApi({
reducerPath: 'usersApi',
baseQuery: fetchBaseQuery({
baseUrl: 'https://dummyjson.com',
prepareHeaders: (headers, { getState }) => {
// Get the token from the Redux auth state
const { token } = getState().auth;
// If the token exists, set it in the Authorization header
if (token) {
headers.set('Authorization', `Bearer ${token}`);
}
// Optional: include credentials if needed by the API
headers.set('credentials', 'include');
return headers;
},
}),
endpoints: (builder) => ({
// Fetch user profile with token in Authorization header
getUserProfile: builder.query({
query: () => '/auth/me',
}),
}),
});
export const { useGetUserProfileQuery } = usersApi;
-
usersApi
(src/api/usersApi.js
):- This API fetches the authenticated user’s profile, setting up
Authorization
headers based on the token from Redux. -
Headers:
prepareHeaders
dynamically attaches the token to every request if available, allowing secure and authorized API requests.
- This API fetches the authenticated user’s profile, setting up
3. Auth Slice (src/features/auth/authSlice.js
)
// src/features/auth/authSlice.js
import { createSlice } from '@reduxjs/toolkit';
const authSlice = createSlice({
name: 'auth',
initialState: {
token: null,
},
reducers: {
setToken: (state, action) => {
console.log('first action', action);
state.token = action.payload;
},
logout: (state) => {
state.token = null;
},
},
});
export const { setToken, logout } = authSlice.actions;
export default authSlice.reducer;
-
authSlice
: A Redux slice manages a specific piece of state, in this case, user authentication. -
State Management: The
authSlice
keeps the user’s token, which is used to access protected API endpoints. -
Actions:
-
setToken
: Stores the authentication token in the Redux state. -
logout
: Clears the token from Redux, effectively logging the user out.
-
4. Reactotron for Debugging (src/reactotronConfig/ReactotronConfig.js
)
import Reactotron, { networking } from 'reactotron-react-native';
import { reactotronRedux } from 'reactotron-redux';
Reactotron.configure({ name: 'app_name' })
.useReactNative()
.use(reactotronRedux()) // <- here i am!
.use(networking())
.connect();
// patch console.log to send log to reactotron
const yeOldeConsoleLog = console.log;
console.log = (...args) => {
yeOldeConsoleLog(...args);
Reactotron.display({
name: 'CONSOLE.LOG',
value: args,
preview: args.length > 0 && typeof args[0] === 'string' ? args[0] : null,
});
};
export default Reactotron;
- Reactotron: Reactotron is a debugging tool that helps track Redux state changes, monitor API requests, and inspect logs.
-
Setup: Configured to capture
console.log
outputs and Redux actions. In development mode, this setup provides a powerful way to debug without adding extra code or altering production performance.
5. Main Application Components
// src/App.js
import React from 'react';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { Provider } from 'react-redux';
import { persistStore } from 'redux-persist';
import { PersistGate } from 'redux-persist/integration/react';
import MainApp from './MainApp';
import rtkStore from './store/store';
const persistor = persistStore(rtkStore);
const App = () => (
<Provider store={rtkStore}>
<PersistGate loading={null} persistor={persistor}>
<SafeAreaProvider>
<MainApp />
</SafeAreaProvider>
</PersistGate>
</Provider>
);
export default App;
-
App Component (
src/App.js
):- The
App
component wraps the entire application inProvider
(to make Redux available) andPersistGate
(to delay rendering until persisted state has been retrieved). -
PersistGate
ensures persisted data loads before the app displays, reducing load time inconsistencies.
- The
// src/MainApp.js
import React, { useEffect, useState } from 'react';
import {
ActivityIndicator,
Button,
FlatList,
Modal,
RefreshControl,
StyleSheet,
Text,
TextInput,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useDispatch, useSelector } from 'react-redux';
import { useLoginMutation } from './api/authApi';
import {
useCreatePostMutation,
useDeletePostMutation,
useGetPostsQuery,
useLazyGetPostsQuery,
useUpdatePostMutation,
} from './api/postsApi';
import { useGetUserProfileQuery } from './api/usersApi';
import { logout } from './features/auth/authSlice';
const MainApp = () => {
const [newPostTitle, setNewPostTitle] = useState('');
const [page, setPage] = useState(1);
const [postsData, setPostsData] = useState([]);
const [refreshing, setRefreshing] = useState(false);
const [isModalVisible, setModalVisible] = useState(false);
const dispatch = useDispatch();
const token = useSelector((state) => state.auth.token);
// Login mutation
const [login, { isLoading: isLoggingIn }] = useLoginMutation();
// Fetch user profile when token is available
const { data: userProfile, refetch: refetchUserProfile } = useGetUserProfileQuery(undefined, {
skip: !token,
});
// Fetch paginated posts
const {
data: posts,
isLoading,
isFetching,
isError,
refetch,
} = useGetPostsQuery({ page, limit: 10 }); // The useQuery hook is used when you want to fetch data on screen load. For example fetch userprofile on profile screen.
// Use the lazy query for refresh to directly fetch page 1
const [triggerFetchFirstPage, { data: lazyData }] = useLazyGetPostsQuery(); // useLazyquery is used when you want to control over the api calling, like on button click.
const [createPost] = useCreatePostMutation();
const [updatePost] = useUpdatePostMutation();
const [deletePost] = useDeletePostMutation();
useEffect(() => {
if (posts) {
setPostsData((prevData) => (page === 1 ? posts : [...prevData, ...posts]));
}
}, [posts, page]);
// Login handler
const handleLogin = async () => {
try {
const credentials = { username: 'emilys', password: 'emilyspass' };
await login(credentials);
console.log('userProfile', userProfile);
refetchUserProfile();
} catch (error) {
console.error('Login failed:', error);
}
};
const handleRefresh = async () => {
setRefreshing(true);
setPage(1); // Reset the page to 1 for the next scrolls
setPostsData([]); // Clear the data to avoid duplications
// Trigger the first page fetch explicitly
const { data } = await triggerFetchFirstPage({ page: 1, limit: 10 });
if (data) {
setPostsData(data); // Set the posts data to the first page's results
}
setRefreshing(false);
};
// Create a new post, add it to the top, and refetch the list
const handleCreatePost = async () => {
if (newPostTitle) {
const { data: newPost } = await createPost({ title: newPostTitle, body: 'New post content' });
setNewPostTitle('');
setPostsData((prevData) => [newPost, ...prevData]);
refetch();
}
};
// Update an existing post and add "HASAN" to its title
const handleUpdatePost = async (post) => {
const { data: updatedPost } = await updatePost({
id: post.id,
title: `${post.title} HASAN`,
});
setPostsData((prevData) =>
prevData.map((item) => (item?.id === updatedPost?.id ? updatedPost : item))
);
};
// Delete a post and remove it from the UI immediately
const handleDeletePost = async (id) => {
await deletePost(id);
setPostsData((prevData) => prevData.filter((post) => post.id !== id));
};
// Load more posts for infinite scrolling
const loadMorePosts = () => {
if (!isFetching) {
setPage((prevPage) => prevPage + 1);
}
};
// Toggle modal visibility
const toggleModal = () => {
setModalVisible(!isModalVisible);
};
if (isLoading && page === 1) return <Text>Loading...</Text>;
if (isError) return <Text>Error fetching posts.</Text>;
return (
<SafeAreaView style={styles.container}>
<View style={{ flexDirection: 'row', alignSelf: 'center' }}>
<Button title="Login" onPress={handleLogin} disabled={isLoggingIn} />
{userProfile && <Button title="Show Profile" onPress={toggleModal} />}
</View>
<TextInput
placeholder="New post title"
value={newPostTitle}
onChangeText={setNewPostTitle}
style={styles.input}
/>
<Button title="Add Post" onPress={handleCreatePost} />
<FlatList
data={postsData}
keyExtractor={(item, index) => index.toString()}
renderItem={({ item }) => (
<View style={styles.post}>
<Text>{item.title}</Text>
<Button title="Update" onPress={() => handleUpdatePost(item)} />
<Button title="Delete" onPress={() => handleDeletePost(item.id)} />
</View>
)}
onEndReached={loadMorePosts}
onEndReachedThreshold={0.5}
ListFooterComponent={isFetching ? <ActivityIndicator color="red" /> : null}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />}
/>
{/* Profile Modal */}
<Modal
visible={isModalVisible}
animationType="slide"
onRequestClose={toggleModal}
transparent
>
<View style={styles.modalContainer}>
<View style={styles.modalContent}>
{userProfile ? (
<>
<Text style={styles.modalTitle}>User Profile</Text>
<Text>Username: {userProfile.username}</Text>
<Text>Email: {userProfile.email}</Text>
<Button title="Close" onPress={toggleModal} />
<Button
title="Logout"
onPress={() => {
toggleModal();
dispatch(logout());
}}
/>
</>
) : (
<Text>Loading Profile...</Text>
)}
</View>
</View>
</Modal>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: { flex: 1, padding: 16, backgroundColor: 'cyan' },
input: { height: 40, borderColor: 'gray', borderWidth: 1, marginBottom: 8, paddingHorizontal: 8 },
post: {
padding: 10,
borderWidth: 1,
borderColor: '#ddd',
backgroundColor: 'pink',
marginBottom: 8,
},
modalContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
},
modalContent: {
width: 300,
padding: 20,
backgroundColor: '#fff',
borderRadius: 10,
},
modalTitle: { fontSize: 18, fontWeight: 'bold', marginBottom: 10 },
});
export default MainApp;
-
MainApp Component (
src/MainApp.js
):-
State and Hooks: Manages local states (e.g., for posts pagination) and hooks like
useLoginMutation
to trigger actions on specific events. -
Login:
- Uses
useLoginMutation
to log the user in and then triggersrefetchUserProfile
to load the user profile data. -
Conditional Querying: Only fetches the user profile when a valid token exists (
skip: !token
), reducing unnecessary API calls.
- Uses
-
Fetching Posts:
- Uses
useGetPostsQuery
to fetch paginated posts, supporting infinite scrolling by fetching more data as the user scrolls. - Refresh Control: Allows users to refresh the posts list, useful for pull-to-refresh functionality on mobile.
- Uses
-
Create, Update, Delete Posts:
-
Create: Calls
createPost
, immediately updating the posts list with the new post at the top. -
Update: Appends
"HASAN"
to a post’s title upon updating. -
Delete: Removes a post and updates the UI without needing a page reload, thanks to cache invalidation from
deletePost
.
-
Create: Calls
-
UI Elements:
- A modal displays the user profile. The profile button only appears if
userProfile
data is loaded, enhancing the user experience.
- A modal displays the user profile. The profile button only appears if
- FlatList: Displays posts in a scrollable, paginated format, enhancing usability.
-
State and Hooks: Manages local states (e.g., for posts pagination) and hooks like
Summary:
Your React Native app uses Redux Toolkit (RTK) Query for efficient data management and API interactions. The setup includes:
Store Configuration: Redux store with
redux-persist
to save specific data across app sessions, a custom middleware for error logging, andReactotron
for debugging in development mode.-
APIs with RTK Query:
-
authApi
handles authentication with alogin
mutation, storing the token in Redux. -
postsApi
provides CRUD operations for posts, using cache tags to automatically refresh data when posts are added, updated, or deleted. -
usersApi
fetches the user profile with dynamic token-based authorization headers.
-
Auth Slice: Manages the auth token and provides actions for setting or clearing the token on login/logout.
-
App and MainApp Components:
- The main app wraps components in
Provider
andPersistGate
, ensuring state is loaded before rendering. -
MainApp
manages posts fetching, creating, updating, and deleting. It conditionally loads data (e.g., fetching user profile only when a token exists), supports pagination and infinite scrolling - Uses FlatList for a paginated post list, modals for the profile, and basic styles for a clean, organized layout.
- The main app wraps components in
Top comments (0)