Greetings!
In a previous article, I talked about redux architecture with reactjs a bit. In this article, I talk about handling side effects with redux-saga.
In a previous article, I talked about redux architecture with reactjs a bit. In this article, I talk about handling side effects with redux-saga.
The complete code for this article is here.
For example, this gives my GitHub account information.
In addition to fetching calls, I have added a loader as well to indicate that data is loading. Other than that, this is a typical file.
One special thing to note here is we do not include FETCH_USER_REQUESTED in the reducer. That is because it will be handled as an effect in the saga.
Happy learning guys ☺
https://redux-saga.js.org/docs/introduction/GettingStarted/
https://redux-saga.js.org/docs/basics/DeclarativeEffects
What is redux-saga?
Redux saga is a side effect manager using ES6 generators. Therefore it gives the added benefit of easy testability.What do we build?
To demonstrate the architecture, let's create a simple GitHub user application where we fetch GitHub handle for a given username.For example, this gives my GitHub account information.
https://api.github.com/users/slmanju
Initialize the project
create-react-app react-redux-saga-example
cd react-redux-saga-example
npm install redux react-redux reselect
npm install redux-saga
npm install axios
Util to fetch actual data
Let's create a little util for HTTP calls using axios.import axios from "axios";
const withError = (promise) => promise.then((data) => [null, data]).catch((err) => [err]);
export const getGitHubUser = async (username) => {
return await withError(axios.get(`https://api.github.com/users/${username}`));
};
GitHub components
These are our components to accept the username and show a user.import { useState } from "react";
const GitHubInput = ({ handleSubmit }) => {
const [username, setUsername] = useState("");
const handleChange = (event) => {
setUsername(event.target.value);
};
const onSubmit = (event) => {
event.preventDefault();
setUsername("");
handleSubmit(username);
};
return (
<div>
<form>
<label>Name: </label>
<input
type="text"
name="username"
value={username}
onChange={handleChange}
/>
<button onClick={onSubmit}>Add</button>
</form>
</div>
);
};
export { GitHubInput };
import './GitHub.css';
const GitHubUser = ({ github }) => {
return (
<div className="container">
<div className="img-container">
<img src={github.image} className="img" />
</div>
<div className="data">
<div>{github.name}</div>
<div>{github.bio}</div>
<a href={github.url} target="_blank">{github.username}</a>
</div>
</div>
);
};
export { GitHubUser };
Actions, reducers, and selectors
Let's quickly define these. I assume you have an understanding of these.In addition to fetching calls, I have added a loader as well to indicate that data is loading. Other than that, this is a typical file.
One special thing to note here is we do not include FETCH_USER_REQUESTED in the reducer. That is because it will be handled as an effect in the saga.
import { createSelector } from "reselect";
export const FETCH_USER_REQUESTED = "github/fetch";
export const FETCH_USER_SUCCESS = "github/fetchSuccess";
export const FETCH_USER_FAILED = "github/fetchFailed";
export const LOADING = "github/loading";
export const fetchUser = (username) => {
return {
type: FETCH_USER_REQUESTED,
username,
};
};
export const fetchUserSuccess = (user) => {
return {
type: FETCH_USER_SUCCESS,
user,
};
};
export const fetchUserFailed = () => {
return { type: FETCH_USER_FAILED };
};
const initialState = {
loading: false,
users: [],
};
export const githubReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_USER_SUCCESS:
const { user } = action;
return {
loading: false,
users: [
...state.users,
{
id: user.id,
username: user.login,
name: user.name,
url: user.html_url,
bio: user.bio,
image: user.avatar_url,
},
],
};
case FETCH_USER_FAILED:
return {
...state,
loading: false,
};
case LOADING:
return {
...state,
loading: true,
};
}
return state;
};
const selectGitHub = (state) => state.githubReducer;
export const selectUsers = createSelector(selectGitHub, (state) => state.users);
export const selectLoading = createSelector(
selectGitHub,
(state) => state.loading
);
Configure store
Here is the configuration for the store. We are yet to create the saga configuration though. Saga is applied as a middleware to the store.import {
applyMiddleware,
combineReducers,
legacy_createStore as createStore,
} from "redux";
import createSagaMiddleware from "redux-saga";
import { githubReducer } from "./github";
import githubSaga from "./github-saga";
const sagaMiddleware = createSagaMiddleware();
const rootReducer = combineReducers({ githubReducer });
const store = createStore(rootReducer, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(githubSaga);
export default store;
Provider in index
Now, as usual, we configure the Provider for the store.import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { Provider } from "react-redux";
import store from "./store";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
App
Let's visit the App.jsx as well. There is nothing special in this as well. Note that we are dispatching fetchUser function within this.import { useDispatch, useSelector } from "react-redux";
import "./App.css";
import { GitHubUser } from "./github/GitHubUser";
import { GitHubInput } from "./github/GitHubInput";
import { fetchUser, selectLoading, selectUsers } from "./store/github";
function App() {
const users = useSelector(selectUsers);
const loading = useSelector(selectLoading);
const dispatch = useDispatch();
const onAddUser = (username) => {
dispatch(fetchUser(username));
};
return (
<div className="App">
<h3 className="title">GitHub Users</h3>
<GitHubInput handleSubmit={onAddUser} />
{loading && <div>Loading...</div>}
<hr />
{users.map((user) => (
<GitHubUser github={user} key={user.id} />
))}
</div>
);
}
export default App;
Up
to now, we saw the usual stuff. Nothing special other than configuring saga. It
is time to write the actual saga effect.Saga effect
Now, let's have a look at the saga effect.import { call, put, takeLatest } from "redux-saga/effects";
import { getGitHubUser } from "../utils/github-utils";
import {
fetchUserFailed,
fetchUserSuccess,
FETCH_USER_REQUESTED,
LOADING,
} from "./github";
function* fetchUser(action) {
yield put({ type: LOADING });
const [error, user] = yield call(getGitHubUser, action.username);
if (!error) {
yield put(fetchUserSuccess(user.data));
} else {
yield put(fetchUserFailed());
}
}
function* githubSaga() {
yield takeLatest(FETCH_USER_REQUESTED, fetchUser);
}
export default githubSaga;
There are
a couple of things to pay attention to here. These come with testability in mind
with the support of generators. Instead of direct calls like dispatch or HTTP
calls, this gives us easy unit testability.- call - creates a plain object describing the function call. The redux-saga middleware takes care of executing the function call and resuming the generator with the resolved response
- put - creates a plain JavaScript Object to instruct the middleware that we need to dispatch some action, and let the middleware perform the real dispatch
- takeLatest - starts a new saga task in the background. automatically cancels any previous saga task started previously if it's still running
Happy learning guys ☺
References
https://redux-saga.js.org/https://redux-saga.js.org/docs/introduction/GettingStarted/
https://redux-saga.js.org/docs/basics/DeclarativeEffects
Comments
Post a Comment