DEV Community

Konrad Lisiczyński
Konrad Lisiczyński

Posted on

Taming network with redux-requests, part 2 - Basic usage

In the previous part of this series we mentioned many issues connected with making AJAX requests and how Redux could help us with them. We also introduced redux-requests library.

Now we will take those issues one by one and see how they are solved in redux-requests. Before we could do that though, we need to learn how to use this library.

Initial setup

Before you start, we need to install required dependencies first:

npm install axios @redux-requests/core @redux-requests/axios

As you noticed, we will use axios to make AJAX requests, but this library supports Fetch API and others too, so note you are not forced to use axios by any means.

Also, probably you already have those, but just in case make sure you have below installed too:

npm install redux reselect

Now, to start using redux-requests, you need to add below code in a place you initialize Redux store, something like:

import axios from 'axios';
import { handleRequests } from '@redux-requests/core';
import { createDriver } from '@redux-requests/axios';

const configureStore = () => {
  const { requestsReducer, requestsMiddleware } = handleRequests({
    driver: createDriver(axios),
  });

  const reducers = combineReducers({
    requests: requestsReducer,
  });

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

  return store;
};

So, as you can see, all you need to do is call handleRequests function with a driver of your choice and use the returned reducer and middleware in createStore.

Queries

After initial setup is done, you will gain a power to send AJAX requests with just Redux actions!

For example, imagine you have and endpoint /books. With pure axios, you could make a request as:

axios.get('/books').then(response => response.data);

With redux-requests all you need to do is write a Redux action and dispatch it:

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

// somewhere in your application
store.dispatch(fetchBooks());

fetchBooks is just a Redux action with request object. This object is actually a config object passed to a driver of your choice - in our case axios. From now on let's call such actions as request actions.

So, what will happen after such an action is dispatched? The AJAX request will be made and depending on the outcome, either FETCH_BOOKS_SUCCESS, FETCH_BOOKS_ERROR or FETCH_BOOKS_ABORT action will be dispatched automatically and data, error and loading state will be saved in the reducer.

To read response, you can wait until request action promise is resolved:

store.dispatch(fetchBooks()).then(({ data, error, isAborted, action }) => {
  // do sth with response
});

... or with await syntax:

const { data, error, isAborted, action } = await store.dispatch(fetchBooks());

However, usually you would prefer to read this state just from Redux store. For that you can use built-in selectors:

import { getQuery } from '@redux-requests/core';

const { data, error, loading } = getQuery(state, { type: FETCH_BOOKS });

What is query by the way? This is just a naming convention used by this library, actually borrowed from GraphQL. There are two sorts of requests - queries and mutations. Queries are made just to fetch data and they don't cause side-effects. This is in contrast to mutations which cause side-effects, like data update, user registration, email sending and so on. By default requests with GET method are queries and others like POST, PUT, PATCH, DELETE are mutations, but this also depends on drivers and can be configured.

Mutations

What about updating data? Let's say you could update a book with axios like that:

axios.post('/books/1', { title: 'New title' });

which would update title of book with id: 1 to new title.

Again, let's implement it as Redux action:

const updateBook = (id, title) => ({
  type: 'UPDATE_BOOK',
  request: {
    url: `/books/${id}`,
    method: 'post',
    data: { title },
  },
  meta: {
    mutations: {
      FETCH_BOOKS: (data, mutationData) =>
        data.map(book => book.id === id ? mutationData : book),
    }
  },
});

// somewhere in your application
store.dispatch(updateBook('1', 'New title'));

There are several interesting things here. First of all, notice post method, so this request action is actually a mutation. Also, look at meta object. Actually request actions can have not only request object, but also meta. The convention is that request object is related to a driver, while meta allows you to pass driver agnostic options, all of which will be described later. Here we use mutations, which in this case is used to update data of FETCH_BOOKS query. The first argument is data (current data of FETCH_BOOKS query) and mutationData (data returned from server for UPDATE_BOOK mutation).

And how to read responses and mutation state? Similar to queries:

store.dispatch(updateBook('1', 'New title')).then(({ data, error, isAborted, action }) => {
  // do sth with response
});

... or with await syntax:

const { data, error, isAborted, action } = await store.dispatch(updateBook('1', 'New title'));

... or just by using selector:

import { getMutation } from '@redux-requests/core';

const { error, loading } = getMutation(state, { type: UPDATE_BOOK });

Notice no data in getMutation - this is because mutations are made to cause side-effects, like data update. We don't store data in reducers for mutations,
we do this only for queries.

Request actions philosophy

Notice, that usually you would do such a thing like data update with a reducer. But this library has a different approach, it manages the whole remote state with one global reducer (requestsReducer) and advocates having update instructions in requests actions themselves. This has the following advantages:

  • you don't need to write reducers, just actions
  • all logic related to a request is kept in one place, encapsulated in a single action
  • because there is one global reducer, remote state is standardized which allowed to implement many features like caching, automatic normalisation and so on
  • as a consequence of above, you also don't need to write selectors, they are provided for you

A theoretical disadvantage is that passing a function like update function to an action makes it not serializable. But in reality this is not a problem, only reducers have to be serializable, actions not, for example time travel will still work.

Of course you still could listen to request actions in your reducers, but it is recommended to do this only for an additional state, so you would not duplicate state stored in requestsReducer, which is never a good thing.

What's next?

In the next part of this series, we will discuss race conditions and importance of requests aborts.

Top comments (0)