In this guide, we’ll cover:
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:
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:
Use React Query if:
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.
// 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 with redux-persist to save certain parts of the Redux state locally, so they persist even when the app restarts.
redux-persist:
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:
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.
RTK Query simplifies API calls by auto-generating Redux slices, hooks, and caching. Here’s a breakdown of the APIs you defined:
// 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;
// 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;
// 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;
// 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;
// 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;
// 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;
// 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> <ul> <li> <strong>MainApp Component (src/MainApp.js)</strong>: <ul> <li> <strong>State and Hooks</strong>: Manages local states (e.g., for posts pagination) and hooks like useLoginMutation to trigger actions on specific events.</li> <li> <strong>Login</strong>: <ul> <li>Uses useLoginMutation to log the user in and then triggers refetchUserProfile to load the user profile data.</li> <li> <em>Conditional Querying</em>: Only fetches the user profile when a valid token exists (skip: !token), reducing unnecessary API calls.</li> </ul> </li> <li> <strong>Fetching Posts</strong>: <ul> <li>Uses useGetPostsQuery to fetch paginated posts, supporting infinite scrolling by fetching more data as the user scrolls.</li> <li> <em>Refresh Control</em>: Allows users to refresh the posts list, useful for pull-to-refresh functionality on mobile.</li> </ul> </li> <li> <strong>Create, Update, Delete Posts</strong>: <ul> <li> <em>Create</em>: Calls createPost, immediately updating the posts list with the new post at the top.</li> <li> <em>Update</em>: Appends "HASAN" to a post’s title upon updating.</li> <li> <em>Delete</em>: Removes a post and updates the UI without needing a page reload, thanks to cache invalidation from deletePost.</li> </ul> </li> <li> <strong>UI Elements</strong>: <ul> <li>A modal displays the user profile. The profile button only appears if userProfile data is loaded, enhancing the user experience.</li> </ul> </li> <li> <strong>FlatList</strong>: Displays posts in a scrollable, paginated format, enhancing usability.</li> </ul> </li> </ul> <hr> <h2> Summary: </h2> <p>Your React Native app uses <strong>Redux Toolkit (RTK) Query</strong> for efficient data management and API interactions. The setup includes:</p> <ol> <li><p><strong>Store Configuration</strong>: Redux store with redux-persist to save specific data across app sessions, a custom middleware for error logging, and Reactotron for debugging in development mode.</p></li> <li> <p><strong>APIs with RTK Query</strong>:</p><ul> <li> <strong>authApi</strong> handles authentication with a login mutation, storing the token in Redux.</li> <li> <strong>postsApi</strong> provides CRUD operations for posts, using cache tags to automatically refresh data when posts are added, updated, or deleted.</li> <li> <strong>usersApi</strong> fetches the user profile with dynamic token-based authorization headers.</li> </ul> </li> <li><p><strong>Auth Slice</strong>: Manages the auth token and provides actions for setting or clearing the token on login/logout.</p></li> <li> <p><strong>App and MainApp Components</strong>:</p> <ul> <li>The main app wraps components in Provider and PersistGate, ensuring state is loaded before rendering.</li> <li> 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 </li> <li>Uses FlatList for a paginated post list, modals for the profile, and basic styles for a clean, organized layout.</li> </ul> </li> </ol> <blockquote> <p>FULL CODE->
The above is the detailed content of Efficient Data Handling in React Native with RTK Query. For more information, please follow other related articles on the PHP Chinese website!