Basic usage
Initial setup
Before you start using redux-requests
library, just add below snippet to your code:
import axios from 'axios';import { handleRequests } from '@redux-requests/core';import { createDriver } from '@redux-requests/axios'; // or another driver
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 FETCH_BOOKS = 'FETCH_BOOKS';
const fetchBooks = () => ({ type: FETCH_BOOKS, request: { url: '/books', // you can put here other Axios config attributes, like data, headers etc. },});
// somewhere in your applicationstore.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, pristine } = 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.
For people wondering, pristine
is true only when there was no request made for a given type yet.
This flag could be useful when server could reply with data
as null
and you would
need to know whether data
is really null
or just because no request was made.
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 UPDATE_BOOK = 'UPDATE_BOOK';
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 applicationstore.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?
To get more familiar with the usage of this library, it is very important to go through tutorial first which will help understand API of this library and its assumptions.
Then, you could check out how to use drivers and pick a driver of your choice or write your own if needed.
It is also advised to read actions and selectors guides.
Then, you could see examples, read other guides or scan the whole API to see all the available options.