State management is one of the most important portion of any Front end development framework. Almost every FE framework offers one or many state management libraries. For example, Redux & Recoil for React, Vuex for VueJS and NgRx for Angular. In this article, we're going to create a very simple Reading List application which will have a redux store set up and we will be using FakerAPI for the mock response.
You can checkout the demo application Here .
Also, the source-code can be found Here on my GitHub. It is very basic application which fetches books from FakerAPI and you'll also be able to add Books.
Prerequisite
I assume that you already have a good understanding of React Components, Props & states(in general).
Getting Started
So we start simple by creating a React application using CRA and installing required dependencies afterwards.
create-react-app reading-list
This will generate the react application. Now, navigate into the newly created application and install the dependencies using
cd reading-list
npm install redux react-redux redux-thunk redux-devtools-extension axios
Now, few things to note here, redux alone is independent of any frameworks. react-redux is what let us use redux for react application. Also, we need some kind of middleware, redux-thunk in our case for basic Redux side effects logic, including complex synchronous logic that needs access to the store, and simple async logic like AJAX requests since With a plain basic Redux store, you can only do simple synchronous updates by dispatching an action. Middleware extends the store's abilities, and lets you write async logic that interacts with the store.
Also, redux-devtools-extension makes it easy to integrate Redux DevTools which speeds up our app debugging process. Axios works great for fetching data from the apis.
Folder Structure
Now, let's take a look at our folder structure
We'll create 3 folders actions , components & reducers inside our src folder. Inside the components folder, we'll create 3 components, BookList for looping through the List of Books, BookForm for adding a new Book & BookDetail for displaying each book's Detail.
Inside reducers folder, we'll have 2 files, index.js which will be our rootReducer & bookReducer.
Setting up the store
In order to set up the store, replace the src/index.js file with
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
// Imports for Redux
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension';
// root reducer import
import rootReducer from './reducers';
const store = createStore(
rootReducer,
composeWithDevTools(applyMiddleware(thunk))
);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
Creating components
Since the store has been set up, we can start writing our components. Add the following code to build up our components:
// src/components/BookDetail.js
import React from 'react';
const BookDetails = ({ book }) => {
return (
<li>
<div className="title">{book.title}</div>
<div className="author">{book.author}</div>
</li>
);
};
export default BookDetails;
// src/components/BookForm.js
import React, { useState } from 'react';
import { connect } from 'react-redux';
import { addBook } from '../actions/bookActions';
const BookForm = ({ dispatch }) => {
const [title, setTitle] = useState('');
const [author, setAuthor] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
const newBook = {
title,
author,
id: 5,
};
dispatch(addBook(newBook));
setTitle('');
setAuthor('');
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="book title"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
<input
type="text"
placeholder="author"
value={author}
onChange={(e) => setAuthor(e.target.value)}
required
/>
<input type="submit" value="add book" />
</form>
);
};
export default connect(null)(BookForm);
// src/components/BookList.js
import React, { useEffect } from 'react';
import { connect } from 'react-redux';
import BookDetails from './BookDetails';
import { fetchBooks } from '../actions/bookActions';
const BookList = ({ dispatch, books }) => {
useEffect(() => {
dispatch(fetchBooks());
}, [dispatch]);
return books.length ? (
<div className="book-list">
<ul>
{books.map((book) => {
return <BookDetails book={book} key={book.id} />;
})}
</ul>
</div>
) : (
<div className="empty">No books to read</div>
);
};
const mapStateToProps = (state) => ({
books: state.books.books,
});
export default connect(mapStateToProps)(BookList);
Setting up Reducers
Reducers are what responsible for mutating states. It reads the dispatched action type and mutates the state accordingly. There is one main reducer generally called rootReducer which keeps track of all the other reducers. If you look at src/index.js, in createStore method, we only passed on Reducer which is root Reducer. The rootReducer contains all the other reducers.
Add the following code in src/reducers/index.js
import { combineReducers } from 'redux';
import booksReducer from './booksReducer';
const rootReducer = combineReducers({
books: booksReducer,
});
export default rootReducer;
We see that it is calling combineReducers method from redux and taking all the other reducers.
Add the following code to src/reducers/bookReducer.js
import { GET_BOOKS, ADD_BOOK } from '../actions/bookActions';
export const initialState = {
books: [],
};
export default function bookReducer(state = initialState, action) {
switch (action.type) {
case GET_BOOKS:
return {
...state,
books: action.payload,
};
case ADD_BOOK:
return {
...state,
books: [...state.books, action.payload],
};
default:
return state;
}
}
Setting up actions
Add the following code to src/actions/bookActions.js
import Axios from 'axios';
export const GET_BOOKS = 'GET_BOOKS';
export const ADD_BOOK = 'ADD_BOOk';
export const fetchBooks = () => async (dispatch) => {
const data = await fetchData();
dispatch({
type: GET_BOOKS,
payload: data,
});
};
export const addBook = (newBook) => async (dispatch) => {
dispatch({
type: ADD_BOOK,
payload: newBook,
});
};
// fetch data from the API
const fetchData = async () => {
try {
const res = await Axios.get(
'https://fakerapi.it/api/v1/custom?_quantity=5&author=name&id=counter&title=city'
);
return res.data.data;
} catch (error) {
console.log(error);
}
};
Styling
Since we're mainly focused on setting up redux, it doesn't mean our application has to look ugly. That's why i've already written some basic styling which will make our app look decent.
Replace all the codes in src/index.css with following
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: #553055;
}
.App {
background: #4c2a4c;
margin: 20px auto;
width: 90%;
max-width: 700px;
color: #eee;
}
.navbar {
padding: 10px 20px;
text-align: center;
background: #6d3d6d;
}
.navbar h1 {
margin: 10px 0;
}
.book-list {
margin: 20px;
}
.book-list ul {
padding: 0;
list-style-type: none;
}
.book-list li {
background: #6d3d6d;
border-radius: 4px;
padding: 10px;
cursor: pointer;
margin: 10px 0;
}
.book-list li:hover {
opacity: 0.7;
text-decoration: line-through;
}
.book-list .title {
font-weight: bold;
color: #fff;
font-size: 1.2em;
}
.book-list .author {
font-size: 0.9em;
color: #ddd;
}
.empty {
margin: 20px;
text-align: center;
}
form {
padding: 20px;
}
input[type='text'] {
width: 100%;
padding: 10px;
box-sizing: border-box;
margin: 6px 0;
background: #3c1f3c;
color: #fff;
border: 0;
}
input[type='submit'] {
margin: 10px auto;
background: #eee;
border: 0;
padding: 6px 20px;
display: block;
}
Now, finally, let's add our components to src/App.js . Replace all the codes in src/App.js with following
import BookForm from './components/BookForm';
import BookList from './components/BookList';
function App() {
return (
<div className="App">
<BookList />
<BookForm />
</div>
);
}
export default App;
Now, if you've followed everything accordingly, once you start the server, you can see the application running. Also if you look at Redux DevTools you'll be able to see how the states changed, what actions were launched.
If you run in any problem, you can always use the code from Here as a reference.
Top comments (1)
Hi, I'm a Redux maintainer. As an FYI, we now recommend several newer patterns than what's shown in this post. In particular, you should be using our official Redux Toolkit package to set up the store and write your Redux logic, and we recommend using a "feature folder" structure with "slice" files for logic rather than splitting files across multiple folders.
I strongly recommend reading through the new newly rewritten official tutorials in the Redux docs, which have been specifically designed to teach you how Redux works and show modern practices:
The older patterns shown in almost all other tutorials on the internet are still valid, but not how we recommend writing Redux code today.
You should also read through the Redux "Style Guide" docs page, which explains our recommended patterns and best practices. Following those will result in better and more maintainable Redux apps.
In addition, the easiest way to start a new project is with the official Redux+JS template for Create-React-App. It comes with Redux Toolkit and the React-Redux hooks API already set up when the project is created.