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 built into Redux Toolkit. You'll explore how to create and use queries, handle mutations, and effectively manage server state in your applications.

Learning Objectives

Content

Understanding RTK Query

What is RTK Query?

RTK Query is a powerful data fetching and caching solution that is part of the Redux Toolkit. It provides a streamlined API to simplify asynchronous data fetching, caching, and state management in React applications.

Why RTK Query?

RTK Query offers several advantages that make it an excellent choice for managing server state:

  • Simplified Data Fetching: No more manually setting up actions, reducers, and middleware for each API request. RTK Query abstracts these complexities, letting you declare what data you need, not how to fetch it.
  • Built-in Caching and Automatic Refetching: RTK Query automatically caches your data and provides intelligent re-fetching, optimizing your application's performance and reducing unnecessary network requests.
  • Seamless Integration with Redux: As part of Redux Toolkit, RTK Query integrates perfectly with the Redux ecosystem, allowing for centralized state management.
  • Streamlined State Management: RTK Query manages loading states and error handling, freeing developers to focus on building features.

RTK Query acts as a middleware between your React application and your backend APIs, replacing manual fetch calls and state management with hooks that handle the entire lifecycle of API calls.

Setting Up RTK Query

Setting Up the API Service

The first step in using RTK Query is to create an API slice using the createApi function. This slice will define how your application interacts with your API.

// 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 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/' }),
  
  // Endpoints will be defined here in future steps
  endpoints: (builder) => ({})
});

Integrating the API Service with the Store

Once you've created your API slice, you need to integrate it with your Redux store:

// store.js
import { configureStore } from '@reduxjs/toolkit'
import { usersApi } from './usersApi'

// Configuring the Redux store, adding our API slice and middleware
export const store = configureStore({
  // The root reducer object where we mount our slice reducers
  reducer: {
    // Mounting the API reducer under a dynamic key
    [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),
});

This setup injects RTK Query's capabilities into your Redux store, allowing it to manage server state alongside your client state.

Creating Queries

Defining Endpoints

After setting up the API service, you need to define endpoints that correspond to API operations:

// usersApi.js (continued from above)
export const usersApi = createApi({
  reducerPath: 'usersApi',
  baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:9009/api/' }),
  endpoints: (builder) => ({
    // Query endpoint to fetch all users
    getUsers: builder.query({
      query: () => 'users',
    }),
    
    // Query endpoint to fetch a single user by ID
    getUserById: builder.query({
      query: (userId) => `users/${userId}`,
    }),
  }),
});

// Export the auto-generated hooks
export const { useGetUsersQuery, useGetUserByIdQuery } = usersApi;

Using Query Hooks in Components

RTK Query auto-generates hooks for each endpoint that you can use directly in your components:

// UsersList.js
import React from 'react';
import { useGetUsersQuery } from './usersApi';

function UsersList() {
  // The hook provides data and loading/error states
  const { data: users, isLoading, isError, error } = useGetUsersQuery();

  if (isLoading) {
    return <div>Loading users...</div>;
  }

  if (isError) {
    return <div>Error loading users: {error.message}</div>;
  }

  return (
    <div>
      <h2>Users</h2>
      <ul>
        {users.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
}

Using Tags

Cache Invalidation with Tags

RTK Query uses tags to manage cache invalidation. Tags allow you to mark which queries should be refetched when mutations occur:

// usersApi.js with tags
export const usersApi = createApi({
  reducerPath: 'usersApi',
  baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:9009/api/' }),
  tagTypes: ['User'],  // Define tag types
  endpoints: (builder) => ({
    getUsers: builder.query({
      query: () => 'users',
      // Provide tags to this query
      providesTags: ['User'],
    }),
    
    getUserById: builder.query({
      query: (userId) => `users/${userId}`,
      // Provide a specific tag for each user
      providesTags: (result, error, userId) => [{ type: 'User', id: userId }],
    }),
    
    // Mutation to add a new user
    addUser: builder.mutation({
      query: (newUser) => ({
        url: 'users',
        method: 'POST',
        body: newUser,
      }),
      // Invalidate the User tag to trigger refetching
      invalidatesTags: ['User'],
    }),
  }),
});

export const { 
  useGetUsersQuery, 
  useGetUserByIdQuery,
  useAddUserMutation 
} = usersApi;

With this setup, whenever the addUser mutation is executed, any query with the User tag will be automatically refetched, ensuring your UI stays in sync with the server data.

Monitoring Requests

Handling Request States

RTK Query provides comprehensive request state management, allowing you to easily handle loading, success, and error states in your UI:

// UserForm.js
import React, { useState } from 'react';
import { useAddUserMutation } from './usersApi';

function UserForm() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  
  // The mutation hook returns an array with the trigger function and result object
  const [addUser, { isLoading, isSuccess, isError, error }] = useAddUserMutation();
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      // Trigger the mutation
      await addUser({ name, email });
      // Clear form on success
      setName('');
      setEmail('');
    } catch (err) {
      // Error is handled by RTK Query
      console.error('Failed to add user:', err);
    }
  };
  
  return (
    <div>
      <h2>Add New User</h2>
      {isSuccess && <p className="success">User added successfully!</p>}
      {isError && <p className="error">Error: {error.message}</p>}
      
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="name">Name:</label>
          <input
            id="name"
            value={name}
            onChange={(e) => setName(e.target.value)}
            disabled={isLoading}
          />
        </div>
        <div>
          <label htmlFor="email">Email:</label>
          <input
            id="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            disabled={isLoading}
          />
        </div>
        <button type="submit" disabled={isLoading}>
          {isLoading ? 'Adding...' : 'Add User'}
        </button>
      </form>
    </div>
  );
}

RTK Query also provides additional properties like isFetching (for subsequent requests), refetch (to manually trigger refetching), and data (the cached response), making it a complete solution for API state management.

Practice Activities

Guided Project

Project Solution

Additional Resources