Redux Toolkit
Introduction video
Table of Contents
Open Table of Contents
Installation
# NPM
npm install @reduxjs/toolkit react-redux
# Yarn
yarn add @reduxjs/toolkit react-redux
Basic usage
Docs
Barebone example
Document structure
└── src
└── redux
├── slice
│ └── countSlice.ts
├── hooks.ts
└── store.ts
Step setup
Create slice file
//redux/slice/countSlice.ts
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
type CounterStateProps = {
value: number;
};
const initialState: CounterStateProps = {
value: 0,
};
const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
incremented(state) {
state.value++;
},
// very important to pass PayloadAction type for typescript inference to work
amountAdded(state, action: PayloadAction<number>) {
state.value += action.payload;
},
},
});
export const { incremented, amountAdded } = counterSlice.actions;
export default counterSlice.reducer;
Create store
Create store.ts inside src folder
Team recommends only one store file per project although more than 1 is possible but not recommended
// store.ts
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "../redux/slice/counterSlice";
const store = configureStore({
reducer: {
counter: counterReducer,
},
});
export default store;
// important for type inference
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
Configure provider
Configure provider and pass store.ts to it in main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { Provider } from "react-redux";
import App from "./App";
import "./index.css";
import store from "./redux/store";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
Create Hooks file
// redux/hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import { AppDispatch, RootState } from "./store";
// useAppDispatch and useAppSelector setup helps typescript auto complete
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
Use toolkit
// app.jsx
import { useAppDispatch, useAppSelector } from "./redux/hooks";
import { incremented, amountAdded } from "./redux/slice/counterSlice";
import "./App.css";
function App() {
const value = useAppSelector(state => state.counter.value);
const dispatch = useAppDispatch();
function handleIncrement() {
dispatch(incremented());
}
function addOnClick() {
dispatch(amountAdded(2));
}
return (
<>
<h1>React Redux Toolkit</h1>
<div className="card">
<p>Count {value}</p>
<button style={{ marginInline: "5px" }} onClick={handleIncrement}>
Click to increment count
</button>
<button style={{ marginInline: "5px" }} onClick={addOnClick}>
Click to increment by 2
</button>
</div>
</>
);
}
export default App;
RTK Query
Basic use
src
└── redux
├── store.ts
├── hooks.ts
└── slice
└── apiSlice.ts
Create Slice file
// apiSlice.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
const DOGS_API_KEY = 'cbfb51a2-84b6-4025-a3e2-ed8616edf311';
type Breed = {
id: string;
name: string;
image: {
url: string;
};
}
const apiSlice = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: 'https://api.thedogapi.com/v1',
prepareHeaders(headers) {
headers.set('x-api-key', DOGS_API_KEY);
return headers;
},
}),
endpoints(builder) {
return {
fetchBreeds: builder.query<Breed[], number | void>({
query(limit = 10) {
return `/breeds?limit=${limit}`;
},
}),
};
},
});
export default apiSlice
Include Slice file in store
//store.ts
import { configureStore } from "@reduxjs/toolkit";
import apiSlice from "./slice/dogsApiSlice";
const store = configureStore({
reducer: {
[apiSlice.reducerPath]: apiSlice.reducer,
},
middleware: getDefaultMiddleware => {
return getDefaultMiddleware().concat(apiSlice.middleware);
},
});
export default store;
Export query hook
// hooks.ts
import apiSlice from "./slice/dogsApiSlice";
export const { useFetchBreedsQuery } = apiSlice;
Configure provider
Configure provider and pass store.ts to it in main.tsx
// main.ts
import React from "react";
import ReactDOM from "react-dom/client";
import { Provider } from "react-redux";
import App from "./App";
import "./index.css";
import store from "./store";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
Use it
//app.tsx
import "./App.css";
import { useFetchBreedsQuery } from "./redux/hooks";
function App() {
const { data, isLoading, isSuccess, isError, error } =
useFetchBreedsQuery(20);
if (isLoading) {
return <h2>loading...</h2>;
} else if (isSuccess) {
return (
<>
<h2>Number of dogs fetched:- {data.length}</h2>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(3, 400px)",
gap: "5px",
}}
>
{data.map(breed => (
<div
key={breed.id}
style={{ border: "2px solid white", borderRadius: "4px" }}
>
<h2>{breed.name}</h2>
<img
style={{ width: "100%", height: "300px", objectFit: "cover" }}
src={breed.image.url}
alt=""
loading="lazy"
/>
</div>
))}
</div>
</>
);
} else if (isError) {
return <div>Error occured {JSON.stringify(error)}</div>;
} else {
<h2>Hello there</h2>;
}
}
export default App;
Async thunk
Basic fetch post
src
└── redux
├── store.ts
├── hooks.ts
└── slice
└── postSlice.ts
Slice
// postSlice.ts
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";
const POSTS_URL = "https://rickandmortyapi.com/api/character";
export enum LoadingStatus {
IDLE = "idle",
LOADING = "loading",
SUCCEEDED = "succeeded",
FAILED = "failed",
}
type StateProps = {
posts: PostProps[]; // structure of data
status: LoadingStatus;
error: string | undefined;
};
const initialState: StateProps = {
posts: [],
status: LoadingStatus.IDLE, // idle | loading | succeeded | failed
error: "",
};
export const fetchPosts = createAsyncThunk("posts/fetchPosts", async () => {
// try catch block is not needed here
// createAsyncThunk will handle errors here internally
const response = await axios.get(POSTS_URL);
return response.data.results;
});
const postsSlice = createSlice({
name: "posts",
initialState,
reducers: {},
extraReducers(builder) {
builder
.addCase(fetchPosts.pending, state => {
state.status = LoadingStatus.LOADING;
})
.addCase(fetchPosts.fulfilled, (state, action) => {
state.status = LoadingStatus.SUCCEEDED;
state.posts = action.payload;
})
.addCase(fetchPosts.rejected, (state, action) => {
state.status = LoadingStatus.FAILED;
state.error = action.error.message;
});
},
});
export default postsSlice.reducer;
store
// store.ts
import { configureStore } from "@reduxjs/toolkit";
import postReducer from "../redux/slice/postSlice";
export const store = configureStore({
reducer: {
posts: postReducer,
},
});
// important for type inference
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
Hooks
// hooks.ts
// redux/hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import { AppDispatch, RootState } from "./store";
// useAppDispatch and useAppSelector setup helps typescript auto complete
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
Use
// App.tsx
import { fetchPosts, LoadingStatus } from "./redux/slice/postSlice";
import { useEffect } from "react";
import { useAppDispatch, useAppSelector } from "./redux/hooks";
export default function App() {
const { status, error, posts } = useAppSelector(state => state.posts);
const dispatch = useAppDispatch();
useEffect(() => {
if (status === LoadingStatus.IDLE) {
// fetch all post on component mount
dispatch(fetchPosts());
}
}, []);
if (status === LoadingStatus.LOADING) return <h2>Loading...</h2>;
else if (status === LoadingStatus.SUCCEEDED)
return (
<div>
<h1>Hello world</h1>
{posts.map(post => (
<div key={post.id} style={{ border: "2px solid #fff" }}>
<h2>{post.name}</h2>
<p>{post.gender}</p>
<img src={post.image} loading="lazy" />
</div>
))}
</div>
);
else if (status === LoadingStatus.FAILED) return <h2>{error}</h2>;
}
ApiSlice setup
src
└── redux
├── store.js
├── hooks.js
└── slice
└── postSlice.js
store.js
//store.ts
import { configureStore } from "@reduxjs/toolkit";
import { apiSlice } from './slice/apiSlice';
const store = configureStore({
reducer: {
[apiSlice.reducerPath]: apiSlice.reducer,
},
middleware: getDefaultMiddleware => {
return getDefaultMiddleware().concat(apiSlice.middleware);
},
});
export default store;
hooks.js
// hooks.ts
import { apiSlice } from "./slice/apiSlice";
export const { useGetPostsQuery, useAddPostsMutation, useUpdatePostsMutation, useDeletePostsMutation } = apiSlice;
apiSlice.js
// apiSlice.js
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
export const apiSlice = createApi({
reducerPath: "api",
baseQuery: fetchBaseQuery({ baseUrl: "https://653c7e46d5d6790f5ec805f4.mockapi.io/" }),
tagTypes: ['Posts'],
endpoints: (builder) => ({
getPosts: builder.query({
query: () => "/posts",
providesTags: ['Posts']
}),
addPosts: builder.mutation({
query: (todo) => ({
url: "/posts",
method: "POST",
body: todo
}),
invalidatesTags: ["Posts"]
}),
updatePosts: builder.mutation({
query: (todo) => ({
url: `/posts/${todo.id}`,
method: "PUT",
body: todo
}),
invalidatesTags: ["Posts"]
}),
deletePosts: builder.mutation({
query: ({ id }) => ({
url: `/posts/${id}`,
method: "DELETE",
body: id
}),
invalidatesTags: ["Posts"]
}),
})
});
index.js
// index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
import { Provider } from 'react-redux'
import store from './app/redux/store'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
)