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
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)