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)