Web Unit 3 Sprint 10 - Redux & State Management
Module 4: RTK Query
In this module, you'll learn about RTK Query, a powerful data fetching and caching tool included in Redux Toolkit. You'll explore how to efficiently manage API requests, cache responses, and handle loading and error states.
Learning Objectives
- Explain what RTK Query is and the problems it solves
- Create an API slice using createApi
- Define endpoints and transformations
- Implement data fetching and caching strategies
- Set up automatic re-fetching and cache invalidation
Introduction to RTK Query
RTK Query is an essential tool in a developer's arsenal for efficient data management. Let's dive right in!
Why RTK Query?
RTK Query is a powerful data fetching and caching solution that is part of the Redux Toolkit. It provides a succinct API to simplify asynchronous data fetching, caching, and state management in React applications. Here's why it's becoming a go-to for many developers:
Simplified Data Fetching
Gone are the days of manually setting up actions, reducers, and middleware for each API request. RTK Query abstracts these complexities, allowing developers to declare what data they need, not how to fetch it.
Built-in Caching and Automatic Refetching
RTK Query automatically caches your data and provides intelligent re-fetching. It knows when to fetch new data and when to provide cached data, optimizing your application's performance and reducing unnecessary network requests.
Seamless Integration with Redux
As a part of Redux Toolkit, RTK Query integrates seamlessly with the Redux ecosystem, allowing for centralized state management and easy access to data throughout your application.
Streamlined State Management
RTK Query reduces the need for boilerplate code and streamlines state management. It manages loading states and error handling, freeing developers to focus on building features.
How does RTK Query Fit In?
Understanding where RTK Query fits in your application architecture is crucial. It acts as a middleware between your React application and your backend APIs. Instead of writing fetch calls and managing states manually, RTK Query provides hooks that handle the lifecycle of API calls, including caching, updating, and error handling.
RTK Query in Action
Through this module, we'll see RTK Query in action, demonstrating how to set up queries to fetch data, how to use mutations to send data, and how tags can be used to manage automatic re-fetching of data. This practical approach ensures that by the end of this module, you'll have a solid understanding and hands-on experience with RTK Query.
RTK Query is the gateway to adopting a more efficient and modern approach to handling data in React applications. It's designed to make server-state management more accessible and less error-prone, thereby increasing developer productivity and application reliability.
In the upcoming objectives, we will dive deeper into each aspect of RTK Query, providing you with the knowledge and skills needed to leverage its full potential.
Setting up RTK Query
This objective is dedicated to mastering the setup of RTK Query within a React application. RTK Query is a powerful data fetching and caching tool provided by the Redux Toolkit, which simplifies server-state management in modern applications. Our objective is to construct an API slice to handle user data requests and seamlessly integrate this service into our Redux store, paving the way for efficient data management.
How to Build It
Setting Up the API Service
Our journey begins with the establishment of an API slice. The
createApi
function from RTK Query is a factory that
generates an object containing a set of tools designed for data
fetching. We'll configure our slice with a
reducerPath
that uniquely identifies the slice within
our store and initiate our baseQuery
with the root URL
of our backend API.
// frontend/state/usersApi.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
// The usersApi slice - handling API interactions for user data
export const usersApi = createApi({
// Unique key that defines where the data will be stored in the Redux state
reducerPath: 'usersApi',
// Setting up the base query with the base URL pointing to the backend service
baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:9009/api' }),
// Placeholder for our endpoint definitions - to be implemented
endpoints: builder => ({
// Endpoints will be defined here in future steps
})
})
Integrating the API Service with the Store
Now, let's weave our usersApi
into the fabric of our
Redux store configuration. We incorporate our API slice's reducer
into the store's root reducer. Additionally, we infuse the store's
middleware chain with the API slice's middleware to ensure that it
can intercept and handle any relevant actions.
// frontend/state/store.js
import { configureStore } from '@reduxjs/toolkit'
import { usersApi } from './usersApi'
// Configuring the Redux store, adding in our API slice and middleware
export const store = configureStore({
// The root reducer object where we mount our slice reducers
reducer: {
// Other reducers might exist
// Mounting the API reducer under a dynamic key generated by the API slice
[usersApi.reducerPath]: usersApi.reducer,
},
// Customizing the store's middleware chain
middleware: getDefaultMiddleware =>
// Adding the API middleware to the default middleware chain
getDefaultMiddleware().concat(usersApi.middleware),
})
RTK Query in Action
With our usersApi
slice and store configuration in
place, the stage is set for us to define specific endpoints. These
endpoints are akin to doorways that our components will use to send
and receive data, carrying out actions such as fetching user lists,
creating new users, or updating existing user information.
Observing the Behavior
With RTK Query operational, we can turn our attention to the Redux DevTools to check the states that are set up for us.
Creating Queries
In this objective we delve into how RTK Query enables us to retrieve data using simple yet powerful query definitions. We'll flesh out our usersApi slice to include a query endpoint for fetching user data and then utilize this in a component to display a list of users.
How to Build It
Defining a Query Endpoint
We expand our usersApi
slice by adding a query
endpoint. This endpoint represents a specific data retrieval
operation—fetching users in this case. We define a query function
that tells RTK Query what to fetch and how to fetch it.
// frontend/state/usersApi.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
// This is the centralized API slice for user-related data fetching
export const usersApi = createApi({
// Identifies the API slice's place in the global state structure
reducerPath: 'usersApi',
// Sets up the base query with the root URL to the backend server
baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:9009/api' }),
// The endpoints represent operations our components can invoke
endpoints: builder => ({
// 'getUsers' is a query operation to fetch user data
getUsers: builder.query({
// Pointing to the 'users' endpoint on our server
query: () => 'users',
}),
})
})
// Auto-generated hook for the 'getUsers' query
export const {
useGetUsersQuery,
} = usersApi
Consuming the Query in a Component
With the query endpoint in place, we can now use the auto-generated
useGetUsersQuery
hook in our components. This hook
takes care of the data fetching lifecycle, including loading and
error states.
// frontend/components/App.js
import React from 'react'
import { useGetUsersQuery } from '../state/usersApi'
// This component displays a list of users utilizing data fetched with RTK Query
export default function Todo() {
// Destructuring 'data' from the query hook, aliased as 'users' for clarity
const { data: users } = useGetUsersQuery()
return (
<div>
<h2>Users</h2>
<h3>Managed with RTK Query</h3>
<ul>{
// Rendering a list item for each user if the data is available
users?.map(user => (
<div key={user.id}>
{user.username}
</div>
))
}
</ul>
</div>
)
}
Visualizing the Data
Once the Todo
component mounts,
useGetUsersQuery
is invoked, initiating the data
fetching process. The list of users is then rendered on the screen
as soon as the data is available, demonstrating the seamless
integration of data fetching with RTK Query.
Mutations and Cache Invalidation
"Creating Mutations" guides us through the process of altering server-side data using RTK Query. Unlike queries, which request data without causing changes to the server state, mutations are used for creating, updating, or deleting data—essentially any operation that modifies server-side data. This module teaches how to define and use mutations within our application.
How to Build It
Understanding Queries vs. Mutations
Before diving into the code, let's clarify the difference between queries and mutations within the context of RTK Query and data fetching:
- Queries are operations that retrieve data from an API without causing side effects. They are meant for "GET" operations and are typically used to sync the server state with the client.
- Mutations, on the other hand, are operations that modify server-side data. They encompass "POST", "PUT", "PATCH", or "DELETE" operations and often result in changes to the data the server returns.
The distinction is crucial for understanding how to structure interactions with an API in a Redux application.
Setting Up a Mutation Endpoint
Building on our existing usersApi
, we add a mutation endpoint. This createUser
mutation allows us to send a new user's username to the server, requesting the creation of a new user entity.
// src/api/usersApi.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const usersApi = createApi({
reducerPath: 'usersApi',
baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:9009/api' }),
endpoints: builder => ({
getUsers: builder.query({
query: () => 'users',
}),
createUser: builder.mutation({
// Specifies the query for the mutation, including the URL, HTTP method, and body
query: username => ({
url: 'users',
method: 'POST',
body: { username },
}),
}),
}),
})
// Hooks automatically generated by RTK Query for our endpoints
export const {
useGetUsersQuery,
useCreateUserMutation,
} = usersApi
Implementing Mutations in Components
With the mutation endpoint ready, we employ the useCreateUserMutation
hook in our
App
component. This hook provides us with a create
function that we can call to
trigger the mutation.
// src/components/App.js
import React from 'react'
import { useGetUsersQuery, useCreateUserMutation } from '../state/usersApi'
export default function App() {
const { data: users } = useGetUsersQuery()
const [createUser] = useCreateUserMutation()
return (
<div>
<h2>Users</h2>
<h3>Managed with RTK Query</h3>
<ul>{
users?.map(user => (
<div key={user.id}>{user.username}</div>
))
}</ul>
{/* Button to trigger the mutation */}
<button onClick={() => createUser('Test User')}>
Create test user
</button>
</div>
)
}
The createUser
function can be called with the new user's username as an argument. When invoked,
it performs a "POST" request to the server's 'users' endpoint, which is expected to handle the user creation
logic.
When we call createUser
, RTK Query automatically manages the lifecycle of the mutation. It
optimizes the request process, handles potential loading states, and updates the cache upon completion. This
way, the component stays declarative, and the data flow remains clear and predictable.
Using Tags
In this objective, we're exploring the use of 'tags' in RTK Query. Tags are a feature of RTK Query that facilitate automatic re-fetching of data when performing actions that may change the data on the server side, such as mutations. This is part of RTK Query's advanced cache management capabilities, ensuring that the UI is kept up-to-date with the latest server state.
How to Build It
What are Tags>
Tags in RTK Query are identifiers associated with query results. They play a crucial role in cache invalidation strategies, which determine when to fetch fresh data. Here's a quick breakdown:
providesTags
: This is used in a query to define which tags the query's result will provide. When data is fetched using this query, RTK Query will keep track of the provided tag as being "available".invalidatesTags
: Used in a mutation to specify which tags should be invalidated when the mutation is successfully executed. If a mutation invalidates a tag, RTK Query will mark it as "outdated", and any queries providing that tag will be re-fetched to provide fresh data.
Adding Tags to Our Endpoints
We'll modify our usersApi
service to utilize tags for cache control. By specifying
providesTags
in the getUsers
query and invalidatesTags
in the
createUser
mutation, we inform RTK Query about the relationships between our operations and the
cached data.
// frontend/state/usersApi.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const usersApi = createApi({
reducerPath: 'usersApi',
baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:9009/api' }),
tagTypes: ['Users'], // Declaring the types of tags we'll use for invalidation
endpoints: builder => ({
getUsers: builder.query({
query: () => 'users',
providesTags: ['Users'], // The 'getUsers' query provides the 'Users' tag
}),
createUser: builder.mutation({
query: username => ({
url: 'users',
method: 'POST',
body: { username },
}),
invalidatesTags: ['Users'], // Invalidate 'Users' tag upon mutation
}),
}),
})
export const {
useGetUsersQuery,
useCreateUserMutation,
} = usersApi
Using Tagged Endpoints in Components
Within our components, we use the queries and mutations just as before, but now RTK Query will automatically
re-fetch the getUsers
query whenever we call the createUser
mutation, thanks to the
tags.
// frontend/components/App.js
import React from 'react'
import { useGetUsersQuery, useCreateUserMutation } from '../state/usersApi'
export default function App() {
const { data: users } = useGetUsersQuery()
const [createUser] = useCreateUserMutation()
return (
<div>
<h2>Users</h2>
<h3>Managed with RTK Query</h3>
<ul>{
users?.map(user => (
<div key={user.id}>{user.username}</div>
))
}</ul>
<button onClick={() => createUser('New User')}>
Add New User
</button>
</div>
)
}
The Power of Tags
By leveraging tags, we greatly simplify the logic around maintaining fresh data within our application. With RTK Query handling the complexities of invalidating and re-fetching queries, developers can focus more on building out the UI and user experience, trusting that the data presented is current and consistent with the server state.
Understanding and using tags is a powerful aspect of RTK Query that optimizes data synchronization across our application. It's an example of how RTK Query abstracts away complex caching logic and provides developers with intuitive tools to maintain data consistency.
Monitoring Requests
This objective focuses on how to track the status of asynchronous operations in RTK Query. Using RTK Query's hooks, we can access the state of each request, including loading, success, and error states, which can be used to inform the UI and enhance user experience.
How to Build It
Extracting Request Status from RTK Query
When we use RTK Query's hooks for fetching data (useGetUsersQuery
) or performing mutations
(useCreateUsersMutation
), we get several pieces of state that describe the status of the
associated request. This status can be used to display loaders, success messages, or error messages.
In the provided screenshot of App.js, we can observe the usage of destructuring to extract various states from RTK Query's hooks:
// frontend/components/App.js
import React from 'react'
import { useGetUsersQuery, useCreateUserMutation } from '../state/usersApi'
export default function App() {
// Destructuring the state and functions from the useGetUsersQuery hook
const {
data: users,
error: usersError,
isLoading: usersLoading,
isFetching: usersFetching,
} = useGetUsersQuery()
// Destructuring the mutation function and related state from useCreateUserMutation
const [createUser, {
error: creationError,
isLoading: userCreating,
}] = useCreateUserMutation()
// Rest of the component
}
Understanding the Destructured State
data
: This is the response from our query or mutation. In the case ofuseGetUsersQuery
, it would be the list of users returned from our server.isLoading
: A boolean that indicates if the query/mutation has been initiated but a response has not yet been received.isFetching
: Similar toisLoading
, but specifically for queries, it remains true for as long as a request is in-flight, including background refetching.error
: Contains error information if the query or mutation fails.
Usage in UI
We can use these states to provide feedback in the UI, such as displaying loading indicators or error
messages. For example, we could render a spinner while usersLoading
or userCreating
is true, and display creationError.message
if an error occurs during user creation.
// Inside the App component's return statement
{
usersLoading ? <p>Loading users...</p> : (
<ul>
{users?.map(user => <div key={user.id}>{user.username}</div>)}
</ul>
)
}
{
userCreating && <p>Creating user...</p>
}
{
creationError && <p>Error creating user: {creationError.message}</p>
}
By monitoring the request status provided by RTK Query hooks, we gain fine-grained control over the user interface, allowing us to react to changes in request state and provide a responsive and informative experience for the end-user.
Module Project
For this project you will revisit the Inspirational Quotes app, which allows users to view, create and delete quotes. Until today, the application has been using data originating in the front end files. Now, you will connect your Redux app to a real API and sync its state with that of the server, via requests of type GET, POST, PUT and DELETE. This takes us much closer to how real web applications work in the real world!
The Module Project contains advanced problems that will challenge and stretch your understanding of the module's content. The solution video is available below in case you need help or want to see how we solved each challenge (note: there is always more than one way to solve a problem). If you can successfully complete all the Module Projects in a sprint, you are ready for the Sprint Challenge and Assessment.
The link below will provide you with a copy of the Module Project on GitHub:
- Starter Repo: RTK Query
- Fork and clone the code repository to your machine, and
- open the
README.md
file in VSCode, where you will find instructions on completing this Project. - Submit your GitHub url for the updated repository to the Sprint Challenge Submissions tab of your BloomTech portal for grading and review.
Watch if you get stuck or need help getting started.