React Redux-Saga with Hooks
Setting up React Redux-Saga using Hooks in 13 steps.
Intro
The aim of this tutorial/blog is to introduce redux and redux-sagas with a brief — easy to digest explanation. I think where a lot of React tutorials fall short is when the writer stops using React as it was intended and gives the reader a bunch of code that lives across one or two folders. It makes the tutorial easier to write but harder for the reader to understand the process of how it may work in the real world. Given this I will also do my best to follow a typical React pattern / folder structure throughout.
I will be using React Hooks in this tutorial given most current/future React applications will be written using Hooks, however I have created two complete repositories of this tutorial one using hooks and the other using class components.
You can find two different complete repositories of this tutorial here:
We will be creating the golden application of all online coding tutorials — a counter application. First, we will create an empty React application using create-react-app, then we will introduce React Redux to manage our state and then finally adding Redux-Sagas to allow us to make asynchronous HTTP requests to an external API (although in this example we will just use a delay to mock this) and save them to our Redux store.
Libraries
I promised to keep this brief so I’ll assume you have some React knowledge already, but I will touch on what the three main libraries we’ll be using are (Outside of React).
- Is a predictable state container for JavaScript applications
- It helps developers create applications that behave consistently by creating a global store(state) within an application.
- This library provides the bindings to use with Redux, it gives us the provider that puts the store into React.
- A middleware library that allows our Redux store to interact with resources outside of itself asynchronously.
Simply we can use Redux combined with Saga’s to request data from an external API, return the response, and dispatch it to our reducer which will then update our Redux store. Vola tutorial done, see you next time.
In all seriousness, this is a brief flow diagram that may help you visually understand the flow I just explained. Throughout this tutorial I will update it to add some additional context.
Many developers follow a specific folder structure that separates the different parts of Redux/Redux-Saga’s from one another, so let’s begin by creating our React app, setting up our folder structure and installing our dependencies.
“npx create-react-app redux-flow-counter-example”“cd redux-flow-counter-example/src”“mkdir actions reducers components sagas”“npm install redux react-redux redux-saga”
You may clear out all the default create-react-app files if you’d like!
Step 1. Creating the UI component
src/components/Counter.js
Building our UI — this needs little explanation; we’ll be back to make some updates later!
import React from "react";
const CounterComponent = () => {
return (
<div>
<h1>Counter - Redux Saga Flow Example</h1>
<button>
Increment + 1
</button>
<button
Decrement - 1
</button>
<div>Count: 0</ div>}
</div>
);
};
export default CounterComponent;
Step 2. Creating action types
src/actions/actionTypes.js
export const INCREMENT_REQUEST = "INCREMENT_REQUEST"
export const DECREMENT_REQUEST = "DECREMENT_REQUEST"
Action Types are dispatched from our actions to our reducer (Don’t worry, we’ll be building these soon!) to change the state of the store. It’s important to give them clear names of what they’re going to do.
Step 3. Creating actions
src/actions/index.js
import { INCREMENT_REQUEST, DECREMENT_REQUEST } from "./actionTypes";export const incrementAction = (step) => {
return {
type: INCREMENT_REQUEST,
payload: { step: step },
};
};export const decrementAction = (step) => {
return {
type: DECREMENT_REQUEST,
payload: { step: step },
};
};
Actions also known as action creators are functions that return an action object. These objects must firstly always include a type attribute which defines what action type it belongs to the follow field(s) will hold additional information about what happened by convention this is called the payload. It’s also important to remember that these functions do nothing on their own and do nothing to update the store they must be dispatched.
As you can see above, we’re creating two actions and their types are being imported from our actionTypes.js file. We’re also passing “step” into these functions and adding it as our payload in our application “step” will represent the number for our counter to increase by.
Step 4. Dispatch our new actions from the UI
src/components/CounterComponent.js
import React from "react";
import { useDispatch } from "react-redux";
import { decrementAction, incrementAction } from "../actions";
const CounterComponent = ({dispatch }) => {
const dispatch = useDispatch();return (
<div>
<h1>Counter - Redux Saga Flow Example</h1>
<button onClick={() => dispatch(incrementAction(1))}>
Increment + 1
</button>
<button onClick={() => dispatch(decrementAction(1))}>
Decrement - 1
</button>
<div>Count: 0 </div>
</div>
);
};
export default CounterComponent;
Now we can return to our UI component and update the onClick handlers with dispatch functions. These dispatch functions will call the actions that we created in our last step. While we’re here it’s important to know that Dispatch is a function of the Redux store it is the only way to trigger a state change.
This is a visual representation of what we’ve added in the previous steps, we have a UI component which functions call a dispatch to call an action.
Step 5. Create the reducer
src/reducers/counterReducers.js
import {
INCREMENT_REQUEST,
DECREMENT_REQUEST,
} from "../actions/actionTypes";
const initialState = {
value: 0,
loading: null,
error: null,
};
const counterReducers = (state = initialState, action) => {
switch (action.type) {
case INCREMENT_REQUEST:
return {
...state,
value: state.value + 1,
loading: false,
error: null,
};
case DECREMENT_REQUEST:
return {
...state,
value: state.value - 1,
loading: false,
error: null,
};
default:
return state;
}
};
export default counterReducers;
Reducers are functions that make changes to the state of our application when an action is called. They include a switch state inside ours we’re going to create 3 different cases for now: INCREMENT_REQUEST, DECREMENT_REQUEST and default.
Looking at the objects returned from each of these switch statements you’ll be able to see a brand new object is created from each. Using a spread operator a new object is created with a copy of the previous state and then the value attribute is overridden with the new state value. The default case covers if an unknown action is called, then it should return the existing state so our application doesn’t break.
Step 6. useSelector and accessing global state
src/CounterComponent.js
import React from "react";
import { decrementAction, incrementAction } from "../actions";
import { useSelector, useDispatch } from "react-redux";const CounterComponent = ({dispatch}) => {
const counter = useSelector((state) => state.counterReducers);
const dispatch = useDispatch();return (
<div>
<h1>Counter - Redux Saga Flow Example</h1>
<button onClick={() => dispatch(incrementAction(1))}>
Increment + 1
</button>
<button onClick={() => dispatch(decrementAction(1))}>
Decrement - 1
</button>
<div>Count: {counter.value}</div>
</div>
);
};
export default CounterComponent;We’re almost done!
Back in our component we can now add a useSelector function to include the state.
Now we have access to our global state we can update our UI component to make use of it. We named our state “counter” so we will add “counter.value” to display this to the user.
Step 7. Combine multiple reducers
src/reducers/index.js
import { combineReducers } from "redux";
import counterReducers from "./counterReducers";
const allReducers = combineReducers({
counterReducers,
});
export default allReducers;
This isn’t essential as we’re only using a single reducer in this example, but redux does provide us a function to combine all of our reducers into one in preparation to pass it to our store. This can be much neater in large scale redux projects.
Step 8. Create store within the root index of the React App
src/index.js
import React from "react";
import ReactDOM from "react-dom";
import { createStore } from "redux";
import { Provider } from "react-redux";
import CounterComponent from "./components/CounterComponent";
import allReducers from "./reducers";
let store = createStore(allReducers);
const App = (
<Provider store={store}>
<CounterComponent />
</Provider>
);
ReactDOM.render(App, document.getElementById("root"));
This is a visual example of what we’re doing in this step:
It’s time to complete our redux setup. The above image gives us a good idea about what we’re going to achieve here. We’ve already created our combined reducers, now we’re going to create a store that holds them and then wrap our component in a provider that will provide our store globally to our application.
Looking at the code snippet we import the createStore function from the redux library and pass it our reducers. Now we can import the Provider wrapper from the react-redux binding library and wrap our component with it.
Don’t forget to pass down the store we’ve created! But we have now completed the redux part of this tutorial.
Before we go onto Redux-Saga’s — this is what we’ve achieved so far. Hopefully you’re able to have a solid understanding as we’ve viewed each part of the diagram as we’ve completed it.
Step 9. Create counterSagas
src/sagas/counterSagas.js
Strap in — this is probably going to be our most wordy step but it’s what we came for — lets make some sagas!
But before we begin lets go over generator functions, yield and our redux-saga imports — generator functions (function* ()) allow us to write functions that produce a sequence of results instead of a single value. The Yield keyword is used to pause and resume generator functions at each step of the process it is the generator function version of return.
Here we’ve got 3 new imports form the redux-saga library these are functions but they’re also known as effect creators.
- takeEvery
- Allows multiple fetch instances to be started concurrently
- In our application this function will watch for every instance of INCREMENT_REQUEST and return the result
- call
- Using call will block our generator function from continuing until a resolved response has been returned
- In our application this means that the next step of our async functions will not proceed until the delay function has been completed
- put
- Unlike call put will not block and once the previous yield function has been completed it will dispatch an action to the reducer
- In our application this means after the delay function completes an action will be dispatched and our store will be updated
For the purpose of our example application, we’re going to create a delay function which will mimic a delayed response from an API and save us some extra work.
Next, we’re going to create our increment and decrement generator functions. We could also refer to these as our worker sagas — from what we discussed above we know that yield call will prevent us from progressing until a response is returned. The worker sagas are where some side effect occurs in our case a 2 second delay.
How are these going to be called?! We also need to create some watcher sagas, by using takeEvery these functions will watch for the action passed in as the first parameter and then trigger the worker saga we’ve passed in as the second parameter.
When a watcher saga is called and the process is beginning the action will also call the reducer and this is where a different piece of state to indicate the saga has begun its process may exist for example loading, fetching etc.
10. Add new cases to the reducer and actionTypes
src/actions/actionTypes.js
export const INCREMENT_REQUEST = "INCREMENT_REQUEST";
export const INCREMENT_SUCCESS = "INCREMENT_SUCCESS";
export const INCREMENT_FAILURE = "INCREMENT_FAILURE";
export const DECREMENT_REQUEST = "DECREMENT_REQUEST";
export const DECREMENT_SUCCESS = "DECREMENT_SUCCESS";
export const DECREMENT_FAILURE = "DECREMENT_FAILURE";
src/reducers/counterReducers.js
import {
INCREMENT_REQUEST,
INCREMENT_SUCCESS,
INCREMENT_FAILURE,
DECREMENT_REQUEST,
DECREMENT_SUCCESS,
DECREMENT_FAILURE,
} from "../actions/actionTypes";
const initialState = {
value: 0,
loading: null,
error: null,
};
const counterReducers = (state = initialState, action) => {
switch (action.type) {
case INCREMENT_REQUEST:
return { ...state, loading: true, error: null };
case INCREMENT_SUCCESS:
return {
...state,
value: state.value + action.payload.step,
loading: false,
error: null,
};
case INCREMENT_FAILURE:
return { ...state, error: action.error };
case DECREMENT_REQUEST:
return { ...state, loading: true, error: null };
case DECREMENT_SUCCESS:
return {
...state,
value: state.value – action.payload.step,
loading: false,
error: null,
};
case DECREMENT_FAILURE:
return { ...state, error: action.error };
default:
return state;
}
};
export default counterReducers;
Now that we’re using saga’s we need to add some new actionTypes as well as some new cases inside of our reducer. As we discussed in the previous step.
11. Create a rootSaga
src/sagas/rootSaga.js
import { all } from "redux-saga/effects";
import { watchIncrement, watchDecrement } from "./counterSagas";
export default function* rootSaga() {
yield all([watchIncrement(), watchDecrement()]);
}
Much like when we combine our reducers, we’re going to combine our sagas into one place that is known as a “rootSaga”. This saga (yes rootSaga is going to be a saga of sagas!) will be passed to our store. Although the rest of our application remains unaware of these Saga’s the middleware will intercept our redux-actions, run our saga before completing the action.
Once again, we’re importing another function from the redux-saga library. This function tells the saga to run all the other sagas that have been passed to it concurrently and wait for them all to complete.
Step 12. Add loading state to the counter
src/components/CounterComponent.js
Since we’re using a time delay in place of a real API call lets display a loading screen to the user (We added this value to our state object in step 10). As before we will access our global state and our reducer will return the most current information for us to display to the end user.
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { decrementAction, incrementAction } from "../actions";const CounterComponent = () => {
const counter = useSelector((state) => state.counterReducers);
const dispatch = useDispatch();return (
<div>
<h1>Counter - Redux Saga Flow Example</h1>
<button onClick={() => dispatch(incrementAction(1))}>
Increment + 1
</button>
<button onClick={() => dispatch(decrementAction(1))}>
Decrement - 1
</button>
{counter.loading ? <div>loading</div> : <div>Count: {counter.value}</div>}
</div>
);
};export default CounterComponent;
Step 13. Create Saga middleware and update store
src/index.js
import React from "react";
import ReactDOM from "react-dom";
// Redux
import { createStore, applyMiddleware } from "redux";
import { Provider } from "react-redux";
// Redux saga
import createSagaMiddleware from "@redux-saga/core";
import CounterComponent from "./components/CounterComponent";
import allReducers from "./reducers";
import rootSaga from "./sagas/rootSaga";
// Middleware
const sagaMiddleware = createSagaMiddleware();
let store = createStore(allReducers, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(rootSaga);
ReactDOM.render(
<Provider store={store}>
<CounterComponent />
</Provider>,
document.getElementById("root")
);
We must share our saga’s to our application by creating the saga middleware imported from its own library and pass it to our store.
We should now be able to boot up our application where we’ll be able to increase and decrease our counter value (our global state) with a slight time delay.
Conclusion
Congratulations! We’ve now created a basic counter app that uses both redux and redux-saga. If this wasn’t as brief as you expected — I hope it was at least informative.
Before I let you go, let’s take one last look at our flow diagram one more time to see where Redux Saga’s fit into our process. You can see a new box has been added between action and reducers. When an action is called from the UI or a user one of our watcher sagas will pick it up and alert our worker saga. Our worker saga will then send a request off to an external api (or in our case our delay function) upon its return it will be passed back to our reducer which will update or store.