This is part of the Ext JS to React blog series. You can review the code from this article on the Ext JS to React Git repo.
In the previous article, Binding with React, we saw how each component can manage its own state. This works well within limited scopes, but as applications grow component state’s limitations begin to emerge. A complex application may need to pass common data throughout your application via props on child items. For example, what if the data for the user in the form were to come from somewhere higher up in the application? Each component would have to set a property on a child until the data traveled down to the form. To broadcast changes in the form, you’d have to notify each parent of the changes up the component hierarchy. This can quickly become brittle spaghetti code. This is where state could be abstracted away from individual components using Redux.
Redux Overview
With Redux, state becomes a global concept where there is only a single state instead of many. This is often referred to as the single source of truth and is a store (donโt confuse this with an Ext JS store – itโs not holding an array of records, itโs holding your application state). You can think of it as a global object that holds information pertaining to the current state of your application. The global object being a tree, each leaf is the state of a particular component in your app. This is different from Ext JS where you may have many view models spread around your application. Redux is more akin to having a single view model at the root of your application.
Reducers
Another difference is how you update the state. With Ext JS, you would use the set
method directly on the view model that holds your data. With Redux, the state is actually read only. That seems odd, right? How does the state update over time, then? Redux works by dispatching an action and something referred to as a reducer will return the new state. So you donโt edit the state object, you return a completely new state. A reducer is a pure function that takes the current state and action that was dispatched as arguments and will return the new state.
Note: Don’t worry – this post will have examples that cover all of this information. Stick with us. ๐
Actions
There is one more critical piece of the Redux puzzle that is important to know and that is actions. An action is what will dispatch data that will then be received and processed by the reducer. An action is a simple object that must contain the type
property to signify what kind of action it is. This is like an event name when firing an event in Ext JS. Itโs common to abstract out the logic of creating actions which we will discuss in detail later.
Redux Setup
Note: While not a requirement for React, this article’s code examples assume you’re starting with a starter app generated by create-react-app. Later in the article we’ll be altering a Form class created in the Binding with React article so it may be helpful to have it open and accessible while going through this article.
Before getting into the example code below, we’ll first need to install the redux and react-redux packages:
npm install --save redux npm install --save react-redux
Create a Redux Store
There are going to be multiple files to connect all the dots. The directory structure will also grow in complexity somewhat beyond the simple React-only structures seen in past blog articles in this series. However, in the end, youโll see the separation of logic and the construct will all make sense, we promise. The first thing we need to do is create the store so that our state can live somewhere. This is commonly done in the root file where your application is kicked off. For those following along in a starter app generated by create-react-app, this would be src/index.js
which would look like:
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import registerServiceWorker from './registerServiceWorker'; import { Provider } from 'react-redux'; import { createStore } from 'redux'; import userApp from './reducers'; const store = createStore(userApp); ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') ); registerServiceWorker();
The Provider
class from the react-redux package allows the store to be accessed from anywhere in your application. This means all components can connect to the global state. The store is created by passing the reducer (we will see this soon). Finally, we wrap the App
instantiation with the Provider
which receives the store as a prop.
Define the Reducers
When creating a reducer, we start to think of TDD (test-driven development) where you create the test and then you create the code so the test passes. With a reducer, you create the reducer first. Then you create the action that works with the reducer. Remember, the reducer is what controls the new state so itโs the end of the flow of state management. Itโs also common to nest your reducers in separate files within a reducers
directory.
In the following example, we will define a user
reducer and an index
that can combine multiple reducers into one. We will combine multiple reducers into one despite this example having only the one reducer. We do this to demonstrate the structure you would create in your own projects since a real application would likely have many reducers. The index reducer would be located at src/reducers/index.js
. It is what was passed to the createStore
function in the src/index.js
file from the previous example.
Combining Reducers
This index reducer is simple and compact and would look like:
import { combineReducers } from 'redux'; import user from './user'; const userApp = combineReducers({ user }); export default userApp;
This index reducer is taking the user reducer and using the combineReducers
function to create a single reducer. The great thing about the combineReducer
is that the โchildโ reducers are now only responsible for the nested state. In this case, the user reducer is being set using user
as the property so it will only be expected to return state for the user
property of the global state object.
User Reducer
Now we need to create the user reducer located at src/reducers/user.js
:
import { UPDATE_USER } from '../actions'; const DEFAULT_USER = { id: 1, name: 'Don Draper', email: 'don.draper@scdp.com', phone: '+1 (212) 555-0112', company: 'Sterling Cooper Draper Pryce', department: 'Marketing', title: 'Creative Director' }; function user(userData = DEFAULT_USER, action) { switch (action.type) { case UPDATE_USER: return Object.assign({}, userData, action.payload); default: return userData; } } export default user;
The user reducer is where the new state is formed and returned. The user reducer only cares about the nested user part of the global state, so we only need to return the user data. With redux, when an action is dispatched it’s dispatched to all reducers. Itโs common to use a switch
statement and check for defined action types. When an action type is recognized, we return the new state for that nested state object. In this case, we take the old user state data, create a new object, and then apply the changed delta to form a new user object including the specific change. So if you change a userโs name, the action.payload
object is only expected to have the new user name.
Define the Action Creators
As we did with reducers, in order to create an action we need to create a user specific file and an index file. The index file will be in charge of collating the various action creators and exporting all the action creators in use throughout your application. Letโs look at src/actions/index.js
:
import { UPDATE_USER, updateUser } from './user'; export { UPDATE_USER, updateUser };
Here, we import two variables and then export them. For this small example, this file doesnโt do anything meaningful. However, in a real application, you will have many more action creators that you would want to combine in this index action.
The user action creator at src/actions/user.js
would be:
export const UPDATE_USER = 'UPDATE_USER'; export function updateUser (payload) { return { payload, type: UPDATE_USER }; }
The updateUser
function is expecting to get the change delta object and will return an object (an action). The object can be anything but the type
property is important as that is what is checked in the switch
statement in the reducer. Itโs a good idea to have a constant as the type. We are using UPDATE_USER
in the action’s updateUser
function, but itโs also used in the reducerโs switch
statement. This action name is now defined in a single location so any change here would be automatically reflected in the action creator as well as the reducer.
Dumbing Down the Form
The Form class we defined in the Binding with React article now needs to go under the knife. Previously, it was using Reactโs component state, but we wonโt need that anymore. That also means we can use the function syntax instead of a full class when defining the form. We also want to move the form from src/Form.js
to src/components/user/Form.js
. We will address the need to move the file a bit later in the article. The new form would look like:
import React from 'react'; const renderField = (props, name, label = name, type = 'text') => { return ( <div style={{ marginBottom: '12px' }}> <label style={{textTransform: 'capitalize'}}> {label} <input type="text" name={name} value={props[name]} onChange={props.handleFieldChange} style={{ marginLeft: '12px' }} /> </label> </div> ); }; const Form = props => { const submit = e => { e.preventDefault(); // do submit }; return ( <form> {renderField(props, 'name')} {renderField(props, 'email', undefined, 'email')} {renderField(props, 'phone', 'Phone Number', 'tel')} {renderField(props, 'company')} {renderField(props, 'department')} {renderField(props, 'title')} <button onClick={submit}>Submit</button> </form> ); }; export default Form;
The form is now a simple function that receives props and then uses the renderForm
function to create the individual inputs. This can now be called a simple presentational component as there is no logic. It will only handle presenting with no business logic used to handle user interactions.
Handling User Interactions in the Form
So far, weโve created the store to hold the state, a reducer to return new state, an action creator that will get dispatched, and we’ve modified the form to remove everything but the presentational bits. To finish this off, we need a way to handle user interactions such as typing in a field. For this, we need to create a container component that will connect the form to Redux. We will store this component in src/containers/user/Update.js
and will look like:
import { connect } from 'react-redux'; import { updateUser } from '../../actions'; import Form from '../../components/user/Form'; const mapStateToProps = state => { return Object.assign({}, state.user); }; const mapDispatchToProps = dispatch => { return { handleFieldChange: e => { const { name, value } = e.target; const change = { [name]: value }; dispatch(updateUser(change)); } }; }; export default connect(mapStateToProps, mapDispatchToProps)(Form);
Update Form Class Explained
The react-redux package provides a connect
function that wraps a view with some utility functions in order to pass the state to the form as props. The connect
function also dispatches actions that the reducers use to update the state. The mapStateToProps
argument is a function that will receive the whole state. It returns an object that will be passed to the target component as props.
In this case, we want the state.user
object and we pass each property of that object as individual props to the form. The mapDispatchToProps
function returns more props that are passed to the form, but gets the Redux store’s dispatch
function. The dispatch
function accepts an action creator function. Here, we use the updateUser
action creator and pass it the change delta object. Once dispatched, the action is provided to all reducers to selectively handle and return a new state.
In this example, we have an onFieldChange
function passed in as a prop. It is called via the onChange
attribute for each form field. The onFieldChange
handler creates the change delta object, creates the action, and dispatches that action to be handled by the appropriate reducer. You can think of this container component like you might a view controller with Ext JS.
Bringing it All Together in the Application
Finally, in src/App.js
instead of using the presentational form component when rendering the form, we need to use the Update container (since it’s been furnished with state props and dispatching functions courtesy of the react-redux connect
magic):
import React from 'react'; import Form from './containers/user/Update'; export default () => <Form />;
Notice that we now do not need to pass a user object to the form. Instead, the form will get the user data from the user reducer since we defined a default user object. In a real application, the user data would come from somewhere in the application logic instead of being hardcoded like we did in this example.
Using Devtools to Inspect the Global State
For those using the Chrome browser for development, you may want to check out the Redux DevTools plugin. The DevTools plugin makes it easy to inspect the state object at runtime. To enable this example to work with the DevTools plugin modify call to createStore()
in the src/index.js
file to be:
const store = createStore( userApp, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() );
Now when you run the example you can open the “Redux” tab on the Chrome DevTools panel and select the “State” sub-tab. The state’s user node can be expanded to reveal the data we’re using to populate our form.
Wrap It Up
There is a bit of a learning curve to understand what each piece does, but the separation of concerns helps organize and abstract the individual mechanics of the system. Redux is one of, if not the most popular, state management tools. To learn more, please refer to the Redux online documentation. There you’ll find detailed documentation to explain concepts including plenty of example code. Stay tuned for our next blog article where we consider an alternative state management solution: MobX.
Seth Lemmons
Related Posts
-
Ext JS to React: FAQ
This is part of the Ext JS to React blog series. React is Facebook's breakout…
-
Ext JS to React: Migration to Open Source
Worried about Migrating from Ext JS? Modus has the Answers Ideraโs acquisition of Sencha has…