It is easier to build web applications with the React library alone. But if the app is getting bigger, we really need a state-management tool. React library itself contains Context API that works perfectly to initialize states and functions globally on our app. Also Redux, a third-party state-management tool gives more control over our state management. But we need middlewares like Redux-Thunk, Redux-Saga, etc. to handle the side-effects of Redux. Here we will discuss the steps to integrate or use Redux-Saga in a React app by building a simple blog as an example.
Prerequisites
To follow this article, the reader should be aware of the following technologies:-
- JavaScript ES6
- React.js library
- Using Bootstrap in a React app
- Basics for Redux state-management tool
What we will learn
In this article, we will learn the following things:-
- What is Redux-Saga
- To Create a React project
- Adding Bootstrap(react-bootstrap) in our app
- Creating multiple routes(act as pages)
- Creating components in a React app
- Integrating Redux-Saga in our app
- Using third-party APIs
- Structuring a React Reduxt-Saga project
What is Redux-Saga?
Redux-Saga is a middleware that handles the side effects of Redux. We might get confused with the word side effect.
What is a side effect:-
The normal redux flow is when an action is dispatched, some state is changed. But when implementing the real-world use cases, we might need to call APIs, access local storage, etc., and change states according to the results. These processes are called side effects.
Redux can only change states with an action dispatch. But sometimes we need to call APIs, access local storage before changing the state. These are called side effects.
How to solve side effects:-
We can solve these side effects by using middlewares like redux-thunk, redux-saga, etc. with pure redux.
Here in this article, we are discussing the Redux-Saga and not the Redux-Thunk.
How to use Redux-Saga in a React app
So let us start learning the steps of integrating Redux-Saga in a React app. Here we are going to build a simple blog in React and Redux-Saga. So that we can know the exact file structure and steps of building a large-scale app.
About the app we are going to build
By using a third-party API, we will get the list of posts and show this result on the home page as cards. Clicking each post will direct us to another page that displays the single post.
Also, clicking the title on the Navigation bar will direct us to the home page.
The below GIF will give us an idea about the workflow of the app.
I am also giving the complete file structure of the app for a better understanding of the app we are going to build.
I recommend you to refer the file structure in each step to get the position of the file.
Also, note that the blog we are building is to make you understand the concept of using redux-saga in React app. So let us start coding the app from scratch.
Create a new React project
The first step is setting up a React application on your system. This can be easily done using the NPX tool.
So, install Node.js on your system first and create a react application using NPX. Don’t bother about the term NPX, because it’s a tool coming with NPM(Node Package Manager) 5.2+ onwards which will install on your system with Node.js itself.
If you need further assistance in the installation of React on your system, use the below links.
Install React on Windows, Ubuntu, and macOS
npx create-react-app react-redux-saga-example-blog
This command will create a react application with the project name react-redux-saga-example-blog
Now enter the project directory and start the app.
cd react-redux-saga-example-blog npm start
It will open up the React application we have created in our browser window with the address https://localhost:3000. The port may vary if 3000 is busy.
Now we can use our favorite code editor to edit our project. I personally recommend Visual Studio Code.
Integrate Boostrap in our app
We are using the react-boostrap package for integrating Bootstrap in our React app.
Install the packages React-bootstrap and Bootstrap in our app with the below command.
npm install react-bootstrap@next bootstrap@5.1.1
Now import the boostrap.min.css in the index.js file.
// index.js
import "bootstrap/dist/css/bootstrap.min.css";
Now can use React-bootstrap components inside our React app.
Add some custom SCSS styles
We are also adding custom-style files(SCSS) to our app. But to compile the SCSS styles, we need to install the node-sass package first. So, install the supported version 4.14.1 of node-sass in our app.
npm install node-sass@4.14.1
Now write the styles in /assets/scss/style.scss file.
// assets/scss/style.scss
a {
text-decoration: none !important;
}
.home {
margin-top: 2rem;
.posts {
display: flex;
justify-content: center;
}
}
.posts {
img {
width: 100%;
height: auto;
margin-bottom: 3rem;
}
h1 {
margin-bottom: 1rem;
}
margin-top: 2rem;
display: flex;
justify-content: center;
}
.loader {
position: absolute;
left: 40%;
top: 50%;
}
.card {
margin-bottom: 1rem;
.card-title {
font-size: 1.5em;
color: black;
}
.card-text {
font-size: 1em;
color: black;
}
&:hover {
background: rgb(228, 228, 228);
}
}
We need to import this style file in the index.js file as well.
// index.js
import "./assets/scss/style.scss";
Implementing the multiple routes
We need two pages in our app. Home page to show the list of posts and another page to show the single post.
Route | Corresponding page |
/ | Home page |
/:id | Post page |
The /:id refers to a dynamic route where we need to replace the post id such as /1, /2, /3, etc.
This id will be used to fetch the corresponding single post from the API.
To implement multiple routes or pages in a react app, we need to install the react-router-dom package.
npm i react-router-dom
Now import the BrowserRouter component from the react-router-dom package in the src/index.js file.
import { BrowserRouter } from "react-router-dom";
Also, wrap the entire app inside this BrowserRouter component.
<BrowserRouter>
<App />
</BrowserRouter>
We need to add some more code to the index.js file later and so I am not giving the complete index.js file here.
Now we need to define the routes and the corresponding route components inside the App.js file.
<Router>
<Navigation />
<Switch>
<Route path="/" exact component={() => <Home />} />
<Route path="/:id" exact component={() => <Singlepost />} />
</Switch>
</Router>
We will code the Navigation component, Home component and SinglePost component later.
So that the complete App.js file is given below.
// src/App.js
import React from "react";
import Home from "./pages/Home";
import Singlepost from "./pages/SinglePost";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import "react-loader-spinner/dist/loader/css/react-spinner-loader.css";
import Navigation from "./components/Navigation";
function App() {
return (
<div className="App">
<Router>
<Navigation />
<Switch>
<Route path="/" exact component={() => <Home />} />
<Route path="/:id" exact component={() => <Singlepost />} />
</Switch>
</Router>
</div>
);
}
export default App;
Setup a top Navigation
A top navigation bar with a title section is needed in our app where clicking the title will direct us to the home page.
// src/components/Navigation.js
import React from "react";
import { Link } from "react-router-dom";
function Navigation() {
return (
<div className="navigation">
<nav class="navbar navbar-expand navbar-dark bg-dark">
<div class="container">
<Link class="navbar-brand" to="/">
React Redux-Saga Blog
</Link>
</div>
</nav>
</div>
);
}
export default Navigation;
Setup a .env file
I am setting up a .env file to store the API URL.
Note that, every time you change the .env file, we should restart the app.
REACT_APP_APP_URL= "https://jsonplaceholder.typicode.com"
The Redux-Saga part
Almost all features are implemented in our app except the main part which is redux-saga. If you are already working with Redux projects, you will get a better understanding of the following steps.
Setup store and wrap the entire app inside Provider
First of all, set up a store and wrap the entire app inside a Provider component and pass the store as a prop. This is common for a Redux project.
Create a directory with name store and inside, create an index.js file.
// src/store/index.js
import { createStore, applyMiddleware, compose } from "redux";
import createSagaMiddleware from "redux-saga";
import rootReducer from "./reducers";
import rootSaga from "./sagas";
const sagaMiddleware = createSagaMiddleware();
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
rootReducer,
composeEnhancers(applyMiddleware(sagaMiddleware))
);
sagaMiddleware.run(rootSaga);
export default store;
We will discuss reducers and the sagas mentioned in this file later.
Now, import the created file and Provider component to our index.js file.
import store from "./store";
import { Provider } from "react-redux";
Wrap the entire app with the Provider component and pass the store as a prop.
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
So that the entire index.js file looks the same as below.
// src/index.js
import React from "react";
import ReactDOM from "react-dom";
import "bootstrap/dist/css/bootstrap.min.css";
import "./assets/scss/style.scss";
import App from "./App";
import { BrowserRouter } from "react-router-dom";
import { Provider } from "react-redux";
import store from "./store";
const app = (
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
);
ReactDOM.render(app, document.getElementById("root"));
Define the action types and actions
Before defining the actions, we need to set up the action types. Action types are used to identify the corresponding reducer from the action. Let us define the action types first.
In this article, we are only dealing with the posts and so, create a directory named posts inside the store and add the actionTypes.js file.
Note that, in this file, we are just assigning the action type strings to variables for easily importing them in actions, reducers, and saga.
// src/store/posts/actionTypes.js
export const GET_POSTS = "GET_POSTS";
export const GET_POSTS_SUCCESS = "GET_POSTS_SUCCESS";
export const GET_POSTS_FAIL = "GET_POSTS_FAIL";
export const GET_POST_DETAILS = "GET_POST_DETAILS";
export const GET_POST_DETAILS_SUCCESS = "GET_POST_DETAILS_SUCCESS";
export const GET_POST_DETAILS_FAIL = "GET_POST_DETAILS_FAIL";
Now import these variables to an action file /store/posts/actions.js file.
import {
GET_POSTS,
GET_POSTS_SUCCESS,
GET_POSTS_FAIL,
GET_POST_DETAILS,
GET_POST_DETAILS_SUCCESS,
GET_POST_DETAILS_FAIL,
} from "./actionTypes";
Also, define all the actions related to posts inside it.
export const getPosts = () => {
return {
type: GET_POSTS,
};
};
export const getPostsSuccess = (posts) => {
return {
type: GET_POSTS_SUCCESS,
payload: posts,
};
};
export const getPostsFail = (error) => {
return {
type: GET_POSTS_FAIL,
payload: error,
};
};
...
When we are loading the home page, it dispatches an action getPosts() just after the component is mounted.
It then reaches the corresponding action, reducer, saga and fetches the posts from API, and dispatches the getPostsSucces() action if the saga response is successful otherwise dispatches getPostsFail() if the saga response is a failure.
The complete actions.js file will be the same as mentioned below.
// src/store/posts/actions.js
import {
GET_POSTS,
GET_POSTS_SUCCESS,
GET_POSTS_FAIL,
GET_POST_DETAILS,
GET_POST_DETAILS_SUCCESS,
GET_POST_DETAILS_FAIL,
} from "./actionTypes";
export const getPosts = () => {
return {
type: GET_POSTS,
};
};
export const getPostsSuccess = (posts) => {
return {
type: GET_POSTS_SUCCESS,
payload: posts,
};
};
export const getPostsFail = (error) => {
return {
type: GET_POSTS_FAIL,
payload: error,
};
};
export const getPostDetails = (id) => {
return {
type: GET_POST_DETAILS,
payload: id,
};
};
export const getPostDetailsSuccess = (post) => {
return {
type: GET_POST_DETAILS_SUCCESS,
payload: post,
};
};
export const getPostDetailsFail = (error) => {
return {
type: GET_POST_DETAILS_FAIL,
payload: error,
};
};
The Reducer part
Create a file reducer.js inside /store/posts directory. Import all the action types we declared earlier.
import {
GET_POSTS,
GET_POSTS_SUCCESS,
GET_POSTS_FAIL,
GET_POST_DETAILS,
GET_POST_DETAILS_SUCCESS,
GET_POST_DETAILS_FAIL,
} from "./actionTypes";
Now initialize the states needed for the posts.
const initialState = {
posts: [],
post: {},
loadingPosts: false,
loadingPostDetails: false,
error: {
message: "",
},
};
Now, define the PostReducer.
const PostReducer = (state = initialState, action) => {
switch (action.type) {
case GET_POSTS:
state = { ...state, loadingPosts: true };
break;
case GET_POSTS_SUCCESS:
state = { ...state, posts: action.payload, loadingPosts: false };
break;
case GET_POSTS_FAIL:
state = {
...state,
error: {
message: "Error",
},
loadingPosts: false,
};
break;
...
default:
state = { ...state };
break;
}
return state;
};
The complete reducer file looks the same as below.
// src/store/posts/reducer.js
import {
GET_POSTS,
GET_POSTS_SUCCESS,
GET_POSTS_FAIL,
GET_POST_DETAILS,
GET_POST_DETAILS_SUCCESS,
GET_POST_DETAILS_FAIL,
} from "./actionTypes";
const initialState = {
posts: [],
post: {},
loadingPosts: false,
loadingPostDetails: false,
error: {
message: "",
},
};
const PostReducer = (state = initialState, action) => {
switch (action.type) {
case GET_POSTS:
state = { ...state, loadingPosts: true };
break;
case GET_POSTS_SUCCESS:
state = { ...state, posts: action.payload, loadingPosts: false };
break;
case GET_POSTS_FAIL:
state = {
...state,
error: {
message: "Error",
},
loadingPosts: false,
};
break;
case GET_POST_DETAILS:
state = { ...state, loadingPostDetails: true };
break;
case GET_POST_DETAILS_SUCCESS:
state = { ...state, post: action.payload[0], loadingPostDetails: false };
break;
case GET_POST_DETAILS_FAIL:
state = {
...state,
error: {
message: "Error",
},
loadingPostDetails: false,
};
break;
default:
state = { ...state };
break;
}
return state;
};
export default PostReducer;
Coding the Saga portion
We need to import 3 APIs from redux-saga/effects which are takeLatest, put and call.
You might be confused with some terms here. I can explain them.
- call: Creates an Effect description that instructs the middleware to call the function
fn
withargs
as arguments. - put: Creates an Effect description that instructs the middleware to schedule the dispatching of action to the store.
- Yield: The yield keyword pauses generator function execution and the value of the expression following the yield keyword is returned to the generator’s caller.
- takeLatest: Forks a
saga
on each action dispatched to the Store that matchespattern
. And automatically cancels any previoussaga
task started previously if it’s still running.
Other than the takeLatest, there are APIs such as takeEvery, takeMaybe, etc.
For more, you can refer to the Saga docs for effect creators.
import { takeLatest, put, call } from "redux-saga/effects";
import { GET_POSTS, ... } from "./actionTypes";
import {
getPostsSuccess,
getPostsFail,
...
} from "./actions";
import { getPosts, getPostDetails } from "../../helpers/backend_helper";
function* onGetPosts() {
try {
const response = yield call(getPosts);
yield put(getPostsSuccess(response));
} catch (error) {
yield put(getPostsFail(error.response));
}
}
function* CartSaga() {
yield takeLatest(GET_POSTS, onGetPosts);
...
}
export default CartSaga;
So that the complete saga.js file looks the same as below.
// src/store/posts/saga.js
import { takeLatest, put, call } from "redux-saga/effects";
import { GET_POSTS, GET_POST_DETAILS } from "./actionTypes";
import {
getPostsSuccess,
getPostsFail,
getPostDetailsSuccess,
getPostDetailsFail,
} from "./actions";
import { getPosts, getPostDetails } from "../../helpers/backend_helper";
function* onGetPosts() {
try {
const response = yield call(getPosts);
yield put(getPostsSuccess(response));
} catch (error) {
yield put(getPostsFail(error.response));
}
}
function* onGetPostDetails({ payload: id }) {
try {
const response = yield call(getPostDetails, id);
yield put(getPostDetailsSuccess(response));
} catch (error) {
yield put(getPostDetailsFail(error.response));
}
}
function* CartSaga() {
yield takeLatest(GET_POSTS, onGetPosts);
yield takeLatest(GET_POST_DETAILS, onGetPostDetails);
}
export default CartSaga;
Make a common reducers file and saga file
We have created the reducer and saga for the posts. We have to import these to common reducers and sagas files.
So lets us code the reducers.js file first.
// src/store/reducers.js
import { combineReducers } from "redux";
import PostReducer from "./posts/reducer";
const rootReducer = combineReducers({
PostReducer,
});
export default rootReducer;
Now create sagas.js file as below.
// src/store/sagas.js
import { all, fork } from "redux-saga/effects";
import PostSaga from "./posts/saga";
export default function* rootSaga() {
yield all([fork(PostSaga)]);
}
Setting the helpers
From the previous files, we can see that some files are imported as helpers. Let us take a look at these files.
First, make a file url_helper.js inside the helpers directory. This is just for listing all the paths of our API.
// src/helpers/url_helper.js
//Post
export const GET_POSTS = "/posts";
export const GET_POST_DETAILS = "/posts";
Now let us make a file api_helper.js inside the same helpers directory. Here will will define all the functions to make API calls using the axios package.
// src/helpers/api_helper.js
import axios from "axios";
//apply base url for axios
const REACT_APP_APP_URL = process.env.REACT_APP_APP_URL;
const axiosApi = axios.create({
baseURL: REACT_APP_APP_URL,
});
axios.interceptors.request.use(function (config) {
return config;
});
axiosApi.interceptors.response.use(
(response) => response,
(error) => Promise.reject(error)
);
export async function get(url, config) {
return await axiosApi
.get(url, {
...config,
})
.then((response) => response.data);
}
Finally, we make a file backend_helper.js inside the directory and this is imported to our saga file directly.
If you are good at basic JS, you will understand the logic.
// src/helpers/backend_helper.js
import { get } from "./api_helper";
import * as url from "./url_helper";
//Post
export const getPosts = () => get(url.GET_POSTS);
//Post
export const getPostDetails = (id) =>
get(url.GET_POST_DETAILS, {
params: {
id: id,
},
});
Coding the route components or pages
We have declared two routes / and /:id routes in the App.js file and now the / route returns the Home component and /:id returns the SinglePost component.
Now, let us define these two components. Even though they are components, we are placing them inside the pages directory because, in view, they are pages.
So inside the pages directory, create a Home component.
We will import useDispatch from the react-redux package and getPosts action from the actions file we created.
import { useDispatch } from "react-redux";
import { getPosts } from "../store/posts/actions";
Now we can dispatch the action getPosts just after the component is mounted.
let dispatch = useDispatch();
useEffect(() => {
dispatch(getPosts());
}, []);
The complete Home component will be the same as below.
// src/pages/Home.js
import { Container } from "react-bootstrap";
import Posts from "../components/Posts";
import { useEffect } from "react";
import { useDispatch } from "react-redux";
import { getPosts } from "../store/posts/actions";
export default function Home() {
let dispatch = useDispatch();
useEffect(() => {
dispatch(getPosts());
}, []);
return (
<Container className="home">
<Posts />
</Container>
);
}
In the same manner, we will create a singlePost component. useParams from react-router-dom is used to get the URL parameter values.
import { useParams } from "react-router-dom";
Then we dispatch the function getPostDetails(params.id) if there is any change in params.id.
let params = useParams();
useEffect(() => {
dispatch(getPostDetails(params.id));
}, [params.id]);
The component to show the single post will be the same as below.
// src/pages/SinglePost.js
import { useEffect } from "react";
import { Container } from "react-bootstrap";
import PostDetails from "../components/PostDetails";
import { useDispatch } from "react-redux";
import { getPostDetails } from "../store/posts/actions";
import { useParams } from "react-router-dom";
function SinglePost() {
let params = useParams();
let dispatch = useDispatch();
useEffect(() => {
dispatch(getPostDetails(params.id));
}, [params.id]);
return (
<Container className="single-post">
<PostDetails />
</Container>
);
}
export default SinglePost;
Component to show the posts
To get the posts from the reducer, we are using the useSelector API by react-redux.
import { useSelector } from "react-redux";
import Loader from "react-loader-spinner";
export default function Posts() {
const { posts, loadingPosts } = useSelector((state) => state.PostReducer);
...
}
Posts component will be the same as below.
// src/components/Posts.js
import React from "react";
import { Card, Container, Row, Col } from "react-bootstrap";
import { useSelector } from "react-redux";
import { Link } from "react-router-dom";
import Loader from "react-loader-spinner";
export default function Posts() {
const { posts, loadingPosts } = useSelector((state) => state.PostReducer);
return (
<Container>
{loadingPosts ? (
<div className="loader">
<Loader
type="Bars"
color="#00BFFF"
height={50}
width={100}
timeout={3000} //3 secs
/>
</div>
) : (
posts.map((item) => {
return (
<Row className="posts">
<Col lg={8} md={10} sm={12}>
<Link to={`/${item.id}`}>
<Card>
<Card.Body>
<Card.Title>{item.title}</Card.Title>
<Card.Text>{item.body}</Card.Text>
</Card.Body>
</Card>
</Link>
</Col>
</Row>
);
})
)}
</Container>
);
}
Component to show the single post
In the same method, we access the state post from the reducer.
const { post, loadingPostDetails } = useSelector(
(state) => state.PostReducer
);
The PostDetails component is given below.
// src/components/PostDetails.js
import React from "react";
import { Container, Row, Col } from "react-bootstrap";
import { useSelector } from "react-redux";
import Loader from "react-loader-spinner";
export default function PostDetails() {
const { post, loadingPostDetails } = useSelector(
(state) => state.PostReducer
);
return (
<Container>
{loadingPostDetails ? (
<div className="loader">
<Loader
type="Bars"
color="#00BFFF"
height={50}
width={100}
timeout={3000} //3 secs
/>
</div>
) : (
<Row className="posts">
<Col lg={8} md={10} sm={12}>
<h1>{post.title}</h1>
<div>{post.body}</div>
</Col>
</Row>
)}
</Container>
);
}
Codesandbox
Refer to the CodeSandbox link to view the live app. You can clone this project to your CodeSandbox account and edit the code also.
https://codesandbox.io/s/react-redux-saga-example-blog-bnylq
GitHub
You can always refer to the GitHub repository to clone this project, refer to the code and work on top of it.
https://github.com/techomoro/react-redux-saga-example-blog
Summary
Here we discussed the steps to integrate or use Redux-Saga in a React app by creating a simple blog as an example. We used the exact file structure for building a large-scale react application. Because we commonly use Redux-Saga for a large-scale purpose.
your code have a error in post.js file
posts.map is not a function
Can you please share your Git repository?
There is an error in your code. When I open the codesandbox link, I see the same Type error mentioned above: https://codesandbox.io/s/react-redux-saga-example-blog-bnylq
Hi JB,
I checked your codesanbox, code not wrong, but you can fix this: on file api_helper.js
Fix:
//apply base url for axios
const REACT_APP_APP_URL = “https://jsonplaceholder.typicode.com”;
Because codesanbox can not restart server manual, it not receive data from env file. Server want receive data from env file need off and restart server again.
did u know how to add navigate after some function in saga ?
In my case I am getting error as “Request failed with status code 404 at createAxiosError” and it does not make call to API URL
Please help
Very Help full
good knowledge acquire from this post.