
Create scalable, no-boilerplate redux Apps!
Arc is an abstraction layer to help you reduce boilerplate on redux-apps and also, organize better your code. Additionally, it has utilities to handle async requests.
Why
Redux is awesome! But people often complain about how much boilerplate they have to write when using it. Part of this problem, is because they feel unproductive defining constants, action creators and big reducers, but also because they don’t have a clear idea on how to organize their project, or even how to proper handle async requests. This project, intends to help on all that aspects!
We don’t intend to recreate the wheel, instead, we tried to use what the community are used with, and build up some approaches together in order to clarify the things about the project architecture, code splitting and the things around actions.
Creators and types are generated by a config
// actions.js
import { createActions } from 'redux-arc';
export { types, creators } = createActions('jedi', {
add: null,
});
Create reducers using createReducers and types:
import { createReducers } from 'redux-arc';
import { types } from './actions';
const INITIAL_STATE = [];
const HANDLERS = {
[types.ADD]: (state, action) => [...state, action.payload]
};
export default createReducers(INITIAL_STATE, HANDLERS);
Call the creators providing payload and meta
import { creators } from './actions';
const payload = {
name: 'Luke',
master: 'Yoda',
};
const meta = { foo: 'bar' };
dispatch(creators.add(payload, meta))
Create Async Actions
import { createActions } from 'redux-arc';
export { types, creators } = createActions('jedi', {
add: { url: '/api/jedi' method: 'post'},
});
dispatch(creators.add(payload, meta));
types.ADD.REQUEST // JEDI_ADD_REQUEST
types.ADD.RESPONSE // JEDI_ADD_RESPONSE
createActions creates both, regular and async actions. Async types has REQUEST and RESPONSE type, respectively to when a request starts and when it finishes.
Getting started
yarn add redux-arc
or
npm i --save redux-arc
To understand this docs, you should have a good understand of redux: what are Action creators, what are reducers, middlewares and also, what is a Flux Standard Action. So please, if you need a recap on those concepts, read the links bellow:
Redux Docs
Flux Standard Action
Action creators and Types
When you have to create a new action on redux, the first 2 steps you usually do is defining a const to you action type and then defining an action creator.
const NEW_JEDI = 'NEW_JEDI';
const newJedi = (payload) => ({
type: NEW_JEDI,
payload,
});
The above code is fine, but the problem is, you have dozens or hundreds of action in an application, and you are always writing the same code. Also, if you are not strict on code review, you end up having situations where your action type has a name different from your action creator:
const NEW_JEDI = 'NEW_JEDI';
const addJedi = (payload) => ({
type: NEW_JEDI,
payload,
});
Thinking in the above issues, Arc has a createAction function, that you use to define your actions and it generates the types and action creators automatically for you. Take a look:
import { createActions } from 'redux-arc';
const { types, creators } = createActions('yourNamespace', {
newJedi: null,
});
types.newJedi // YOUR_NAMESPACE_NEW_JEDI
const payload = {
master: 'Yoda',
name: 'Luke',
};
creators.newJedi(payload);
/*
{
type: 'YOUR_NAMESPACE_NEW_JEDI',
payload: {
master: 'Yoda',
name: 'Luke',
}
}
*/
The createActions method, expects a namespace as its first argument, this will be uppercased and will serve as a prefix for the actions. As the second parameter, we need to provide a config object, which the key is the action name and the value is an object with default values for payload and meta, or null if you don’t want to provide defaults. Then, it will return creators and types.
Both creators and types are objects, the first one contains the action creators for the actions you defined. As in the case above we defined an action newJedi, then we have a creator at creators.newJedi.
Creators accepts until three arguments:
- payload: could be of any type. Will become the action.payload
- meta: could be of any type. Will become the action.meta
- error: boolean. Indicates if the action has an error or not. You can omit it if the action has no error.
The api was designed following the flux-standard-action concepts. It can be strict about how your action should look like, but this will help you creating better actions. Also, now you can be sure that the first parameter you provide to the action creator, will become the action.payload.
The types is an object that contains strings. Its keys are the action names, but different from the creators, here they are uppercased:
types.NEW_JEDI
Also, when you do a console.log to see its content, you can see that we prefix the actions with the namespace you provided:
const { types } = createActions('yourNamespace', {
newJedi: null,
});
types.NEW_JEDI // -> YOUR_NAMESPACE_NEW_JEDI
We decided to have the namespace, to not stop you from having actions with the same name in different modules. Don’t worry, you will be able to differ them easily when using redux-dev-tools, just remember to provide unique namespaces.
Async Actions
Originally, Arc was created to be an abstraction layer to handle async request in Redux. So, it has all you need about that. The api to generate async actions and types is the same, you only need to provide some additional params in the action config object. Take a look below:
import { createApiActions } from 'redux-arc';
const { creators, types } = createActions('todo', {
list: { url: 'api/todo', method: 'get' },
read: { url: 'api/todo/:id', method: 'get' },
create: { url: 'api/todo', method: 'post' },
update: { url: 'api/todo/:id', method: 'put' },
});
Above are defined three actions: list, read, create.
Two params are required for a async action config, url and method.
- url: You can define any url you want and it also accepts dynamic params, as you can see in the read action. We defined a dynamic param id, inserting :id
- method: Generally speaking, any http method your request lib supports. This will be used only by you in the asyncMiddleware that you will configure.
Async Creators and Async Types
An async creator is very similar to a simple creator. It accepts payload and meta as arguments. They will further become the action.payload and action.meta, as in a regular creator. The difference here, is that the meta, should be an object, and its values will be used to parse dynamic urls. Considering that, the creator read should be used like that:
const payload = null;
const meta = { id: '123' };
creators.read(payload, meta);
With the above code, the final url to your read request, would be api/todo/123
The async types differ a little bit from the regular ones as well. First, as a api call has two moments (request and response), we need two different types to use inside our reducers. Considering that, Arc returns an object for each type, containing a REQUEST and a RESPONSE key with the respective types:
types.READ.REQUEST == 'TODO_READ_REQUEST';
types.READ.RESPONSE == 'TODO_READ_RESPONSE';
Async Middleware
Arc doesn’t intend to be a request lib, so, you need to tell it how you want to make your requests, and you do that by configuring the asyncMiddleware.
It’s quite simple, take a look below in an example using axios:
import { createAsyncMiddleware } from 'redux-arc';
import axios from 'axios';
const asyncTask = store => done => (options) => {
const { method, url, payload } = options;
const params = method === 'get' ? { params: payload } : payload;
axios[method](url, params).then((error, response) => done(error, response));
};
// create the async middleware
const asyncMiddleware = createAsyncMiddleware(asyncTask);
// set it to the Store
const store = createStore(
reducer,
applyMiddleware(asyncMiddleware),
);
In the above example, we are using axios, but you can use whatever you want to perform the request, just make sure you call done, passing error and response when the request ends.
For more info about asyncTask and createAsyncMiddleware, read Connecting Arc Into Redux
Reducers
Beyond types and action creators, we also have reducers. There are a few ways to deal with them, some approaches use switch case, some others use multiple IFs. With most of them, you end up having a lot of code inside the same function, which makes maintenance and focusing hard. I know some approaches mention that you can split your code into small functions when it gets bigger, but why do not start from something that is easy to scale and also allow you to focus on each action separately?
Thinking about that, we created the function createReducers
createReducers
This factory was created to work standalone, it doesn’t require you to use any other feature from Arc. If you like, you can continue creating your actions and types as you always did. Take a look at the example below:
// vanillaActions.js
export const ADD_NEW_JEDI = 'ADD_NEW_JEDI';
export const addNewJedi = (name, master) => ({
type: ADD_NEW_JEDI,
name,
master,
});
Above we defined our types and action creator using pure JavaScript . Below, you can see how we could use it with createReducers:
import { createReducers } from 'redux-arc';
import { ADD_NEW_JEDI } from './vanillaActions';
const INITIAL_STATE = [];
const onAddNewJedi = (state, action) => [
...state,
{ name: action.name, master: action.master },
];
const HANDLERS = {
[ADD_NEW_JEDI]: onAddNewJedi
};
export default createReducers(INITIAL_STATE, HANDLERS);
You must provide a INITIAL_STATE and a HANDLERS object, which the keys should be the action types and the values should be reducers.
Using createActions with arc’s types object
As the types object is a simple JavaScript object, with strings, it fits perfectly with createReducers. Take a look below in an example using both, a regular and a async action with createReducers:
actions.js:
const { creators, types } = createActions('todo', {
list: { url: 'api/todo', method: 'get' }, // async action
reset: null, // regular action,
});
reducers.js
import { createReducers } from 'redux-arc'
import { types } from './actions';
const INITIAL_STATE = {
listResult: [],
listIsLoading: false,
listError: null,
};
const onListRequest = (state, action) => ({
...state,
listIsLoading: true,
listError: INITIAL_STATE.listError,
});
const onListResponse = (state, action) => {
if (action.error) {
return {
...state,
listIsLoading: INITIAL_STATE.listIsLoading,
listError: action.payload,
}
}
return {
...state,
listIsLoading: INITIAL_STATE.listIsLoading,
listResult: action.payload
}
};
const onReset = state => INITIAL_STATE;
const HANDLERS = {
[types.LIST.REQUEST]: onListRequest,
[types.LIST.RESPONSE]: onListResponse,
[types.RESET]: onReset,
};
export default createReducers(INITIAL_STATE, HANDLERS);
As we mentioned, we are using two kinds of action types, regular and async.
For the regular, there’s no much secret, types.RESET contains a string TODO_RESET generated by createActions, using the namespace and the action name we defined earlier.
The async action, contains an object, with REQUEST and RESPONSE keys, each one containing a string, which is the actual type.
Arc will dispatch the respective request and response action using the same types we have in types object and the same applies for regular actions, so Arc doesn’t open breaches for you to commit a typo.
Async Actions
Response action:
When the request is done, an action with the response will be dispatched. Considering the list example, the response action would look like this:
{
type: 'JEDI_LIST_RESPONSE', // types.LIST.RESPONSE,
meta: {
url: 'api/todo',
method: 'get',
},
payload: [
// resource list
],
}
Error handling
The above example is a a response with success. Accordingly to FSA, errors should be treated as a First class concept. In that case, when you got some error in an async request, the response action will come with the error property as true and the payload will be the actual error. Just like the example below:
{
type: 'JEDI_LIST_RESPONSE', // types.LIST.RESPONSE,
meta: {
url: 'api/todo',
method: 'get',
},
payload: new Error('the request error'),
error: true,
}