In this article, we will learn how to use the Redux Toolkit for state management in React applications.
Most of us when writing React Apps tend to use the most popular Redux library for store management. For me i still use it in many of the applications since writing this post.
The Redux Toolkit library is a wrapper around the Redux to simplify the work with Redux. Basically it uses the same Redux Api internallyو when i checked it i figured it shortens many of the steps that I used to take previously. For this i write this post using a simple real world example so you can learn how to use easily.
To get started we will create a react app first:
npx create-react-app redux-toolkit-tutorial
Once app created go inside the project and install the redux toolkit:
npm install @reduxjs/toolkit react-redux
The react-redux package also still required when working with redux toolkit.
Now let’s think about the app we will build. We will consume an open source Api to search for books through a form. We will be using the openlibrary Api.
We will need two pages for our app, a search form that enables the user to input a search query and lists the searched books, and another page to view the book details.
Install the React Router:
npm install react-router-dom
Then create these folders and files inside the src/ directory of the project:
- pages/ directory
- index.js
- book-details.js
- components/ directory
- book.js
- search-form.js
- search-results.js
The index.js serves as the homepage of the app. The book-details.js represent the book details page. In the same way the components directory contains some helper components like the book.js which is the book item, the search-form.js represent the form to type in and search, the search-results.js is the list of searched books.
I have added the required layout with dummy data inside these pages as shown below:
index.js
import React from "react"; import SearchForm from "../components/search-form"; import SearchResults from "../components/search-results"; function Home() { return ( <div className='container'> <SearchForm /> <SearchResults /> </div> ) } export default Home;
book-details.js
import React from "react"; import {Link} from "react-router-dom"; function BookDetails() { return ( <div className="book-details-page"> <article className='book-item'> <span className="book-cover"> <Link to="/details/OL17930368W"><img width="300" src="//covers.openlibrary.org/b/olid/OL32336498M-M.jpg?ac4fc4&ac4fc4" alt="Cover of: Atomic Habits" title="Cover of: Atomic Habits" /></Link> </span> <div className="book-details"> <div className="result-title"> <h1 className="book-title"> Atomic Habits </h1> <h3 className="book-subtitle"> Tiny Changes, Remarkable Results : An Easy & Proven Way to Build Good Habits & Break Bad Ones </h3> </div> <span className="book-author"> by <a className="author">James Clear</a> </span> <span className="publish-info"> <span className="publish-year"> First published in 2018 </span> <a className='editions'>23 editions</a> </span> </div> </article> </div> ) } export default BookDetails;
search-form.js
import React from "react"; function SearchForm() { return ( <div className='search-form'> <form method="post" action="#"> <input type="text" name="q" placeholder="Enter search term" /> </form> </div> ) } export default SearchForm;
book.js
import React from "react"; import {Link} from "react-router-dom"; function BookItem() { return ( <article className='book-item'> <span className="book-cover"> <Link to="/details/OL17930368W"><img width="133" src="//covers.openlibrary.org/b/olid/OL32336498M-M.jpg?ac4fc4&ac4fc4" alt="Cover of: Atomic Habits" title="Cover of: Atomic Habits" /></Link> </span> <div className="book-details"> <div className="result-title"> <h3 className="book-title"> <Link to="/details/OL17930368W" className="results">Atomic Habits</Link> </h3> </div> <span className="book-author"> by <a className="author">James Clear</a> </span> <span className="publish-info"> <span className="publish-year"> First published in 2018 </span> <a className='editions'>23 editions</a> </span> </div> </article> ) } export default BookItem;
search-results.js
import React from "react"; import BookItem from "./book"; function SearchResults() { return ( <section className='search-results'> <h3>Search Results</h3> <BookItem /> <BookItem /> <BookItem /> <BookItem /> <BookItem /> <BookItem /> <BookItem /> </section> ) } export default SearchResults;
Then also update the App.js:
import React, {Fragment} from "react"; import './App.css'; import { BrowserRouter as Router, Routes, Route, Link } from "react-router-dom"; import Home from "./pages"; import BookDetails from "./pages/book-details"; function App() { return ( <Router> <Fragment> <div className="App"> <header className='App-header'> <h2>Book Search</h2> </header> <Routes> <Route path="/details/:id" element={<BookDetails />} /> <Route path="/" element={<Home />} /> </Routes> </div> </Fragment> </Router> ); } export default App;
In this code i imported the react router assets and add two routes for the app, the home route and the book details route.
Open App.css and add this code so our app can look good:
body { font-family: sans-serif, arial, 'Times New Roman'; } .App-header { background-color: #282c34; display: flex; flex-direction: column; align-items: start; justify-content: center; font-size: calc(10px + 2vmin); color: white; padding-left: 20px; } .search-form { display: flex; justify-content: center; justify-items: center; margin-top: 39px; margin-bottom: 15px; } input[type=text] { font-size: 20px; padding: 10px; border-radius: 10px; border: 1px solid #ccc; } .search-results { margin: 50px 10px 100px 20px; } .book-item { display: flex; margin-bottom: 15px; } .book-item a, .book-item h1, .book-item h3 { color: #504e4e; } .book-cover { margin-right: 20px; } .book-details { display: flex; flex-direction: column; padding-top: 23px; } .publish-info { margin-top: 12px; } .publish-year { font-size: .75em; display: block; color: #666; } .editions { margin-top: 11px; font-size: 15px; color: #02598b; font-size: .75em; } .book-details-page { margin-top: 50px; }
Now by running:
npm run start
You will see the app launched at port 3000.
So far what we have done is we generated a layout with dummy data. Let’s go ahead and focus in the most important part in this tutorial which is redux-toolkit.
Creating Store
Redux Toolkit provides us with new Api for handling redux store. This Api in fact is a wrapper of the normal redux Api but it aims to make development much easier.
For this reason the Redux Toolkit introduces these Apis:
- configureStore(): this is a wrapper for the default createStore() function which simplified config options with defaults. For example it contains the thunk middleware by default, enables the use of Devtools and combine the slice reducers.
- createSlice(): this function which we will use below to create a slice. It accepts an object of reducer functions, slice name, initial value and generates slice reducer with action creators and action types.
- createReducer(): with this function you can supply a lookup table of action types to map reducer functions. You can think of it as the normal switch statements you tend to write before.
- createAction(): generates action creator function for the given action type string.
- createAsyncThunk(): this function generates a thunk that dispatches a promise. It the same as async action in the normal redux.
To make things simple create store/ directory inside of src/
- store/ directory
- index.js
- reducers/ directory
- books-slice.js
We have one file here inside of the reducers/ directory which is the books-slice.js. This file will contain the createSlice() code to create a reducer and actions.
books-slice.js
import { createSlice } from '@reduxjs/toolkit'; import { searchBooks } from '../../services/open-library.api.js'; const initialState = { books: null, loading: false, booksLoaded: false, error: false } const booksSlice = createSlice({ name: 'books', initialState, reducers: { startLoading(state, action) { state.loading = true; state.booksLoaded = false; }, booksLoaded(state, action) { state.loading = false; state.booksLoaded = true; if(action && action.payload.docs) { state.books = action.payload; state.error = false; } }, booksLoadFail(state, action) { state.loading = false; state.booksLoaded = true; state.error = true; } } }); export const {startLoading, booksLoaded, booksLoadFail} = booksSlice.actions; export default booksSlice.reducer; export const fetchBooks = (q) => async dispatch => { dispatch(startLoading()); const response = await searchBooks(q); if(response.docs) { dispatch(booksLoaded(response)); } else { dispatch(booksLoadFail(response)); } };
In this code first i imported the createSlice() wrapper from “@redux/toolkit“. Next i imported the searchBooks() function which make an http request to search for books in the openlibrary, we will create it below.
Next i created i invoked createSlice() and assign it to booksSlice variable. The createSlice() accepts an object which takes a name which identifies this slice, an initial value which in this case contains four keys, (books, loading, booksLoaded, error) and reducers object.
The reducers object contains the functions to update the store. in this case we have three functions, startLoading, booksLoaded, booksLoadFail. Each function accepts (state, action ) just like the normal reducer with switch statements.
Inside each of these functions i am updating the state with relevant values.
Also i created an async action fetchBooks which invokes the searchBooks() function and dispatches the relevant actions above to update the store.
Finally i exported the actions from booksSlice.actions and the reducer from booksSlice.reducer. This means that createSlice() create both the actions and reducer using single code.
Compare this with the normal redux code, you have to write separate logic for actions and separate logic for reducers but Redux Toolkit shortens this process.
Open store/index.js
import { configureStore } from "@reduxjs/toolkit"; import booksReducer from './reducers/books-slice'; const store = configureStore({ reducer: { books: booksReducer } }); export default store;
In this code i called the configureStore() and pass it an object with the available reducers in our app. In this case we have only one reducer but i real world application you might have multiple reducers. Then is exported the store to be use added to the provider in the <App/> component.
Open index.js and add the store:
import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import App from './App'; import store from './store'; ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root'));
Create the services/ directory inside of src/ with these files:
- constants.js
- open-library.api.js
services/constants.js
export const BASE_URL = 'https://openlibrary.org';
services/open-library.api.js
import { BASE_URL } from './constants'; export async function searchBooks(query) { let data; try { const response = await window.fetch(`${BASE_URL}/search.json?q=${query}`) data = await response.json() if (response.ok) { return data } throw new Error(response.statusText) } catch (err) { return Promise.reject(err.message ? err.message : data) } } export async function getBookDetail(id) { let data; try { const response = await window.fetch(`${BASE_URL}/books/${id}.json`) data = await response.json() if (response.ok) { return data } throw new Error(response.statusText) } catch (err) { return Promise.reject(err.message ? err.message : data) } }
Here we have to functions that make http request using javascript fetch() Api. The searchBooks() searchs for books using particular query. And getBookDetail() retrieves the book details using book key.
Updating Components
Now to use the previously created store in UI we will follow the same technique as normal redux. Let’s start with the <SearchForm /> component
components/search-form.js
import React from "react"; import { useDispatch } from "react-redux"; import {fetchBooks} from "../store/reducers/books-slice"; function SearchForm() { const [searchTerm, setSearchTerm] = React.useState(''); const dispatch = useDispatch(); const handleSubmit = e => { e.preventDefault(); if(searchTerm) { dispatch(fetchBooks(searchTerm)); } } return ( <div className='search-form'> <form method="post" action="#" onSubmit={handleSubmit}> <input type="text" name="q" placeholder="Enter search term" value={searchTerm} onChange={e => setSearchTerm(e.target.value)} /> </form> </div> ) } export default SearchForm;
In this form we dispatches an action “fetchBooks” passing in the search term whenever user types in the search input and submits the form by hitting enter key. The fetchBooks action typically will make http request to the dedicated endpoint to retrieve the book data. This is the first stage.
To display this data open search-results.js
components/search-results.js
import React from "react"; import { useSelector } from "react-redux"; import BookItem from "./book"; function SearchResults() { const loading = useSelector(state => state.books.loading); const error = useSelector(state => state.books.error); const books = useSelector(state => state.books.books); return loading ? <h3>Loading...</h3> : error ? <p>Error loading data</p> : books ? ( <section className='search-results'> <h3>Search Results</h3> { books.docs.map((b, index) => { return <BookItem key={index} data={b} /> }) } </section> ) : <></> } export default SearchResults;
In this code we are using useSelector() from “react-redux” to select specific data from the store. Next we select “loading, error, books” keys from the store. Then i check for the loading variable to display a loader indicator in this case it’s a simple text otherwise we display the data.
Now update the book.js which renders a book item.
components/book.js
import React from "react"; import {Link} from "react-router-dom"; function BookItem({data}) { return ( <article className='book-item'> <span className="book-cover"> { data.cover_i && ( <Link to={`${data.lending_edition_s ? '/details/' + data.lending_edition_s : '#'}`}><img width="133" src={`https://covers.openlibrary.org/b/id/${data.cover_i}.jpg`} alt={data.title} /></Link> ) } </span> <div className="book-details"> <div className="result-title"> <h3 className="book-title"> <Link to={`${data.lending_edition_s ? '/details/' + data.lending_edition_s : '#'}`} className="results">{data.title}</Link> </h3> </div> <span className="book-author"> by <a className="author">{data.author_name ? data.author_name[0] : ''}</a> </span> <span className="publish-info"> <span className="publish-year"> First published in {data.first_publish_year} </span> <a className='editions'>{data.edition_count} editions</a> </span> </div> </article> ) } export default BookItem;
This components accepts a single prop “data” that contains the book item object.
The final piece is to update book-details.js which fetches and displays a single book.
pages/book-details.js
import React from "react"; import {Link, useParams} from "react-router-dom"; import { getBookDetail } from "../services/open-library.api"; function BookDetails() { let { id } = useParams(); const [bookDetail, setBookDetail] = React.useState(null); const [loading, setLoading] = React.useState(false); React.useEffect(() => { async function getDetails() { setLoading(true); const response = await getBookDetail(id); setLoading(false); if(response) { setBookDetail(response); } } getDetails(); }, [id]); return ( <> { loading ? <h3>Loading...</h3> : bookDetail ? ( <div className="book-details-page"> <article className='book-item'> <span className="book-cover"> { bookDetail.covers && <img width="300" src={`https://covers.openlibrary.org/b/id/${bookDetail.covers[0]}.jpg`} alt={bookDetail.title} title={bookDetail.title} /> } </span> <div className="book-details"> <div className="result-title"> <h1 className="book-title"> {bookDetail.title} </h1> </div> <span className="book-author"> by <a className="author">{bookDetail.by_statement ? bookDetail.by_statement : ''}</a> </span> <span className="publish-info"> <span className="publish-year"> Published in {bookDetail.publish_date} </span> <span className="publish-year"> Publish country: {bookDetail.publish_country} </span> <a className='editions'>{bookDetail.edition_name}</a> </span> </div> </article> </div> ) : <div>Error loading</div> } </> ) } export default BookDetails;
This page makes use of React.useEffect() to fetch the book details using the getBookDetail(id) function from open-library.api.js we created above. The id param retrieved using the useParams() hook. And then i set state data using setBookDetail() and display the data in the same way we did in <BookItem/> component.
Further Reading
In addition to what you have learned in this post i advice you to check related Apis in Redux Toolkit: