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

Guided Project

Resources

Starter Repo: RTK Query

Solution Repo: RTK Query Solution

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 of useGetUsersQuery, 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 to isLoading, 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.