Ext JS to React: Handling Data with Redux

   JavaScript
Ext JS to React: Handling Data with Redux

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 />;


Ext JS to React: Handling Data with Redux


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.


This website uses cookies

These cookies are used to collect information about how you interact with our website and allow us to remember you. We use this information in order to improve and customize your browsing experience, and for analytics and metrics about our visitors both on this website and other media. To find out more about the cookies we use, see our Privacy Policy.

Please consent to the use of cookies before continuing to browse our site.

Like What You See?

Got any questions?


>