Redux Actions & Reducers
In the front-end, so far we've learned how to setup routes with pages, and create a simple form using redux-form. That is pretty good progress, but we've only been scratching at the React side of this big React + Redux ecosystem. If you don't know what Redux is or how it works, please go back to "Starting on React".
So a little bit of recap:
- Store is a global object that holds the state of the application. If we were to refresh the page and restore the store state, all views should also return to its original state before the refresh
- Actions handles external communication (ie. with API) and dispatches its different stages of the process. We never go flow from actions back to the component directly (except for forms). This is to follow redux's standard of all data flowing in a circle.
- dispatch() must only have two fields: type (a unique name for this dispatch) and payload (data that goes with this dispatch)
- Reducers captures its desired dispatch calls and manipulate the redux store base on the dispatch's type and payload field
Okay, so in the previous chapter, we left off with being able to display a console log of the final recipe object in the RecipeFormContainer.js
. Now let us link that method call to an API call.
Actions
In src/actions, we'll create a recipe.js
file:
/* @flow */
import * as types from './types';
import type {Action, Recipe} from 'src/types';
import request from 'lib/request';
export const createRecipe = (recipe: Recipe): Action => {
return {
name: types.RECIPE_CREATE,
callAPI: request('recipes', {method: 'POST', data: recipe})
};
};
In this recipe action file, we're creating a function that sends a HTTP Request to our api server via [POST] http://localhost:8280/api/recipes
. How is returning an object with fields name
and callAPI
handling this, you ask? There are actually three ways to handle returns: dispatch object, callAPIMiddleware and function with parameter dispatch.
callAPIMiddleware
callAPIMiddleware is a middleware that intercepts the call if the return object contains the following fields: name
& callAPI
. Of course, there are other fields that'll enhance the calling cycle. This middleware makes two dispatch calls throughout its lifecycle (ActionStates from actions/types -> ActionStates
): name + ActionStates.REQUEST
(before HTTP Request call) and name + ActionStates.COMPLETE/ActionStates.FAIL
(either complete or fail based upon HTTP Request result). In complete and fail dispatch call, the payload consists of the object returned from the api, so pay attention to what kind of results the api can return.
Fields
Fields | type | Description | |
---|---|---|---|
name | string | the action type identifier defined in actions/types | |
callAPI | Function \ | Promise | the HTTP Request promise or a function that returns a HTTP Request promise |
shouldCallAPI | ?Function | a function that returns a boolean determining whether this action should be called based on the store state passed into the parameter | |
onComplete | ?Function | a callback function that is triggered on promise complete | |
onError | ?Function | a callback function that is triggered on promise error | |
payload | ?Object | the payload that will be passed into name + ActionStates.Request payload |
Dispatch Object
Dispatch object is an object that contains the following two fields: type and payload. Type holds a unique string that allows reducers to identify this specific broadcast. Payload is the field that'll hold all the other data required for the reducer to use. If a dispatch object is directly returned in the action call, it'll act as a dispatch call.
Dispatch Function
When a function is returned in the action call, the function will have two parameters: dispatch and getState. Dispatch is a function that you can call, with a dispatch object as the parameter. GetState is another function that does not require any fields, and returns the current store object. With the use of dispatch parameter, it acts as a resolve/reject function call in a promise. This way, you can have multiple calls to the api, and have different dispatch calls throughout the process.
In src/types, add the following flow types:
export type Recipe = {
_id?: string,
name: string,
ingredients: Array<Ingredient>
};
export type Ingredient = {
name: string,
amount: Number,
unit: string
};
The objects you see here are object type declarations using the FlowTypes system recommended by Facebook. As of now, there are two ways to implement strong-typed programming in javascript: Flow and TypeScript. They both have their strength and weaknesses but since Flow is favoured by React, and TypeScript is favoured by Angular, so will the community. It'll be easier to debug and research further implementations if we follow their standards.
In src/actions/types
, add the following line:
export const RECIPE_CREATE = 'RECIPE_CREATE';
The fields exported in this file are variables used for actions to set their dispatch identification and for reducer to identify a specific dispatch. Using a variable to handle such identification will maintain consistency if type name were to change. We store all action types in this file for two reasons: awareness of naming conflict, and for listing in our intellisense. When you have a colliding type name, it can cause a lot of issues because you'll trigger unwanted reducer interception. When we put it in the same file, the intellisense will automatically notify you if there is a name collision. When you retrieve types by typing type., intellisense will automatically display everything in the
To make it easily accessible from other files, we'll update our index.js
file in src/actions
:
...
import * as RecipeActions from './recipe';
...
export const ActionCreators = Object.assign({},
RecipeActions,
...
);
Now to link it up with the container, we go back to RecipeFormContainer.js
's onSubmit(recipe)
method and add the action call:
onSubmit(recipe) {
console.log('recipe', recipe);
this.props.Actions.createRecipe(recipe);
}
Reducers
In src/reducers, we'll create a recipe.js
file
/* @flow */
import createReducer from 'lib/createReducer';
import * as types from 'actions/types';
const ActionStates = types.ActionStates;
const initialState = {
items: {}
};
export const recipe = createReducer(initialState, {
[types.RECIPE_CREATE + ActionStates.COMPLETE]: (state, action) => {
return {...state, items: {...state.items, [action.payload._id]: action.payload}};
}
});
Although in actions, the call has dispatched three possible states (REQUEST, COMPLETE/FAIL), we'll just capture COMPLETE for now. In a real-life scenario, we'd capture the other two dispatches as well to update the view.
Again, to make it easily accessible outside this folder, we'll update our index.js
in src/reducers
:
import * as recipeReducer from './recipe';
...
export default combineReducers(Object.assign({},
recipeReducer,
...
);
Test Result
If you don't have api and web running, hit $ npm start
on both. In the browser, go to http://localhost:3000/recipes/create. Have the console log open and fill up the form. When you hit submit, you should get a console log saying action RECIPE_CREATE_COMPLETE
. In there, go down the tree to next state -> recipe -> items -> [id]
. You should see a structure similar to what you input in the form, with an additional id field. What happened here is your recipe form has sent the recipe object to the action function createRecipe(recipe), and that function sends a HTTP Request to our api server via [POST] http://localhost:8280/api/recipes
with the recipe as the data object. Throughout the process of this action call, there are two dispatch calls it broadcasts. Before it starts the HTTP Request, it sends a dispatch notifying reducers it's about to call. When the result comes back, it'll make another dispatch broadcast notifying reducers either this call has succeeded or failed. These states are defined in our src/actions/types -> ActionStates
, and are automatically used in our lib/callAPIMiddleware.js
. You should end up with the following result in the console log.