Network Layer : Extract your API to a client side library using redux-saga

Luke Brandon Farrell
6 min readJun 18, 2019

Applications mostly consist of requesting, modifying and updating data using a REST API. If you are a GraphQL developer, then it would be good to get your opinion on the patterns I introduce in this article.

In our React-Redux applications, when consuming a REST API, requests can be done in many ways.

  • Components (Hooks)
  • Redux-thunk
  • Redux-saga
  • Other third party library

In this article I am going to introduce the redux-saga network layer pattern for consuming a REST API in your application using redux-saga.

This pattern will allow you to extract your API, normalisation logic, and build complex network features in an isolated package which can be used across all the products in your business.

Sounds good? Here it is…

Pattern

REQUEST… … … … … … … … … … RESPONSE

The pattern involves using redux-saga to execute our API requests from well-named actions. In the following example I demonstrate how this pattern would be applied for fetching all orders for a user in an application. We first need to define an action type for request and response.

const FETCH_USER_ORDERS_REQUEST = "FETCH_USER_ORDERS_REQUEST";
const FETCH_USER_ORDERS_RESPONSE = "FETCH_USER_ORDERS_RESPONSE";

This action type can be dispatched to redux to trigger the API request. The network later will listen for the FETCH_USER_ORDERS_REQUEST action using the redux-saga middleware.

Generator function to listen for the REQUEST

In the example above there is a saga which will fetch the users orders from the API using the call method. It will then dispatch the FETCH_USER_ORDERS_RESPONSE with relevant data.

The pattern involves building your network sagas with a base URL parameter meaning it can be configured in your root saga:

function* rootSaga() {
yield all([
UserOrdersSaga(env.API_URL)
]);
}

Using the base URL allows for two things:

  1. Easily provide different URLs for our sandbox and production environments. e.g. api.sandbox , api.staging
  2. Scale each feature (resource) independently… hint.. hint.. Microservices. e.g. api.sandbox.orders/2.2/ , api.sandbox.todos/1.1

Once you have setup redux-saga and configured your network saga in your root saga you can communicate with your API by dispatching actions.

dispatch({
type: FETCH_USER_ORDERS_REQUEST,
payload: {
filter: "pending"
}
})

You can listen for the response in your reducers. This can be done by consuming the FETCH_USER_ORDERS_RESPONSE which will either contain data from your API or an error, with the key error set to true.

case FETCH_USER_ORDERS_RESPONSE: {
if(!action.error)
return { ...state, orders: action.payload.data }
return { ...state, message: "Something went wrong" };
}

Thats it! Fire your REQUEST and consume your RESPONSES to interact with you API.

Extract

Now that I have shown you the crux of the pattern, I can get into more detail about how it can be extracted from your application using NPM.

At Reward Me Now we use the network layer pattern to share all the API logic across our projects, our package is called the reward-network-layer , and is installed into our projects using NPM.

Here is the structure of the reward-network-layer package:

/types <- all the request and response types for each resource
/network <- API logic for each resource
/sagas <- sagas for each resource
index.js

To fire a request we access the types folder and find the request we want to fire. This will trigger a RESPONSE which will be dispatched to the redux store.

import { MY_CUSTOM_REQUST } from "@redu/reward-network-layer"

When building a library similar to this, it can be useful to create an abstract saga to handle all of the network logic, as much of it will be repetitive. At “Reward Me Now” we have an abstract saga which helps us build resources for the network layer.

Abstract API saga

This saga can be used to easily compose requests for the network layer. e.g.

Improved UserOrdersSaga example

The code in the first gist of this article showcasing the UserOrdersSaga can be reduced to the code above using our abstract saga.

Simple! Now extract your network layer to NPM and use it across your projects.

Scroll down for more advanced implementations…

Big Bonus…

Using this pattern we can build some powerful add-ons which can be used across all our projects. Here are some of the “big bonuses” when using this pattern.

Normalisation

We use normalisation to keep the data from our API deterministic to avoid malformed data being filtered through our application and crashing our components.

What if you could write client-side normalisation logic for your API data once and use everywhere?

Using the pattern I’ve outlined above you can do that! but where?

The code for normalisation should go between a successful response form your API and the dispatch of your RESPONSE action. e.g.

const orders = yield call(fetchOrdersRequest, baseUrl, filter);  // Normalise orders here
const normalisedOrders = _normaliseOrders(orders);
yield put({
type: FETCH_USER_ORDERS_RESPONSE,
payload: { data: normalisedOrders }
});

This will always keep your API data reliable across all your projects. When a change occurs in your API, your network layer package can be updated, and that update can be installed in all the consuming projects.

Timeouts

Using this pattern we can easily add a timeout to all our network requests. The timeout could trigger a fallback UI in your components.

Often it is better to let our request timeout after a few seconds and show a fallback UI which lets the user try the request again, instead of allowing the request to load for minutes and keep the user in the land of uncertainty.

So how do we do it?

We use redux-saga race and delay methods. e.g.

const { orders, timeout } = yield race({
orders: call(fetchOrdersRequest, baseUrl, filter);
timeout: delay(8000) // Timeout of 8 secounds
})
if(timeout !== undefined) {
yield put({
type: FETCH_USER_ORDERS_RESPONSE,
payload: {
timeout: true
}
error: true
});
}
...

If timeout resolves to true, then 8 seconds have elapsed… … … and the request has timed-out. You can include a timeout key in your response payload to update your UI accordingly.

Network

What if we could wait for the network status to become available to send our request?

You guessed it… using this pattern and redux-saga we can achieve that.

let status = yield select(state => network.status);while(status !== "available"){
const action = take(NETWORK_STATUS_CHANGED);
status = action.payload.status;
}
const orders = yield call(fetchOrdersRequest, baseUrl, filter);

The example code uses redux-saga select and take to wait for the network to become available before attempting to fetch the user orders.

In this example we have a network reducer which keeps track of the network state, and a NETWORK_STATUS_CHANGED action which is fired when the network status is changed.

Fallback Data

Wouldn’t it be great if we could make our applications work offline?

This is where the idea of fallback data comes into existence. Using redux-saga and this pattern we can listen for failed requests and determine if we want to load fallback data from storage. The implementation follows a few simple steps:

  1. Wait for request to fail
  2. Check why request has failed
  3. If certain conditions are met then load the fallback data
function* loadUserOrders(action){
if(action.error){
const data = yield select(state => user.orders);
if(data === []){
// Load data here. e.g. async storage, local storage
const fallbackData = yield loadUserOrdersFromAsyncStorage();
yield put({
type: FETCH_USER_ORDERS_RESPONSE,
payload: {
data: fallbackData,
fallback: true
}
});
}
}
}
function* DataSaga() {
yield takeLatest(FETCH_USER_ORDERS_RESPONSE, loadUserOrders);
}

Using the method above we can load previously stored orders from the storage. We only do this when the request has failed and there are no orders in our application; to prevent orders being loaded from storage in the case of a refresh.

This allows the user to view their orders and use the application even if they are offline!

--

--

Luke Brandon Farrell

Luke develops mobile applications using React Native. He writes about component-first architecture and design, code readability, and React Native.