React Native + Redux: Implementing Redux Saga For An Asynchronous Flow

By: Jeff Lewis

Jeff Lewis
Level Up Coding

--

Notes:

What Is Redux Saga?

Redux Saga is a library that’s used as a middleware for Redux. A Redux middleware is code that intercepts actions coming into the store via the dispatch() method and can take perform tasks based on the action received. Sagas watch all actions (observer/watcher) that are dispatched from the store. Before the action is passed on to the props, the other function (worker) will process what to do with the action in asynchronous flow.

Redux Saga uses an ES6 feature called Generators, which allows you to write asynchronous code. It’s important to know how the flow of Redux Saga works, specifically what happens with the Sagas (Generator Functions). Generator Functions allow queueing operations using yield, making it asynchronous.

Why Use Redux Saga?

Redux Saga helps Redux with managing application side effects. This makes your code more efficient to execute, easier to test, and allows for better error handling.

1. Better Handling of Actions And Asynchronous Operations

Generators queue asynchronously, which is important when dealing with API requests/responses, communicating to Firebase, or time-sensitive processes.

2. Easier to Read with No Callback Hell

Redux Saga uses generators to queue tasks, which removes the callback hell that comes with Redux-Thunk when using Promises.

3. Better Error Handling

Redux Saga requires 2 Sagas (generator functions) for each piece of state: worker and a watcher. The saga that manages the process is in charge of handling the error, so each piece of the Redux Store State has its own error.

4. Manages Complex Flow

Redux Saga as a middleware allows us to intercept actions before they are passed to the component by mapStateToProps. The Sagas (generator functions) work together to asynchronously execute in a modular style.

5. Declarative Effects

Redux Saga Watchers user Declarative Effects (takeEvery, takeLatest, takeLeading, and etc.), which allow control of the amount of requests. For example, a user is trying to login and is smashing the login button continuously. That would send a request each time a request is made.

We can use the Declarative Effect such as takeLatest to only take the last request.

6. Easy to Test

With the use of ES6 Generators and Declarative Effects (takeEvery, takeLatest, takeLeading, and etc.), building the tests are much easier and cleaner.

Example App + Code

Github Repo: https://github.com/jefelewis/redux-saga-test

A. App Overview

The App will have one reducer: counterReducer. In our store.js file, we will import the root reducer and root saga and integrate them both into our Redux Store with applyMiddleware().

To test if Redux Saga is working in our app, we can press the “-” button (Decrease) as many times as we want and our count will decrease. Our “+” button (Increase) is different because we have an asynchronous delay of 4 seconds. Both of our Increase and Decrease functions are asynchronous due to the Sagas, but the 4 second delay is for demonstration purposes.

Additionally, we are using takeLatest in our watchIncreaseCounter, so only the last action will be used. You can click on the “+” button (Increase) as many times as you want, but the counter will only increase by 1 when 4 seconds have elapsed after the last button click.

B. App Screenshot

Counter.js

C. App File Structure

This example will be using 7 files:

  1. App.js (React Native App)
  2. Counter.js (Counter Screen)
  3. store.js (Redux Store)
  4. index.js (Redux Root Reducer)
  5. counterReducer.js (Redux Counter Reducer)
  6. index.js (Redux Root Saga)
  7. counterSaga (Redux Counter Saga)

D. App Files

App.js

// Imports: Dependencies
import React from 'react';
import { Provider } from 'react-redux';
// Imports: Screens
import Counter from './screens/Counter';
// Imports: Redux Store
import { store } from './store/store';
// React Native App
export default function App() {
return (
// Redux: Global Store
<Provider store={store}>
<Counter />
</Provider>
);
}

Counter.js

// Imports: Dependencies
import React, { Component } from 'react';import { Button, Dimensions, SafeAreaView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { connect } from 'react-redux';
// Screen Dimensions
const { height, width } = Dimensions.get('window');
// Screen: Counter
class Counter extends React.Component {
render() {
return (
<SafeAreaView style={styles.container}>
<Text style={styles.counterTitle}>Counter</Text>
<View style={styles.counterContainer}>
<TouchableOpacity onPress={this.props.reduxIncreaseCounter}>
<Text style={styles.buttonText}>+</Text
</TouchableOpacity>
<Text style={styles.counterText}>{this.props.counter}</Text> <TouchableOpacity onPress={this.props.reduxDecreaseCounter}>
<Text style={styles.buttonText}>-</Text
</TouchableOpacity>
</View>
</SafeAreaView>
)
}
}
// Styles
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
counterContainer: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
},
counterTitle: {
fontFamily: 'System',
fontSize: 32,
fontWeight: '700',
color: '#000',
},
counterText: {
fontFamily: 'System',
fontSize: 36,
fontWeight: '400',
color: '#000',
},
buttonText: {
fontFamily: 'System',
fontSize: 50,
fontWeight: '300',
color: '#007AFF',
marginLeft: 40,
marginRight: 40,
},
});
// Map State To Props (Redux Store Passes State To Component)
const mapStateToProps = (state) => {
console.log('State:');
console.log(state);
// Redux Store --> Component
return {
counter: state.counter.counter,
};
};
// Map Dispatch To Props (Dispatch Actions To Reducers. Reducers Then Modify The Data And Assign It To Your Props)
const mapDispatchToProps = (dispatch) => {
// Action
return {
// Increase Counter
reduxIncreaseCounter: () => dispatch({
type: 'INCREASE_COUNTER',
value: 1,
}),
// Decrease Counter
reduxDecreaseCounter: () => dispatch({
type: 'DECREASE_COUNTER',
value: 1,
}),
};
};
// Exports
export default connect(mapStateToProps, mapDispatchToProps)(Counter);

store.js

// Imports: Dependencies
import { createStore, applyMiddleware } from 'redux';
import { createLogger } from 'redux-logger';
import createSagaMiddleware from 'redux-saga';
// Imports: Redux Root Reducer
import rootReducer from '../reducers/index';
// Imports: Redux Root Saga
import { rootSaga } from '../sagas/index';
// Middleware: Redux Saga
const sagaMiddleware = createSagaMiddleware();
// Redux: Store
const store = createStore(
rootReducer,
applyMiddleware(
sagaMiddleware,
createLogger(),
),
);
// Middleware: Redux Saga
sagaMiddleware.run(rootSaga);
// Exports
export {
store,
}

index.js (Root Reducer)

// Imports: Dependencies
import { combineReducers } from 'redux';
// Imports: Reducers
import counterReducer from './counterReducer';
// Redux: Root Reducer
const rootReducer = combineReducers({
counter: counterReducer,
});
// Exports
export default rootReducer;

counterReducer.js

// Initial State
const initialState = {
counter: 0,
};
// Redux: Counter Reducer
const counterReducer = (state = initialState, action) => {
switch (action.type) {
case 'INCREASE_COUNTER_ASYNC': {
return {
...state,
counter: state.counter + action.value,
};
}
case 'DECREASE_COUNTER': {
return {
...state,
counter: state.counter - action.value,
};
}
default: {
return state;
}
}
};
// Exports
export default counterReducer;

index.js (Root Saga)

// Imports: Dependencies
import { all, fork} from 'redux-saga/effects';
// Imports: Redux Sagas
import { watchIncreaseCounter, watchDecreaseCounter } from './counterSaga';
// Redux Saga: Root Saga
export function* rootSaga () {
yield all([
fork(watchIncreaseCounter),
fork(watchDecreaseCounter),
]);
};

counterSaga.js

// Imports: Dependencies
import { delay, takeEvery, takeLatest, put } from 'redux-saga/effects';
// Worker: Increase Counter Async (Delayed By 4 Seconds)
function* increaseCounterAsync() {
try {
// Delay 4 Seconds
yield delay(4000);
// Dispatch Action To Redux Store
yield put({
type: 'INCREASE_COUNTER_ASYNC',
value: 1,
});
}
catch (error) {
console.log(error);
}
};
// Watcher: Increase Counter Async
export function* watchIncreaseCounter() {
// Take Last Action Only
yield takeLatest('INCREASE_COUNTER', increaseCounterAsync);
};
// Worker: Decrease Counter
function* decreaseCounter() {
try {
// Dispatch Action To Redux Store
yield put({
type: 'DECREASE_COUNTER_ASYNC',
value: 1,
});
}
catch (error) {
console.log(error);
}
};
// Watcher: Decrease Counter
export function* watchDecreaseCounter() {
// Take Last Action Only
yield takeLatest('DECREASE_COUNTER', decreaseCounter);
};

The Saga Continues

And that’s it! Redux Saga is now working in your app and your Redux Store is now asynchronous.

No one’s perfect. If you’ve found any errors, want to suggest enhancements, or expand on a topic, please feel free to send me a message. I will be sure to include any enhancements or correct any issues.

--

--

Full stack React/React Native developer, environmentalist, and beach bum.