Actions

As you probably noticed, this library is all about writing Redux actions. You need to do some basic setup in handleRequests, but then you will mostly write just actions.

Request actions

You probably remember from the tutorial how request actions look like:

const deleteBook = id => ({
type: 'DELETE_BOOK',
request: {
url: `/books/${id}`,
method: 'delete',
},
meta: {
mutations: {
FETCH_BOOKS: data => data.filter(book => book.id !== id),
},
},
});

After a request action is dispatched, AJAX request will be made and then a response action will be dispatched automatically. But how response action looks like actually?

Response actions

After server delivers a response for a request action, one of three results can happen, in our case, either DELETE_BOOK_SUCCESS, DELETE_BOOK_ERROR or DELETE_BOOK_ABORT. See below how those response actions could look like:

{
type: 'DELETE_BOOK_SUCCESS',
response: {
data: {
id: '1',
name: 'deleted book',
},
},
meta: {
mutations: {
FETCH_BOOKS: data => data.filter(book => book.id !== '1'),
},
requestAction: {
type: 'DELETE_BOOK',
request: {
url: '/books/1',
method: 'delete',
},
meta: {
mutations: {
FETCH_BOOKS: data => data.filter(book => book.id !== '1'),
},
},
},
},
}
{
type: 'DELETE_BOOK_ERROR',
error: 'a server error',
meta: {
mutations: {
FETCH_BOOKS: data => data.filter(book => book.id !== '1'),
},
requestAction: {
type: 'DELETE_BOOK',
request: {
url: '/books/1',
method: 'delete',
},
meta: {
mutations: {
FETCH_BOOKS: data => data.filter(book => book.id !== '1'),
},
},
},
},
}
{
type: 'DELETE_BOOK_ABORT',
meta: {
mutations: {
FETCH_BOOKS: data => data.filter(book => book.id !== '1'),
},
requestAction: {
type: 'DELETE_BOOK',
request: {
url: '/books/1',
method: 'delete',
},
meta: {
mutations: {
FETCH_BOOKS: data => data.filter(book => book.id !== '1'),
},
},
},
},
}

As you can see, type of response actions is equal to a related request action with a suffix (_SUCCESS, _ERROR or _ABORT).

Also notice meta in any response action, you can find there requestAction object, which is just a related request action which resulted in the response action. Also all meta keys are copied to response actions for convenience, that's why there is mutations key there. You can use this mechanism also to pass anything to meta, then it will be available in response actions too in case you need it.

Additionally, of course success actions have response key and error actions have error key.

Promisified dispatches

By default in Redux store.dispatch(action) will just return the dispatched action itself. However, this library changes this behaviour for request actions dispatches by returning promises resolving with responses. Because of that, not only you can await requests to be finished, but also you can read responses directly from the places you dispatched requests.

For example:

store.dispatch(fetchBooks()).then(({ data, error, isAborted, action }) => {
// data for success, error for error, isAborted: true for abort
});

As you can see, this promise is always resolved, never rejected. Why? To avoid unhandled promise rejection errors. Imagine you dispatch a request action somewhere, but in this place you are not interested in result. You just do store.dispatch(fetchBooks()). Now, even if you handle error in another place, like by reading error from state, in case of promise rejection the warning would be still there.

Anyway, promise is resolved on response as:

  • when success, as { data, action, ...extraDriverProps }, extraDriverProps are other optional keys next to data inside success response, for example axios and fetch drivers support headers and status, so promise would be resolved to { data, action, headers, status }
  • when error, as { error, action }
  • when abort, as { isAborted: true, action }

So action is always there in case you need an access to response action.

Actually there is one case when promise is rejected - a syntax error. Imagine you make an error in getData or onSuccess interceptor. In those cases promise will be rejected with syntax error itself, otherwise the error would be swallowed and you wouldn't know where a problem is.

FSA actions

If you happen to like writing Redux actions as FSA actions, you can use them for request actions too, for example:

const deleteBook = id => ({
type: 'DELETE_BOOK',
payload: {
request: {
url: `/books/${id}`,
method: 'delete',
},
},
meta: {
mutations: {
FETCH_BOOKS: data => data.filter(book => book.id !== id),
},
},
});

If you do it, response actions will have FSA structure too, for example:

{
type: 'DELETE_BOOK_SUCCESS',
payload: {
response: {
data: {
id: '1',
name: 'deleted book',
},
},
},
meta: {
mutations: {
FETCH_BOOKS: data => data.filter(book => book.id !== '1'),
},
requestAction: {
type: 'DELETE_BOOK',
payload: {
request: {
url: '/books/1',
method: 'delete',
},
},
meta: {
mutations: {
FETCH_BOOKS: data => data.filter(book => book.id !== '1'),
},
},
},
},
}

Action creator libraries

Because this library is just a Redux addon, it is totally compatible with action creator libraries, like redux-smart-actions, redux-actions or redux-act.

For example, when using redux-smart-actions:

import { createAction } from 'redux-smart-actions';
const deleteBook = createAction('DELETE_BOOK', id => ({
request: {
url: `/books/${id}`,
method: 'delete',
},
meta: {
mutations: {
FETCH_BOOKS: data => data.filter(book => book.id !== id),
},
},
}));

Usage for redux-actions and redux-act would be similar. Anyway, the key here to know is that when using libraries like that, you don't need to write constants anymore, just actions! And, because deleteBook.toString() === 'DELETE_BOOK', you can pass request actions themselves instead of request action types in many places, for example instead of:

import { getMutation } from 'redux-requests/core';
const deleteBookMutation = getMutation(state, { type: 'DELETE_BOOK' });

you could just do:

import { getMutation } from 'redux-requests/core';
const deleteBookMutation = getMutation(state, { type: deleteBook });

Thunks

Sometimes your request actions might need to get an information from Redux store. Of course you can always pass it as a function argument, but some people prefer using thunks for this purpose, for example:

const deleteBookThunk = () => (dispatch, getState) => {
const bookId = currentBookIdSelector(getState());
return dispatch(deleteBook(bookId));
};

This approach could prove very convenient, imagine you need to dispatch deleteBook action in multiple places, you would always need to read bookId in each place and pass it to deleteBook. With thunk you must do this only once.

There is a problem though, if you don't like writing constants but you prefer to use action creator libraries, then you would like your thunks to also contain type as toString so that you could pass thunks to getMutation directly for example. Fortunately, this problem is solved by the companion library redux-smart-actions. With its help, you could implement deleteBook as:

import { createThunk } from 'redux-smart-actions';
const deleteBook = createThunk('DELETE_BOOK', () => (dispatch, getState) => {
const id = currentBookIdSelector(getState());
return {
request: {
url: `/books/${id}`,
method: 'delete',
},
meta: {
mutations: {
FETCH_BOOKS: data => data.filter(book => book.id !== id),
},
},
};
});

Now, deleteBook.toString() === 'DELETE_BOOK' again, so you can pass it to functions like getMutation and forget about constants even when writing thunks!

Where to dispatch request actions

This library automatically makes AJAX requests and handles all remote state, but it doesn't care where you dispatch request actions from. This is totally up to you! You can dispatch them in sagas, in a middleware, in observables, in React components, from thunks or even inside routes when using Redux routers like redux-first-router. Basically, in any place you could dispatch a Redux action, you can dispatch request actions, after all request action is also just Redux action!

Last updated on by Konrad