Redux is a predictable state container and will be the library used to store the state of the checkout form, as well as any other relevant state that comes up. This guide assumes you already have a basic knowlegde of what actions, reducers and a store is.
The first thing we need to do is to download the libraries redux
and react-redux
. The latter contains the necessary bindings for React, so that you don't have to wire things up manually. Keep in mind redux is not uniquely linked to React, redux is agnostic about the framework used for rendering the UI and could also be used with Angular for instance.
yarn add redux react-redux
Second step is to create a file where we configure our redux store. Normallly that's done by creating a folder called store and placing inside a single file called configureStore.js
(or store.js
or index.js
). The relative location in the project should be src/js/store/configureStore.js
.
Below you can find the scaffolding of a basic redux store:
import { createStore } from 'redux';
const configureStore = () => {
const reducer = (state = {}, action) => state;
return createStore(reducer);
};
export default configureStore;
createStore
function takes 3 arguments, but only the first one is required, which is the root reducer.
Remember that a reducer is a function that takes the previous state and an action and calculates the new state of the app. For starters and being able to set up the store, let's just add that dummy reducer function that just returns always {}
I like to create a function that wraps the store creation to avoid confussions. ES6 modules are singletons. That is, there's only one instance of the module, which maintains its state. Every time you import that module into another module, you get a reference to the one centralized instance. That means we could have just avoided that function encapsulation and export directly the store as:
const reducer = (state = {}, action) => state;
const store = createStore(reducer);
export default store;
However, with the encapsulation it may be clearer when we perform the store instantiation (from the root React component), since we'll be calling something like configureStore()
, stating that's a one-time operation and shouldn't be done from anywhere else.
Once we have defined the store creation, we need to make our React app aware of it. Let's refactor a bit our entry point by:
- Creating an
index.js
file undersrc/js/
to conduct React bootstrap into the DOM and setup Hot Module Reloading.
import React from 'react';
import { render } from 'react-dom';
import App from './components/App';
render(<App />, document.getElementById('root'));
if (module.hot) {
module.hot.accept(); // HMR for JS
// HMR For CSS modules
document.querySelectorAll('link[href][rel=stylesheet]').forEach((link) => {
const nextStyleHref = link.href.replace(/(\?\d+)?$/, `?${Date.now()}`);
link.href = nextStyleHref; // eslint-disable-line no-param-reassign
});
}
- Moving
App.jsx
insidesrc/js/components
and usingProvider
component fromreact-redux
to inject the store down the tree.
import React from 'react';
import { Provider } from 'react-redux';
import configureStore from '../store';
import Header from './Header';
const store = configureStore();
const App = () => (
<Provider store={store}>
<Header />
</Provider>
);
export default App;
What Provider
does is making the store instance available to all children components by using context. Don't give too much credit to context, it's still an experimental React feature, but comes in handy when you want to pass data through the component tree without having to pass the props down manually at every level, hence a perfect suit for our store.
Last, but not least, let's leverage the amazing redux chrome extensions:
- Download it from the Chrome store
- Go to
configureStore.js
file and make this small tweak.
return createStore(reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
Congratulations! The store setup process is completed and you are realising how easy is to configure it (or maybe it's the amazing writer that is really doing a good job 🚀 )
Redux-form is a library that aims to ease management of a form state in Redux, so that we don't have to manage each form input state ourselves, nor the global state form (submission, isTouched, validation...). redux-form primarily consists of four things:
- A Redux reducer that listens to dispatched redux-form actions to maintain your form state in Redux.
- A React component decorator that wraps your entire form in a Higher Order Component (HOC) and provides functionality via props.
- A Field component to connect your individual field inputs to the Redux store.
- Various Action Creators for interacting with your forms throughout the application.
We need to give the redux-form reducer to Redux. Going to our store creation file:
import { createStore, combineReducers } from 'redux';
import { reducer as formReducer } from 'redux-form';
const configureStore = () => {
const reducers = combineReducers({
form: formReducer,
});
return createStore(
reducers,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
};
export default configureStore;
Keep in mind there is only one global reducer in our app. combineReducers
is an utility helper that allows us to split the reducing function into separate functions, each managing independent parts of the state. That means our global state object will have now a key called form
, where all the form state will live.
Decorate your form component with reduxForm(). This will provide your component with props that provide information about form state and functions to submit your form.
Creating a Form
component for that purpose seems like a good solution. As an example:
import React, { PropTypes } from 'react';
import { Field, reduxForm } from 'redux-form';
import InputBox from './InputBox';
const Form = () => (
<form>
<label htmlFor="firstName">Payment</label>
<Field name="creditCard" component={InputBox} type="checkbox" withBorder />
</form>
);
// Decorate the form component
export default reduxForm({
form: 'checkout', // a unique name for this form
})(Form);
reduxForm
is a Higher Order Component (aka HOC) o decorator. Simply put, it's a function that takes a React Component and returns a new one. One of the typical applications is to return an "enhanced" version of the component passed in, with those enhancements passed down in form of props.
As you can see above, we have wrapped InputBox
into the Field
component provided by react-redux
<Field name="creditCard" component={InputBox} type="checkbox" withBorder />
The Field component is how you connect each individual input to the Redux store. There are three fundamental things that you need to understand in order to use Field correctly:
-
The name prop is required. It is a string path, in dot-and-bracket notation, corresponding to a value in the form values. It may be as simple as 'firstName' or as complicated as contact.billing.address[2].phones[1].areaCode.
-
The component prop is required. It may be a Component, a stateless function, or string name of one of the default supported DOM inputs (input, textarea or select). In our case we are passing
InputBox
Component -
All other props will be passed along to the element generated by the component prop.
withBorder
will be passed down to our originalInputBox
.
In order to not alter the API defined for all our individual components that define the checkout form and decouple them from redux-form library, we can create a HOC that maps props, acting as an adapter. That way, if one day we decide to move away from redux-form, our components remain reusable and composable without the pain of having to refactor them again. We could create something simple like:
import React from 'react';
function fieldMapper(ReactComponent) {
return function TransformedField(props) {
return <ReactComponent {...props.input} {...props.meta} {...props} />; // eslint-disable-line react/prop-types
};
}
export default fieldMapper;
The only downside is we'd have to stick to the naming of input props used by react-redux, since the only thing the adapter is doing so far is destructuring everything under input
prop. It's not bad at all since the naming they follow is quite standard (i.e. checked instead of isChecked), so that would involve still some small tweaks in our component props.
To make the HOC more flexible, we could provide a config option as 2nd argument to fieldMapper that would indicate how we want to perform the mapping, some object like { checked: 'isChecked' }
To use it in our custom components, we have to export the adapted version:
import fieldMapper from '../fieldMapper';
...
export default fieldMapper(InputBox);
There are two ways to provide synchronous client-side validation to the checkout form.
The first is to provide redux-form with a validation function that takes an object of form values and returns an object of errors. This is done by providing the validation function to the decorator as a config parameter, or to the decorated form component as a prop. The second is to use individual validators for each field.
I'd recommend the 2nd approach since it provides us more modularity, so I'll focus on that one.
Each Field
component accepts a validate
prop which can be a function or an array of function validators.
We can define our validators in js/utils/validators.js
:
// Examples of function validators
export const required = value => value ? undefined : 'Required'
export const maxLength = max => value =>
value && value.length > max ? `Must be ${max} characters or less` : undefined
export const maxLength15 = maxLength(15)
export const number = value => value && isNaN(Number(value)) ? 'Must be a number' : undefined
export const minValue = min => value =>
value && value < min ? `Must be at least ${min}` : undefined
export const email = value =>
value && !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(value) ?
'Invalid email address' : undefined
And then import them, so that the Field
components we define can perform the needed validation. Following the example with the InputBox
component:
import React, { PropTypes } from 'react';
import { Field, reduxForm } from 'redux-form';
import InputBox from './InputBox';
import * as validation from '../utils/validation';
const Form = () => (
<form>
<label htmlFor="firstName">Payment</label>
<Field
name="creditCard"
component={InputBox}
type="checkbox"
validate={validation.required}
withBorder />
</form>
);
// Decorate the form component
export default reduxForm({
form: 'checkout', // a unique name for this form
})(Form);
Whenever we have an validation error, we'll receive it in our InputBox
(or any other component we have defined and wrapped with a Field
, such as InputText
) as an error
prop, thanks to our adapter. That would imply that if we don't tick that InputBox and we try to submit the form, validation will yell at us, preventing the form to be submitted and in the end having error='Required'
inside InputBox
, as we have defined the message to be in our validator helper.
How to display that error depends of the design, but it could be something along this lines:
// Render of InputBox
<div className={inputBoxClassNames}>
<label className={Styles.InputBoxLabel} htmlFor={name}>
<input type={type} value={value} id={name} name={name} className={Styles.Input} onChange={onChange} checked={checked} />
<span className={Styles.Button} />
<div className={Styles.Description}>
<span className={Styles.Label}>{label}</span>
{children}
</div>
</label>
{error && <span>{error}</span>} // <-- Display error if we have one
</div>
In order to preserve the form state across page reloads, there is a library that plays nicely with redux and can really help us with that task, called redux-persist.
Its usage is pretty straightforward, we just need to modify the store creation part.
import { createStore, combineReducers, compose } from 'redux';
import { reducer as formReducer } from 'redux-form';
import { persistStore, autoRehydrate } from 'redux-persist';
const reducers = combineReducers({
form: formReducer,
});
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; // eslint-disable-line no-underscore-dangle
const store = createStore(
reducers,
composeEnhancers(
autoRehydrate()
)
);
persistStore(store, {
debounce: 500,
whitelist: ['form'],
});
export default store;
And that's really it! Now our form state is kept across page reloads 😃
You don't need to exhaustevely understand the code, but the idea is that composeEnhancers
or compose
allows us to enhance the capabilites of the basic createStore
, such as with the addition of middleware or data persistance (like in this case).
autorehydrate
checks if we have previous state stashed into localStorage, parses it and feeds our store with it on startup.
persistStore
defines how and what we want to persist, in our case, writing into storage every 500ms and only serializing and saving our form state (everything under state.form
)
So far we have seen how redux-form
works and how it interacts with redux
. Now it's turn to shed some light on how to create any other state needed for our app, as well as how to imperatively dispatch actions ourselves, in order to be able to change the state of our app.
But first let me step back to remind you the 3 core principles of redux:
- Single source of truth: the state of your whole application is stored in an object tree within a single store
- State is read-only: the only way to change the state is to emit (aka dispatch) an action, an object describing what happened
- Changes are made with pure functions: To specify how the state tree is transformed by actions, you write pure reducers. Remember to always return new state objects, instead of mutating the previous state
Being said that and making sure myself you tatoo those 3 principles in your skin, let's move forward.
The 3 pieces we need to put together are Action types, Action Creators and Reducers.
The traditional approach follows splitting those 3 pieces into 3 separate files. However, as you are introducing new functionality in your redux app, you'll find yourself needing to add {actionTypes, actions, reducer}
tuples again and again. Most of the time there will be a unique correlation between action and reducer, so it makes more sense for these pieces to be grouped together in an isolated module that is self contained, instead of having to poke around in three different places per new functionality
Erik Rasmussen came with a proposal called ducks, to define each one of these self contained modules. There are a few rules which are handy to keep in mind while writing your ducks modules. The rules of ducks are as follows, pulled from the repo's documentation:
A module...
- MUST export default a function, the reducer
- MUST export its action creators as functions
- MUST have action types in the form npm-module-or-app/reducer/ACTION_TYPE
- MAY export its action types as UPPER_SNAKE_CASE, if an external reducer needs to listen for them, or if it is a published reusable library
When tackling the implementation of these elements, there is unavoidably a bit of boilerplate and code repetition, such as switch-case pattern for reducer and action creators payloads. There is a nice utility library that aims to reduce that boilerplate, redux-actions, which we'll use in this exemplification.
Another library to keep in your toolbelt is immutability-helper, which allows you to perform copy of data without changing the original source, in order to fulfill the 3rd principle of redux boiled down at the beginning of the section
With all that, and I know you are eager to, let's see some code in place:
import { createAction, handleActions } from 'redux-actions';
import update from 'immutability-helper';
// Actions, action name: ${your-app-name}/${your-duck-name}/${<NOUN>_<VERB>} -> For namespacing and sorting your reducers
const PRODUCTS_REQUEST_FETCH = 'super-checkout/dummy/PRODUCTS_REQUEST_FETCH';
const PRODUCTS_SUCCESS_FETCH = 'super-checkout/dummy/PRODUCTS_SUCCESS_FETCH';
const MORE_PRODUCTS_LOAD = 'super-checkout/dummy/MORE_PRODUCTS_LOAD';
// Reducer, check https://github.com/kolodny/immutability-helper#available-commands to find out about $set, $push...
const initalState = {
isFetching: false,
products: [],
};
const handleFetchProductsRequest = state =>
update(state, {
isFetching: { $set: true },
});
const handleFetchProductsSuccess = (state, { payload }) =>
update(state, {
isFetching: { $set: false },
products: { $set: payload },
});
const handleLoadMoreProducts = (state, { payload }) =>
update(state, {
isFetching: { $set: false },
products: { $push: payload },
});
export default handleActions({
[PRODUCTS_REQUEST_FETCH]: handleFetchProductsRequest,
[PRODUCTS_SUCCESS_FETCH]: handleFetchProductsSuccess,
[MORE_PRODUCTS_LOAD]: handleLoadMoreProducts,
}, initalState);
// Action Creators, action creator name: <verb><Noun> -> to clearly identify what type of function it is
export const fetchProductsRequest = createAction(PRODUCTS_REQUEST_FETCH);
export const fetchProductsSuccess = createAction(PRODUCTS_SUCCESS_FETCH);
export const loadMoreProducts = createAction(MORE_PRODUCTS_LOAD);
Notes:
- Why
handleActions
instead of the documented switch statement? Primarily because it keeps a clean switch-like syntax, while adding block scoping to the cases. This means you can reuse variable names in each “case” createAction
creates a flux-standard action creator. For instance, when we'll callloadMoreProducts(products)
from our code, it'll return the action{ type: 'super-checkout/dummy/MORE_PRODUCTS_LOAD', payload: products }
.
React-redux comprises the official bindings that connects redux to React.
Its API has merely two utilities, <Provider >
and connect()
. We have already seen the first one when injecting the store into our React application and that's the only place where you'll ever use it.
connect
though, is a function that returns a HoC (you should be already familiar with the concept 😃 ). That HoC itself, returns a new, connected component class for you to use. That's why it needs to be invoked two times. The first time with some config arguments and a second time, with the component. We'll ilustrate it with a simple example:
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import { toggleDummy } from '../ducks/dummy';
const DummyToggle = ({ isDummy, onToggleDummy }) => (
<div>
<button onClick={() => onToggleDummy()}>Toggle</button>
<span>{isDummy ? 'Oh ma\'h, I am a dummy component! 🐗 ' : 'I am a clever component 🤓 '}</span>
</div>
);
DummyToggle.propTypes = {
isDummy: PropTypes.bool.isRequired,
onToggleDummy: PropTypes.func.isRequired,
};
export default connect(
state => ({
isDummy: state.dummy.isDummy,
}),
{
onToggleDummy: toggleDummy,
}
)(DummyToggle);
The most important config arguments that connect receives are:
mapStateToProps: Function
: takes a single argument of the entire Redux store’s state and returns an object to be passed as props. It is often called a selector. If this argument is specified, the new component will subscribe to Redux store updates. This means that any time the store is updated, mapStateToProps will be called.mapDispatchToProps: Function || Object
: If an object is passed, each function inside it is assumed to be a Redux action creator. An object with the same function names, but with every action creator wrapped into a dispatch call so they may be invoked directly. For instance, when you click on the button, the onClick listener will fireonToggleDummy()
, which in turn will result ondispatch(onToggleDummy())
For more advance scenarios, check the official API