Redux Saga Pattern for React

Building a React app can be difficult. Data is being shared among many components and having a lot of different states can make it very complex and hard to maintain. To handle that we usually use Redux - state management tool that allows keeping application state consistent and predictable. The state of the application is kept in a single global store, rather than at the component level, and each component can access it wherever it needs. Here I will show you a Redux middleware used to handle side effect - Redux-Saga - a library that is based on generator functions and allows for synchronously written asynchronous code.

Generators in ES6 Javascript

Generator functions are a new way of representing iterators introduced in ES6. It is a function that can be stopped and executed again, saving its context. Its different behavior can be presented by comparing to regular Javascript functions. The regular function will execute all instructions in its body every time it is invoked. In the code below a function sayHello() will return a string value hello. Every time we run this function we will receive this result. If its body will contain more instructions all of them will be executed one by one until a return statement is reached. The function can have multiple return statements, but when it is reached it will stop execution and return a value to the caller.

function sayHello() {
return 'hello';
}
sayHello();
// 'hello'

Generators work differently. It is a function that will return a Generator object when called. If it is assigned to a variable you can call a next() method to get an object that contains a value from the next stopping point and done flag to show the status of the function. Generator function uses a different syntax than a regular function. You define it using either function* X or function *X, both mean the same.

function* sayHi() {
yield 'hi';
}
const hiGen = sayHi();
hiGen.next();
// { value: 'hi', done: false }

Instead of return statement that will stop the generator, you use yield command for stopping point. An example below will show the difference in its behaviour.

function* gen() {
yield 'tommy';
yield 'tommy2';
return 'tommy3';
yield 'tommy4';
}
const g = gen();
g.next();
// {value: "tommy", done: false}
g.next();
// {value: "tommy2", done: false}
g.next();
// {value: "tommy3", done: true}
g.next();
// {value: undefined, done: true}

You can also easily iterate over a Generator:

const gen = function* () {
yield 'tommy';
yield 'tommy2';
yield 'tommy3';
yield 'tommy4';
}
const iterator = gen();

for (let val of iterator) {
console.log(val);
}

The code I presented above use a for ... of loop. In this case, calls to next () are controlled by the loop. They are available through it "on the fly": the loop itself controls the occurrence of the true value of done property, and for each iteration, it returns the next value as val variable.

redux-saga and configuration

Before starting on Redux-Saga I will explain what Saga is. It is a design pattern similar to Transactions, used to orchestrate many events that may occur in a distributed system in an undefined order. The time between events can be relatively long, so you should persuade its state while waiting for the next event. I found also that Saga is sometimes called Persistent Multi-Listener. It means that is is an object with a persistent state that can listen to many events.

redux-saga is a middleware for Redux which alows using ES6 generators for asynchronous calls. You can find more information on library page

npm install --save-dev redux-saga

After installing redux-saga middleware we need to import createSagaMiddleware from it to store.ts file.

The rest of the code is pretty standard configuration - we call the createSagaMiddleware function, and its result is passed as a parameter called by the applyMiddleware function. It is important to assign a result to a variable. This is the object that we use at the end. As you can see, we pass the rootSaga function to it's run() function. A rootSaga is an object that combines all of our sagas, in this example I just import it from ./sagas/search. Here is the code for this file:

import createSagaMiddleware from 'redux-saga';
import rootSaga from './sagas/search';

export function makeStore(): Store<Action> {
const sagaMiddleware = createSagaMiddleware();
const store: Store = composeWithDevTools(applyMiddleware(sagaMiddleware))(createStore)(
rootReducer,
initialState
);
sagaMiddleware.run(rootSaga);

return store;
}

Example application

To show a practical example I created an app for searching movies. It is very simple. You have input where you can type a movie name and a search button. It will send a request to OMDB API and you will receive an array of movies in the response. To handle that I created four actions - search(),searchRequestStarted(), updateMoviesState(), showError(). First one is used to send a searchQuery parameter to API. Next searchRequestStarted is called from saga just before API request, it is used to set isLoading flag to true so it can be used for spinner. updateMoviesState will update movies state in Redux store with received results. If anything bad happens a showError action will be fired to show an Error message to the user.

export const search = (searchQuery: string): ISearchAction => {
return {
type: SEARCH,
payload: { searchQuery }
};
};
export const searchRequestStarted = (): ISearchRequestStartedAction => {
return {
type: SEARCH_REQUEST_STARTED
};
}

export const updateMoviesState = (data: IMovie[]): IUpdateMoviesStateAction => {
return {
type: UPDATE_MOVIES_STATE,
payload: { data }
};
};
export const showError = (errorMessage: string): IShowErrorAction => {
return {
type: SHOW_ERROR,
payload: { errorMessage }
};
};

Actions are handled in the reducer presented below. When a search is started an isLoading flag is changed so we know that request is in progress. When it is finished we changed it back to the false state. When a request was successful we will update movies state with received data, if not - an error message will be stored in movies state and data will be cleared so previous results will disappear from the screen. One can ask why SEARCH_REQUEST_STARTED is needed if we can react on SEARCH that initiates the process. The order here is very important, the saga is run before reducer, so when you start a search process you should dispatch first an action that will be used as a notification. If isLoading was changed in SEARCH action it will be changed after the request is done, so too late. Depending on the search request result we have two actions to update the state with results or with an error message. This could be done in one action, using SEARCH action only, but I like to do it this way as it is cleared code to me.

export default function movies(
state: IMoviesState = {} as IMoviesState,
action: IMoviesAction

): IMoviesState {
return produce(state, draft => {
switch (action.type) {
case SEARCH_REQUEST_STARTED: {
draft.isLoading = true;
draft.errorMessage = '';
return;
}
case UPDATE_MOVIES_STATE: {
draft.data = action.payload.data;
draft.isLoading = false;
return;
}
case SHOW_ERROR: {
draft.data = [];
draft.isLoading = false;
draft.errorMessage = action.payload.errorMessage;
return;
}
}
});
}

The most important part here is a code from the saga. This file is full of functions provided by the library, but I will explain all of them. We have two types of sagas - watcher saga and worker saga. Watcher saga will listen to dispatched action and then it will run a worker an associated saga. At the very bottom, we export a rootSaga generator function. This is the one imported in store.ts. It uses all and fork effect to create an array of watcher saga with non-blocking calls. watchSearchRequest is a watcher saga using takeEvery effect. It listens to SEARCH action and every time this action occurs it will pause itself and let the doSearchRequest worker saga do the rest. There are a few different effects that can be used here, for example, takeLatest will run worker saga only for the latest action occurrence. The worker saga in the very beginning will call a function using call effect. The function is defined below and it is a simple API request made using axios. When we receive a response it will be used to update the movies state. For this purpose, we will dispatch an action from worker saga using put effect. When anything bad happens an error will be caught and an action showError will be dispatched and an error message will be sent to the store. This is a simple solution based on generators to handle an asynchronous API call. On every search action a searched text - searchQuery parameter - will be used to call omdbapi and response from omdb server will be next used to update redux state.

import { takeEvery, put, fork, call, all} from 'redux-saga/effects';

function* doSearchRequest(action: ISearchAction): Generator {
try {
const response = yield call(searchMovies, action.payload.searchQuery);
yield put(updateMoviesState((response as IResponse).data.Search));
} catch (e) {
yield put(showError(e.message));
}
}

function* watchSearchRequest(): SagaIterator {
yield takeEvery(SEARCH, doSearchRequest);
}

const searchMovies = (searchQuery: string): AxiosPromise => {
return axios.request({
url: 'https://omdbapi.com/?apikey=yourapikey&s=' + searchQuery,
method: 'GET'
});
};

export default function* rootSaga(): SagaIterator {
yield all([fork(watchSearchRequest)]);
}

To start a search I added a small form in Search component, which will dispatch searchRequestStarted, search action on submit event. First one will change a isLoading flag so we know action is in progress, and second will start a search process.

const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
dispatch(searchRequestStarted());
dispatch(search(searchTerm));
};

The Movies component takes data from movies state and when state is updated it will display error message if any exists, or it maps data to display Movie components otherwise.

	const mapState = useCallback(
(state: IStore) => ({
data: state.movies.data,
errorMessage: state.movies.errorMessage
}),
[]
);
const {
data,
errorMessage
}: {
data: IMovie[];
errorMessage: string | undefined;
} = useMappedState(mapState);

...

{errorMessage ? (
<div className="alert alert-danger">{errorMessage || 'something went wrong'}</div>
) : (
<div className="container pull-down">
{data.map((movie: IMovie, i: number) => (
<Movie movie={movie} key={i} />
))}
</div>
)}

This is a very short and simple explanation of how a Redux-Saga pattern can be used to make asynchronous API calls using redux-saga library. This is of course introduction and the library provides a lot of effects that can be used to orchestrate many events and create complicated transactions. If you are interested in full source code, it is available on my github page

© 2022 Tommy Bernaciak. All rights reserved.