Learning React & Redux in 2023 — Part 20

Jeff P
7 min readOct 11, 2023

--

Media Project part 2

This follows on from the first section of the media project. In the first section we focussed on Async Thunks, but now we’ll turn our attention to Redux Toolkit Query

To use RTK Query, we import { createAPI } from the redux toolkit. When we create this, we’ll also be adding some configuration called endpoints

when we fetch data we use Query, when we manipulate data we use Mutation

We’ll want to create three API instances as follows:

We’ll start by creating a new folder in our store called “apis” and inside this folder, create a new file called albumsApi.js

import { createAPI } from "@reduxjs/toolkit/query/react";

const albumsApi = createAPI({
reducerPath: "albums",
});

The reducerPath is used to specify the name of the slice under which the generated Redux state and reducers for the API will be stored.

reducerPath: "albums": This specifies the name of the slice where the API-related state will be stored. In this case, the generated reducers and state for the API will be placed under the slice named "albums".

fetchBaseQuery

Up until now we’ve being using axios to interface with our DB, but RTK Query using the JavaScript fetch library to interact, so we need to import redux fetchBaseQuery

import { createAPI, fetchBaseQuery } from "@reduxjs/toolkit/query/react";

We then specify the base URL of our db

const albumsApi = createAPI({
reducerPath: "albums",
baseQuery: fetchBaseQuery({
baseUrl: "http://localhost:3005",
}),
});

Endpoints

Next we add in a function called “endpoints” with an argument called “builder”.

The structure of this function is as follows:

Note this is a Query as we are just fetching data, not changing it, which would be a Mutation

Also note that we don’t have to pass in anything in the body of the request with a fetch.

we then export useFetchAlbumsQuery (created automatically for us because our simplified name was defined as “fetchAlbums” and we assigned this is a data fetch (Query) so “use” is prepended to the front and Query is added to the end to make use FetchAlbums Query which together is useFetchAlbumsQuery

export const { useFetchAlbumsQuery } = albumsApi;
export { albumsApi };

Also note that we set our reducerPath key to “albums” which will be significant when we update the Redux store index.js file next:

IT’s worth noting that whenever we create an API for RTK Query, a slice is created for us AUTOMATICALLY. This slice is going to make a combined reducer. We need to bring this into our store for the single big reducer object.

Our store is already aware of the users: usersReducer from the usersSlice we created, but we now also need to make the store reducer aware of the albumsApi reducer.

We could add something like:

albums: albumsApi.reducer;

This would work because in our albumsApi we specified our reducerPath as “albums”

const albumsApi = createAPI({
reducerPath: "albums",
});

However, it’s usually safer to reference the reducerPath, rather than type it in, so instead we could write:

[albumsApi.reducerPath]: albumsApi.reducer

which basically says, replace whatever the value is for reducerPath (in this case “albums” and place that instead of [albumsApi.reducerPath]

So our store reducer would now look like this:

reducer: {
users: usersReducer,
[albumsApi.reducerPath]: albumsApi.reducer,
},
import { configureStore } from "@reduxjs/toolkit";
import { setupListeners } from "@reduxjs/toolkit/query";
import { usersReducer } from "./slices/usersSlice";
import { albumsApi } from "./apis/albumsApi";

export const store = configureStore({
reducer: {
users: usersReducer,
[albumsApi.reducerPath]: albumsApi.reducer,
},
middleware: (getDefaultMiddleware) => {
return getDefaultMiddleware().concat(albumsApi.middleware);
},
});

setupListeners(store.dispatch);

export * from "./thunks/fetchUsers";
export * from "./thunks/addUser";
export * from "./thunks/removeUser";
export { useFetchAlbumsQuery } from "./apis/albumsApi";

Redux middleware acts as a medium between dispatching an action and handing over the action to the reducer. It listens to all dispatches and executes code with the details of the actions and the current states. It is possible to combine multiple middlewares to add new features, and each middleware does not require an understanding of what happened before and after.

Action -> Middleware -> Reducer

setupListeners(store.dispatch);: This sets up listeners for the RTK Query API endpoints to handle automatic caching and invalidation. The store.dispatch is passed to the setupListeners function so that it can listen to dispatched actions.

We’ll also update our AlbumsList component, so that we import useFetchAlbumsQuery, and for now we’ll just console log the results of calling this.

import { useFetchAlbumsQuery } from '../store';

function AlbumsList({ user }) {
const results = useFetchAlbumsQuery(user);

console.log(results);
// console.log(data, error, isLoading);

return <div>Albums for {user.name}</div>;
}

export default AlbumsList;

In order to see this in the console, we’ll manually add in an example album into our db.json file:

{
"users": [
{
"id": 1,
"name": "Myra"
},
{
"name": "Elsie Price",
"id": 3
},
{
"name": "Harold Strosin",
"id": 4
}
],
"albums": [
{
"id": 10,
"title": "Hiking Trip",
"userId": 1
}
],
"photos": []
}

What we’ve done is create an album object with a random id, a title of “Hiking Trip” and then set the userId to be 1, which is the id of Myra.

So now if we open our page…

and then expand Myra…

We’d see the fetch request based on our albumsApi.js file, where the params were as follows:

            params: {
userId: user.id,

Our entire GET request would’ve been:

http://localhost:3005/albums?userId=1

As this is the user we expanded.

So we’ve passed in a user.id of 1 because we expanded the name with the id of 1, and therefore based on this…

const results = useFetchAlbumsQuery(user);

console.log(results);

we see the results back from the db in our fetch request:

{id: 10, title: "Hiking Trip", userId: 1}

as this is the result of returning the object with a userId of 1

We can prove this by adding in another album, and providing a userId of another user, for example Elsie Price who had an ID of 3:

{
"users": [
{
"id": 1,
"name": "Myra"
},
{
"name": "Elsie Price",
"id": 3
},
{
"name": "Harold Strosin",
"id": 4
}
],
"albums": [
{
"id": 10,
"title": "Hiking Trip",
"userId": 1
},
{
"id": 77,
"title": "Trip to Spain",
"userId": 3
}
],
"photos": []
}

now when we expand Elsie Prices album, we see the following in the fetch request:

Also note the console.log itself, which returns the following:

{status: 'pending', isUninitialized: false, isLoading: true, isSuccess: false, isError: false, …}
AlbumsList.js:6 {status: 'pending', endpointName: 'fetchAlbums', requestId: 'hGdBVfFKWb0SXUOXL8K3v', originalArgs: {…}, startedTimeStamp: 1691019409802, …}
AlbumsList.js:6 {status: 'fulfilled', endpointName: 'fetchAlbums', requestId: 'hGdBVfFKWb0SXUOXL8K3v', originalArgs: {…}, startedTimeStamp: 1691019409802, …}

If I deliberately “break” the fetch request, for example, by changing the port from 3005 to 3006 in the albumsApi.js file baseURL, then I will see rejected instead of fulfilled:

If we actually expand this object we’ll see the following:

So we can see that no data was returned, an error was produced, and isLoading is set to false and isError set to true

Let’s compare this to when it was fulfilled:

this time we DO have a data object that was retrieved, and also isError was set to false, which essentially means there was no eorr, and therefore no error object at all.

Because we know that useFetchAlbumsQuery(user) is returning all of this information, we caan use it to actually render the album data to our expandable panel, rather than just show it in the console.

Let’s update AlbumsList.js with the following:

import { useFetchAlbumsQuery } from "../store";
import Skeleton from "./Skeleton";
import ExpandablePanel from "./ExpandablePanel";
import Button from "./Button";

function AlbumsList({ user }) {
const { data, error, isLoading } = useFetchAlbumsQuery(user);

let content;
if (isLoading) {
content = <Skeleton times={3} className="h-10 w-full" />;
} else if (error) {
content = <div>Error fetching data...</div>;
} else {
content = data.map((album) => {
const header = <div>{album.title}</div>;
return (
<ExpandablePanel key={album.id} header={header}>
<div>Album ID: {album.id}</div>
</ExpandablePanel>
);
});
}

return <div className="ml-3">{content}</div>;
}

export default AlbumsList;

so we’re destructuring data, error and isLoading from useFetchAlbumsQuery(user)

we define a new variable called content, and then set multiple if statements.

if isLoading == true, then show 3x skeleton bars

if error == true, then show the text “Error fetching data…”

else if we have data, then map instances of the album array, and for each one, show an ExpandablePanel component which contains a div of the album.id value.

On screen we see this:

Adding an Album with Mutation

so far we’ve focussed on Query, but now we’ll add albums to the db, and to do this we use Mutation

To do this we’ll create another “endpoint” inside albumsApi.js

Difference between Queries and Mutations

--

--

Jeff P
Jeff P

Written by Jeff P

I tend to write about anything I find interesting. There’s not much more to it than that really :-)

No responses yet