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 a previous article, Handling Application State with Redux, we saw how to use static data to load values into a form and keep the application state in sync as a user edits the form. However, in the real world data may not be already loaded or generated within the application. More than likely, we would need to load the data remotely. In this article, weโll start with the form we built in the previous Redux article and show how to add remote loading into the mix.
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.
Sync to Async Data Fetching
When we first looked at Redux, we saw that dispatching actions is a synchronous flow. However, loading remote data is performed asynchronously. Obviously this breaks the flow of our previous Redux setup. To handle asynchronous actions, we need to use a Redux middleware solution like redux-thunk. redux-thunk is a popular library for handling side effects in a React app. It handles asynchronous code within an app using Redux. Other popular libraries designed to assist in localizing the handling of side effects are redux-saga, redux-observable, and redux-promise.
redux-thunk allows a function to be returned in a store.dispatch()
call. Within this returned function, there is a callback function argument that will finish the dispatching of the result in an asynchronous flow. Letโs take a look at how we can modify the form from the previous article.
First, install redux, react-redux, redux-thunk, and optionally redux-devtools (for those developing with Chrome);
npm install --save redux npm install --save react-redux npm install --save redux-thunk npm install --save redux-devtools
redux-thunk Middleware Setup
We can edit src/index.js
to add the middleware to Redux:
import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; // add this line import thunk from 'redux-thunk'; // add this line import { applyMiddleware, createStore } from 'redux'; // add this line import { composeWithDevTools } from 'redux-devtools-extension'; // add this line import './index.css'; import App from './App'; import registerServiceWorker from './registerServiceWorker'; import userApp from './reducers'; // add this line const store = createStore(userApp, composeWithDevTools(applyMiddleware(thunk))); // add this line ReactDOM.render( <Provider store={store}> // add this line <App /> </Provider>, // add this line document.getElementById('root') ); registerServiceWorker();
Thatโs all we need to setup redux-thunk with Redux as itโs a simple middleware. Not a bad setup instruction!
User Reducer Defined
Next, we need to edit our user reducer (src/reducers/user.js
) to modify the default user and add in the handler for when the user data is loaded:
import { LOAD_USER, UPDATE_USER } from '../actions'; const DEFAULT_USER = { id: 0, name: '', email: '', phone: '', company: '', department: '', title: '' }; function user(userData = DEFAULT_USER, action) { switch (action.type) { case UPDATE_USER: return Object.assign({}, userData, action.payload); case LOAD_USER: return Object.assign({}, action.payload); default: return userData; } } export default user;
You may be wondering why the default user is a bunch of strings. For this simple example, the data passed to the form component needs to have defined values. Undefined values results in React throwing an error saying that we are switching from uncontrolled to controlled components. Having empty strings will circumvent that error. We also have a case clause to listen for the user load action where itโll return the user that was passed with the action.
Combining Reducers with the Reducer Index
The user reducer is combined in src/reducers/index.js
. The reducer index file (and ultimately all combined reducers) is referenced as userApp
in the src/index.js
file:
import { combineReducers } from 'redux'; import user from './user'; const userApp = combineReducers({ user }); export default userApp;
User Action Defined
To do the actual loading, we need to define a new user action creator (src/actions/user.js
):
export const LOADING_USER = 'LOADING_USER'; export const LOAD_USER = 'LOAD_USER'; export const LOAD_USER_ERROR = 'LOAD_USER_ERROR'; export const UPDATE_USER = 'UPDATE_USER'; export function loadUser(id) { return dispatch => { dispatch({ type: LOADING_USER, payload: { id } }); return fetch('/user.json') .then(res => res.json()) .then( payload => dispatch({ type: LOAD_USER, payload }), payload => // error dispatch({ type: LOAD_USER_ERROR, payload }) ); }; } export function updateUser(payload) { return { payload, type: UPDATE_USER }; }
We create four constants to hold two new action types: one for when a load has started, one for when the loading has finished, one for if the user load has failed, and another if the user is updated. This loadUser
action creator returns a function that takes the dispatch
callback function. Within this function, we dispatch the user loading action so that we can call any interim functions such as masking a view (see the Floating Components article for more about masking a component). We do this to let the user know that something is happening while we fire off the actual loading using the Fetch API. If the loading was successful, we dispatch the load user action (LOAD_USER
). Else, the load user error (LOAD_USER_ERROR
) action will be dispatched.
Combining Actions with the Action Index
The user action is made available to any containers using react-redux in src/actions/index.js
:
import { LOAD_USER, loadUser, UPDATE_USER, updateUser } from './user'; export { LOAD_USER, loadUser, UPDATE_USER, updateUser };
react-redux Form Container
The loadUser
and updateUser
functions are then imported with import { loadUser, updateUser } from '../../actions';
in the example below. We also need to add a prop that gains access to the dispatch
function so that we can load user data. To do this, we need to edit src/containers/user/Update.js
to add the function to mapDispatchToProps
:
import { connect } from 'react-redux'; import { loadUser, updateUser } from '../../actions'; import Form from '../../components/user/Form'; const mapStateToProps = state => Object.assign({}, state.user); const mapDispatchToProps = dispatch => ({ loadUser: id => dispatch(loadUser(id)), onFieldChange: e => { const { name, value } = e.target; const change = { [name]: value }; dispatch(updateUser(change)); } }); export default connect(mapStateToProps, mapDispatchToProps)(Form);
React Form Class
These modifications turn our synchronous Redux form example into an asynchronous, remote loading example. The only thing we need to do is to trigger the loadUser
function. This should be triggered within the Form
component just before the form’s structure is returned (props.loadUser(props.id);
):
import React from 'react'; const renderField = (props, name, label = name, type = 'text') => ( <div style={{ marginBottom: '12px' }}> <label style={{textTransform: 'capitalize'}}> {label} <input type="text" name={name} value={props[name]} onChange={props.onFieldChange} style={{ marginLeft: '12px' }} /> </label> </div> ); const Form = props => { props.loadUser(props.id); 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;
Sample User Data
The form data youโll see in the browser now comes from public/user.json
since it’s now being remotely loaded.
{ "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" }
Asynchronous Form Example
To create an instance of the form, we’ll use the following src/App.js
:
import React from 'react' import Form from './containers/user/Update' export default () => <Form />;
Conclusion
The way Redux operates using abstracted out reducers and action creators is not the same as what we see used in Ext JS. Once you have them created, the API to subsequently load the data is similar to a form.load()
you would do with Ext JS. Of course, handling asynchronous calls is not limited to loading data for a form. You can load anything with the methods shown such as fetching an array for a list, grid, or chart.
As you develop an application, youโll likely identify opportunities to abstract aspects of your code as you’ll likely introduce duplicate code when loading remote data for multiple components within the app. The next blog article in the series will continue our look at handling data queries, but within the context of another popular state manager, MobX.
Mitchell Simoens
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…