Introduction
As React Native developers, we appreciate Redux for its robust state management capabilities. However, Redux falls short when dealing with asynchronous data, also known as side effects. In this article, we will explore how Redux Saga addresses this shortcoming and demonstrate how to incorporate Redux Saga into our applications. So, let's dive in without further delay.
- Redux Saga: What is it?
- Features of Redux Saga
- Redux Saga Terminology
- Simple Quote App
- Conclusion
These are the topics we will cover in this article. If you want to learn more about Redux, check out my previous article on An Introduction to Redux in React Native.
What is Redux Saga?
Redux-Saga is a middleware library designed for Redux - a widely used state management library in JavaScript applications. Its primary function is to manage the side effects of Redux actions. Side effects include operations that are asynchronous or impure and cannot be managed directly by Redux actions.
Redux-Saga simplifies the management of side effects in complex applications by using generator functions. It offers a range of helper functions to simplify common side-effect tasks, including managing cancellations and making API calls. By using Redux Saga middleware, developers can manage side effects more efficiently and create scalable applications that are easier to maintain and test.
If you want to know more about generator functions, please read the article on ES6 generators in depth.
Features of Redux Saga
Redux Saga is a powerful side effect management middleware for Redux with a wide range of features. Here are some of the key features offered by Redux Saga.
- Asynchronous operations handling: Redux Saga is a library that is built to handle asynchronous operations effectively. It's specifically designed to manage tasks such as making API calls, accessing local storage, or navigating between pages with ease. It provides a structured and organized way to handle these tasks, making it easier to maintain and keep your code clean.
- Side effect management: Redux Saga separates side effects from reducers, making code more testable and understandable.
- Error handling: Redux Saga has built-in error handling capabilities, allowing Sagas to catch logs and dispatch appropriate actions to notify the user or take corrective actions in case of errors.
- Reusability: Redux Saga can be easily reused throughout an application, increasing modularity and maintainability.
- Maintainability: Redux Saga's use of ES6 generators and separation of concerns improves code maintainability and extensibility.
Redux Saga Terminology
If you recall our previous redux article, we explained redux terminology using the story of Krishna and Radha. Now, let's move forward with that story and dive into understanding saga terminology.
- Action: Krishna decides to prepare a salad for dinner and asks Radha to chop the vegetables.
- Effect: Radha takes the vegetables out of the refrigerator (which can be thought of as a Redux store) and begins to chop them. Since chopping is an asynchronous operation, it might take some time to complete.
- Callback: After Radha finishes chopping the vegetables, she calls Krishna to inform him about it. This can be thought of as Radha dispatching an action to Krishna.
- Reducer: Upon receiving the message from Radha that the vegetables have been chopped, Krishna updates his mental state to reflect this. In a Redux architecture, this mental state can be thought of as the store, which holds the current state of the application.
In the above example, the asynchronous operation of chopping the vegetables is handled by Redux Saga. It achieves this by utilizing a generator function, which is a special type of function that can pause and resume execution. This allows Redux Saga to wait for the vegetables to be chopped before updating the Redux store, ensuring that the state of the store is always accurate and up-to-date.
Simple Quote App
Let's create a basic quote application with Redux and Redux Saga. In this app, we will fetch famous quotes from an API and display them in a FlatList. Join me in following these steps to build it.
Step 1. Project Creation
The first step is to create a new project using the following command.
npx react-native init redux_saga_poc
Step 2. Package Installation
First, let's install the necessary packages. To install the packages, run the following command.
Using npm
#necessary packages for redux saga
npm install redux
npm install react-redux
npm install redux-saga
#Installing extra packages to avoid errors in FlatList while scrolling
npm install react-native-virtualized-view
Using yarn
#necessary packages for redux saga
yarn add redux
yarn add react-redux
yarn add redux-saga
#Installing extra packages to avoid errors in FlatList while scrolling
yarn add react-native-virtualized-view
Step 3. Create Files and Folder Structure
As shown in the image, files and project folders should be arranged in the following way.
Step 4. Create Constants
It's time to create action types constants.
//Location: src/utils/constants.js
export const FETCH_QUOTES = 'fetch_quotes';
export const SET_QUOTES_DATA = 'set_quotes_data';
//Quote public API
export const QUOTE_API = 'https://dummyjson.com/quotes';
Step 5. Create Action
Remember the last article's story about Krishna telling his wife to keep the vegetables in the refrigerator? Where Krishna's instruction is an action, In theory, we can say the same thing as.
A Redux action sends data from your application to the store. Normally, an action consists of two properties: its type and its payload (if it's included). The "type" refers to an uppercase string that is assigned to a constant and describes an action. The "payload" is extra data that can be included.
//Location: src/actions/quote_action.js
import {FETCH_QUOTES, SET_QUOTES_DATA} from '../utils/constants';
// Action creator function for initiating the fetch quotes action
export function fetchQuotes() {
return {
type: FETCH_QUOTES,
};
}
// Action creator function for setting quotes data in the Redux store
export function setQuotes(data) {
return {
type: SET_QUOTES_DATA,
data,
};
}
Step 6. Create Saga Middleware
In the story where Krishna asked his wife to chop vegetables for the salad, chopping was an asynchronous operation, which is a side effect. Similarly, in the quote application, fetching quote data is an asynchronous task that is being accomplished by Saga middleware. The question that arises is, What is middleware?
Middleware can be understood as a layer between reducers and dispatch actions. It is called before the action is dispatched. It helps in managing complex logic and handling side effects.
//Location: src/saga.js
import {takeEvery, put} from 'redux-saga/effects';
import {QUOTE_API, FETCH_QUOTES} from './utils/constants';
import {setQuotes} from './actions/quote_action';
// Worker saga function to handle the asynchronous fetching of quotes data
function* getQuotesData() {
const response = yield fetch(QUOTE_API);
// Parsing the JSON data from the API response
const responseData = yield response.json();
// Dispatching the 'setQuotes' action with the retrieved quotes data
yield put(setQuotes(responseData.quotes));
}
// Watcher saga function to listen for the FETCH_QUOTES action and trigger the worker saga
function* sagaData() {
// Using 'takeEvery' to watch for every occurrence of the FETCH_QUOTES action and call 'getQuotesData
yield takeEvery(FETCH_QUOTES, getQuotesData);
}
// Exporting the watcher saga to be run in the Redux Saga middleware
export default sagaData;
The Redux saga provides helper functions for effects, which return an object containing information that tells the Redux middleware to perform additional actions. These effect functions are executed within generator functions.
Here are some useful helpers in the Redux Saga.
- takeEvery(): Every action that is called is executed, and its result is returned.
- takeLastest(): Performing a series of actions will only return the result of the last action.
- take(): Please wait until you are given a specific action before proceeding.
- put(): Initiate an action dispatch.
- call(): Call a function and pause the saga if it returns a promise.
- race(): Run multiple effects at the same time and cancel all of them if one of them ends.
For more information about the Redux Saga helper function, please see the link below.
Redux Middleware API: https://redux-saga.js.org/docs/api/
Step 7. Create Reducer
Remember the last article story, where Radha (Krishna's wife) serves as the Redux reducer. She follows the instructions and puts the vegetables in the refrigerator. Technically speaking, this can be described as follows:
A reducer is a specialized function that has two parameters - state and action. It is immutable, meaning it cannot be modified, and always returns a duplicate of the whole state. Usually, a reducer comprises a switch statement that examines all possible action types.
//Location: src/reducers/quote_reducer.js
import {SET_QUOTES_DATA} from '../utils/constants';
// Initial state for the quotes data in the Redux store
const initialState = [];
export const quoteReducer = (state = initialState, action) => {
switch (action.type) {
// Case for setting quotes data
case SET_QUOTES_DATA:
return {
...state,
quotes: action.data,
};
// Default case for any other action type
default:
return {
...state,
quotes: [],
};
}
};
To proceed, it's time to create a root reducer. Your project may have multiple reducers, but the root reducer will consolidate all of them in one place.
//Location: src/reducers/root_reducer.js
import { combineReducers } from "redux";
import { quoteReducer } from "./quote_reducer";
const rootReducer = combineReducers({
quoteReducer
})
export default rootReducer;
Step 8. Create Store
In the story's previous article, we saw how the refrigerator served as a central hub for storing vegetables. From a technical perspective, we can summarize it as follows:
The Redux application state resides in the store, which is initialized with a reducer.
//Location: src/store.js
import {configureStore} from '@reduxjs/toolkit';
import rootReducer from './reducers/root_reducer';
// Importing Redux Saga middleware and the root saga
import createSagaMiddleWare from 'redux-saga';
import sagaData from './saga';
// Creating an instance of Redux Saga middleware
const sagaMiddleware = createSagaMiddleWare();
// Configuring the Redux store with the root reducer and middleware
const store = configureStore({
reducer: rootReducer,
middleware: () => [sagaMiddleware],
});
sagaMiddleware.run(sagaData);
export default store;
As shown in the code above, we configure the Redux store using the root reducer and middleware. Then, we run the root saga by calling sagaMiddleware.run(sagaData).
Step 9. Create the View (Quote Component)
We dispatch the action using the useDispatch hook and retrieve the data using the useSelector hook. If you're not familiar with these hooks, I suggest reading the article "An Introduction to Redux in React Native". Finally, the data is displayed in a FlatList.
//Location: src/components/Quote.js
import {FlatList, StyleSheet, Text, View} from 'react-native';
import React, {useEffect} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import {fetchQuotes} from '../actions/quote_action';
import {ScrollView} from 'react-native-virtualized-view';
export default function Quote() {
// Accessing quotes data from the Redux store
const quotesData = useSelector(state => state.quoteReducer.quotes);
// Dispatch function to trigger the fetchQuotes action
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchQuotes());
}, []);
return (
<View style={styles.mainContainer}>
<View style={styles.mainSubContainer}>
<Text style={styles.headerTitle}>Quotes - Keep The Fire On</Text>
{quotesData.length > 0 && (
<ScrollView>
<FlatList
keyExtractor={item => item.id}
data={quotesData}
renderItem={({item}) => (
<View style={styles.cardContainer}>
<Text style={styles.quoteTitle}>{item.quote}</Text>
<Text style={styles.quoteAuthor}>{item.author}</Text>
</View>
)}
/>
</ScrollView>
)}
</View>
</View>
);
}
const styles = StyleSheet.create({
mainContainer: {
backgroundColor: '#f1f1f1',
},
mainSubContainer: {
marginHorizontal: 10,
marginVertical: 15,
},
headerTitle: {
fontSize: 24,
fontWeight: 'bold',
color: '#6750A4',
marginBottom: 10,
},
cardContainer: {
backgroundColor: '#f1f1f1',
padding: 10,
margin: 10,
borderRadius: 6,
elevation: 3,
},
quoteTitle: {
fontSize: 18,
color: '#454343',
},
quoteAuthor: {
fontSize: 18,
color: 'black',
fontWeight: 'bold',
textAlign: 'right',
color: '#6750A4',
},
});
Step 10. Create Redux Wrapper
We are almost done. We just need to wrap the react native main component around the redux provider. To do this, we need to create a React Redux provider where we can pass the store and keep the main app component inside the provider.
//Location: index.js
import {AppRegistry} from 'react-native';
import App from './App';
import {name as appName} from './app.json';
import {Provider} from 'react-redux';
import store from './src/store';
const AppRedux = () => (
<Provider store={store}>
<App />
</Provider>
)
AppRegistry.registerComponent(appName, () => AppRedux);
Step 11. Add Quote Component to App.tsx
//Location: App.tsx
import React from 'react';
import Quote from './src/components/Quote';
export default function App() {
return <Quote />;
}
You're all set! Go ahead and run your app to start playing with it.
The source code can be downloaded from the GitHub link provided below.
Quote App With Redux + Redux Saga: https://github.com/socialmad/redux_saga_poc/
Conclusion
Redux is a great state management solution for React Native apps. However, when it comes to handling side effects, like asynchronous API calls, the Redux Saga middleware plays a major role in making it easier. As a React Native developer, it's advisable to add Redux Saga to your project. Additionally, if you're looking to ace a job interview for a React native developer position, brush up on your knowledge of Redux and Redux Saga middleware. Thanks for reading this article! If you enjoyed it, feel free to connect with me on LinkedIn.