DEV Community

Konrad Lisiczyński
Konrad Lisiczyński

Posted on

Taming network with redux-requests, part 1 - Introduction

The problem

Let's face it. Even in the 21st century, making AJAX requests and management of remote state is still surprisingly hard! It looks easy at the beginning, but the more experienced you become, the more aware you are about problems you didn't consider before. Race conditions, requests aborts, caching, optimistic updates, error handling, showing spinners for loading state per request, server side rendering... This list could go on... Network is just unpredictable and we really need something... predictable to counter that!

The solution

The most predictable and battle-tested state management system I could think of is Redux. Really, you might say? Redux belongs to the past, Redux is verbose, you might not need Redux...

In my opinion those statements are not justified and Redux is a perfect candidate for a building block to create abstractions to solve all real-live networking problems in your apps. It is very stable, has very big community, solid addons and... it is not as verbose as many people say. For me Redux is not just a library, it is just a perfect architecture to build on and has a minimalistic API, you write just functions basically, so you can just use Javascript, not a framework!

So, after this short introduction, let's prove that Redux is still a valid tool and shouldn't be forgotten just yet and tame the dreaded network!

Making AJAX requests with Redux

As a start, let's pretend we wanna make an AJAX call to fetch books. We will use very popular axios library for that. So, to fetch books, we could do this:

import axios from 'axios';

const fetchBooks = () => {
  return axios.get('/books');
}

How to convert it to Redux? Most of the time people use redux-saga, redux-thunk or a custom middleware. Let's use thunk to keep things simple:

const fetchBooks = () => dispatch => {
  dispatch({ type: 'FETCH_BOOKS' });

  return axios.get('/books').then(response => {
    return dispatch({
      type: 'FETCH_BOOKS_SUCCESS',
      response,
    });
  }).catch(error => {
    return dispatch({
      type: 'FETCH_BOOKS_ERROR',
      error,
    });
  });
}

So what we did here? We just created the fetchBooks thunk which dispatches FETCH_BOOKS action, makes AJAX request and then, depending on the outcome dispatches FETCH_BOOKS_SUCCESS or FETCH_BOOKS_ERROR action. To use it, we could just:

store.dispatch(fetchBooks());

But you might think, wow! That's indeed very verbose, you just proved that Redux belongs to the past! Don't be so fast though, we will make it super short by creating nice abstractions later!

Adding reducer to store books state

Let's write a reducer which will listen to above actions and handle books state:

const initialState = {
  data: null,
  pending: 0, // for loading state
  error: null,
};

const booksReducer = (state = initialState, action) => {
  switch (action.type) {
    case 'FETCH_BOOKS':
      return { ...state, pending: state.pending + 1 };
    case 'FETCH_BOOKS_SUCCESS':
      return {
        data: action.response.data,
        pending: state.pending - 1,
        error: null,
      },
    case 'FETCH_BOOKS_ERROR':
      return {
        data: null,
        pending: state.pending - 1,
        error: action.error,
      },
    default:
      return state;
}

Above is self-explanatory, we just increment pending counter on request and decrement on success or error, plus we save data and error depending on the response type.

Why loading state as a counter not boolean flag by the way? Well, to handle parallel requests. Imagine a sequence: FETCH_BOOKS, FETCH_BOOKS, FETCH_BOOKS_SUCCESS, FETCH_BOOKS_SUCCESS. After the first FETCH_BOOKS_SUCCESS loading state would be set to false, which would be not correct as we still would have another pending requests. Counter solution won't fall to this trick, as pending would be 1 so you would know that books are still fetching.

Anyway, again, very verbose! All this code just to make one AJAX requests and to fetch books and store them in reducer. That's crazy! In my apps I could have dozens of such endpoints and connecting to them like that would be a nightmare! Well, you are right, but let's abstract it for the rescue! Remember? Those are just functions and we can use normal programming techniques to make them reusable, without any special API!

Abstracting thunk and reducer

Let's start with thunk:

const createRequestThunk = (
  type,
  axiosConfig,
) => () => dispatch => {
  dispatch({ type });

  return axios(axiosConfig).then(response => {
    return dispatch({
      type: `${type}_SUCCESS`,
      response,
    });
  }).catch(error => {
    return dispatch({
      type: `${type}_ERROR`,
      error,
    });
  });
}

So, we refactored fetchBooks thunk into reusable createRequestThunk creator. Now creating fetchBooks is as easy as:

const fetchBooks = createRequestThunk(
  'FETCH_BOOKS',
  { url: '/books' },
);

As you can see, integrating with new endpoints is super simple. Let's do the similar thing to reducer:

const initialState = {
  data: null,
  pending: 0, // for loading state
  error: null,
};

const createRequestReducer = type => (
  state = initialState,
  action,
) => {
  switch (action.type) {
    case type:
      return { ...state, pending: state.pending + 1 };
    case `${type}_SUCCESS`:
      return {
        data: action.response.data,
        pending: state.pending - 1,
        error: null,
      },
    case `${type}_ERROR`:
      return {
        data: null,
        pending: state.pending - 1,
        error: action.error,
      },
    default:
      return state;
}

Again, we just refactored booksReducer into a reusable createRequestReducer, which can be used like that:

const booksReducer = createRequestReducer('FETCH_BOOKS');

Not as verbose as people say after all, isn't it? In my opinion myths about Redux verbosity was taken due to examples from official docs, many basic examples and so on. Functions are the easiest to abstract and reuse, especially in comparison to classes which have internal state which is always problematic.

Ok, but does it answer all problems we mentioned at the beginning? We just made it fast to create thunks and reducers, but what about race conditions and other things? Above examples just proved that Redux is nice for making abstractions. Before I answer, I will ask another question, should we really be worried about those on the app level? We should be worried only about writing business logic and network problems should be solved on a library level. That's why I created redux-requests.

Introducing redux-requests

Declarative AJAX requests and automatic network state management for Redux

Redux-Requests showcase

With redux-requests, assuming you use axios you could refactor a code in the following way:

  import axios from 'axios';
- import thunk from 'redux-thunk';
+ import { handleRequests } from '@redux-requests/core';
+ import { createDriver } from '@redux-requests/axios'; // or another driver


  const FETCH_BOOKS = 'FETCH_BOOKS';
- const FETCH_BOOKS_SUCCESS = 'FETCH_BOOKS_SUCCESS';
- const FETCH_BOOKS_ERROR = 'FETCH_BOOKS_ERROR';
-
- const fetchBooksRequest = () => ({ type: FETCH_BOOKS });
- const fetchBooksSuccess = data => ({ type: FETCH_BOOKS_SUCCESS, data });
- const fetchBooksError = error => ({ type: FETCH_BOOKS_ERROR, error });

- const fetchBooks = () => dispatch => {
-   dispatch(fetchBooksRequest());
-
-   return axios.get('/books').then(response => {
-     dispatch(fetchBooksSuccess(response.data));
-     return response;
-   }).catch(error => {
-     dispatch(fetchBooksError(error));
-     throw error;
-   });
- }

+ const fetchBooks = () => ({
+   type: FETCH_BOOKS,
+   request: {
+     url: '/books',
+     // you can put here other Axios config attributes, like method, data, headers etc.
+   },
+ });

- const defaultState = {
-   data: null,
-   pending: 0, // number of pending FETCH_BOOKS requests
-   error: null,
- };
-
- const booksReducer = (state = defaultState, action) => {
-   switch (action.type) {
-     case FETCH_BOOKS:
-       return { ...defaultState, pending: state.pending + 1 };
-     case FETCH_BOOKS_SUCCESS:
-       return { ...defaultState, data: action.data, pending: state.pending - 1 };
-     case FETCH_BOOKS_ERROR:
-       return { ...defaultState, error: action.error, pending: state.pending - 1 };
-     default:
-       return state;
-   }
- };

  const configureStore = () => {
+   const { requestsReducer, requestsMiddleware } = handleRequests({
+     driver: createDriver(axios),
+   });
+
    const reducers = combineReducers({
-     books: booksReducer,
+     requests: requestsReducer,
    });

    const store = createStore(
      reducers,
-     applyMiddleware(thunk),
+     applyMiddleware(...requestsMiddleware),
    );

    return store;
  };

Above diff shows some similarities to abstractions we made before, but the approach is a little different, we don't use thunks and we have just one global reducer.

Anyway, as you can see, with redux-requests, you no longer need to define error and success actions to do things like error handling or showing loading spinners. You don't need to write requests related repetitive sagas and reducers either. You don't even need to worry about writing selectors, as this library provides optimized selectors out of the box. With action helper library like redux-smart-actions, you don't even need to write constants! So basically you end up writing just actions to manage your whole remote state, so no more famous boilerplate in your Redux apps!

redux-requests features

Just actions

Just dispatch actions and enjoy automatic AJAX requests and network state management

First class aborts support

Automatic and configurable requests aborts, which increases performance
and prevents race condition bugs before they even happen

Drivers driven

Compatible with anything for server communication. Axios, Fetch API,
GraphQL, promise libraries, mocking? No problem! You can also integrate
it with other ways by writing a custom driver!

Batch requests

Define multiple requests in single action

Optimistic updates

Update remote data before receiving server response to improve perceived performance

Cache

Cache server response forever or for a defined time period to decrease
amount of network calls

Data normalisation

Use automatic data normalisation in GraphQL Apollo fashion, but for anything, including REST!

Server side rendering

Configure SSR totally on Redux level and write truly universal code
between client and server

React bindings

Use react bindings to decrease code amount with React even more

What's next?

This is just the beginning of tutorial series. In this part we showed that Redux can be still a valid tool and introduced redux-requests library. In the next parts we will mention many problems we encounter when writing apps connecting with APIs and how we could solve them with the help of redux-requests, so stay tuned!

In the part 2, we will start with basic usage of redux-requests.

Top comments (0)