Reusable data handlers in React & Redux. Simple and flexible!

Henrik Haugberg
Techtive
Published in
11 min readJul 23, 2019

In a previous article, I described how code for editing data in web forms can be simplified using an EntityData architecture that is built around use of path & value for change events. But business logic for applications usually consists of far more processing code than just that.

Examples of actions you might want to implement in applications are:

  • Fetching data from server (asynchronous process)
  • Indicators that data is being retrieved
  • Determine whether data is outdated and needs to fetched again
  • Displaying & traversing data
  • Initiate editing (switch from view to form)
  • Change data, preferably without losing the original value
  • Show what has been edited
  • Edit part of a data set (eg, an object from a list)
  • Validate data
  • Display error messages (preferably by the relevant field)
  • Cancel editing
  • Save the edited values to the server
  • Delete the data
  • Delete parts of data sets

In frameworks such as React, much of this can be entered as state variables. They may be in the components that display the data/form, or in separate variables in parallel with the data itself where they are located (eg in a Redux store). But in the same way as with the form fields, this tends to require a large amount of boiler plate code. The processes are often repeated on many different views in the application, with small variations from place to place.

Manual events

Let’s first make an example we can rectify in the search for the optimal way to handle data flow. Like so many times elsewhere in a developer’s life, it is about standardizing the code, without losing flexibility.

A user named Jon Snow is going to change its name to Aegon Targaryen. For this, the application has a web form for editing profile data:

// actions.js
export const FETCH_USER_INITIATE = 'FETCH_USER_INITIATE';
export const FETCH_USER_COMPLETE = 'FETCH_USER_COMPLETE';
export const FETCH_USER_ERROR = 'FETCH_USER_ERROR';
export const CHANGE_USER = 'CHANGE_USER';
export function fetch = () => async (dispatch) => {
const headers = new Headers();
headers.append('Content-Type', 'application/json');
try {
const res = fetch('http://example.com/me', {
credentials: 'include'
headers
});
const response = await res.json(); if (res.statusCode === 200) {
dispatch({
type: FETCH_USER_COMPLETE
response
});
} else {
dispatch({
type: FETCH_USER_ERROR,
statusCode: res.statusCode,
error: new Error(response.message)
});
}
} catch (e) {
// No response
dispatch({
type: FETCH_USER_ERROR
error: new Error('Could not connect to API');
});
}
}export function change = (path, value) => {
type: CHANGE_USER,
path,
value
};
// reducers.js
import {
FETCH_USER_INITIATE,
FETCH_USER_COMPLETE,
FETCH_USER_ERROR,
CHANGE_USER
} from './actions';
export default function(state = undefined, action) {
switch (action.type) {
case FETCH_USER_INITIATE:
case FETCH_USER_COMPLETE:
case FETCH_USER_ERROR:
return {
...state,
...action.response,
error: action.error,
statusCode: action.statusCode
};
case CHANGE_USER:
return {
...state,
[action.path]: action.value
};
break;
default:
return state;
}
}// UserForm.jsx
import React from 'react';
class UserForm extends React.PureComponent {

constructor(props) {
super(props);

this.state = {
edit: false
};
}
componentDidMount() {
this.props.onFetch();
}

toggleEdit = () => {
this.setState({
edit: !this.state.edit
};
};
render() {
const {
user,
onChange
} = this.props;

return (
this.state.edit ?
<EntityData source={ user } onChange={ onChange }>
<h1>Profile</h1>

<EntityStringField label="Name" path="name" />
<EntityStringField label="E-mail" path="email" />
</EntityData>
:
<div className="user-form">
<h1>
Profile
(<a onClick={ this.toggleEdit }>Edit</a>)
</h1>

<label>Name:</label> { user.name }
<label>E-mail: { user.email }
</div>
);
}

}
const mapStateToProps = state => {
return {
user: state.user
};
}
const mapDispatchToProps = dispatch => {
onFetch: (...args) => dispatch(actions.fetch(...args)),
onChange: (...args) => dispatch(actions.change(...args))
};
export default connect(mapStateToProps, mapDispatchToProps)(UserForm);

This wasn’t really that bad! And it was ~140 lines of code. We have put the data into Redux, and with state controlling how the view work. Then some component logic to handle the user actions. And the forms are using input components as described in the first article to reduce the boilerplate of the form itself, probably reducing it to less than half the amount of code. Just an example of course, probably not how anyone would actually implement it. But for showing the difference.

Entity State

First, let’s pull out all the state that applies to the data around the component state. Since similar processes often repeat themselves, we can define a fixed data structure that contains some metadata needed to perform and display an interface for such processes. By defining the structure rather than making a custom one in each case that fits exactly with the actions you use there, you might get some extra metadata keys, but you can use tested, reusable code for all operations instead of large amounts of boilerplate that require extra maintenance.

Examples of metadata we need for such processes are:

  • The time at which the data was retrieved
  • Original data and modified values side by side
  • Flag indicating whether data is being retrieved or updated (to view spinners etc)
  • Error messages, individually for each field

This has led to a data structure that I call “Entity State”, which is a data set with associated metadata. For more details, see the entity-state README.

I have then added a number of helper functions for repetitive operations one does with such a dataset. Less maintenance, less errors. In addition, I have created a set of helper functions to perform HTTP requests, as it is also a common application data process, and can easily be standardized without compromising flexibility.

entity-state is framework-independent.

React Entity Data

Because we use React and Redux, and I have used this in several other projects as well, I have standardized the logic for this in a set of help functions and components in the library react-entity-data. This includes the wrapper component EntityData as described in the first article, including the possibility of nesting. It also includes Redux actions and reducers that make it easy to write code for these types of views in React applications.

I continue to develop both libraries as needs arise, to achieve the effects I have described here.

Reusing code for events

I will now use these two libraries to achieve the same things as in the example above, but also do the following that are omitted in the example:

  • Show which of the fields have changed (before pressing the save button)
  • Submit the changes back to the server

Maybe even with fewer lines of code! Tidiness, readability and flexibility is high priority. It should be easy to combine with different ways to implement everything you might need to handle data in an application. It is boiler plate code we want to avoid, we should not lock ourselves into one way to make all processes.

// actions.js
import { Http } from 'entity-state';
import { ReduxAC } from 'react-entity-data';
export const FETCH_USER = 'FETCH_USER';
export const TOGGLE_EDIT_USER = 'TOGGLE_EDIT_USER';
export const CHANGE_USER = 'CHANGE_USER';
export const UPDATE_USER = 'UPDATE_USER';
export const fetch = ReduxAC.httpRequest(
FETCH_USER,
() => Http.get('http://example.com/me'),
{
loading: true,
loadResponse: true
}
);
export const toggleEdit = ReduxAC.toggleMode(TOGGLE_EDIT_USER, 'edit');export const change = ReduxAC.stage(CHANGE_USER);export const update = ReduxAC.httpRequest(
UPDATE_USER,
user=> Http.put(`http://example.com/users/${user.id}`,
{
updating: true,
loadResponse: true,
clean: true
}
);
// reducers.js
import { EntityState, ReduxReducers } from 'react-entity-data';
import {
FETCH_USER,
TOGGLE_EDIT_USER,
CHANGE_USER,
UPDATE_USER
} from './actions';
export default ReduxReducers.createReducer(EntityState.initialize(), {
[FETCH_USER]: ReduxReducers.httpRequest,
[TOGGLE_EDIT_USER]: ReduxReducers.toggleMode,
[CHANGE_USER]: ReduxReducers.stage,
[UPDATE_USER]: ReduxReducers.httpRequest
});
// UserForm.jsx
class UserForm extends React.PureComponent {
render() {
const {
userState,
onToggleEdit,
onChange,
onUpdate
} = this.props;

return (
userState.mode === 'edit' ?
<EntityData state={ userState } onChange={ onChange }>
<h1>Profile</h1>

<EntityStringField label="Name" path="name" />
<EntityStringField label="E-mail" path="email" />
<button onClick={ onUpdate }>Save</button>
</EntityData>
:
<div className="user-form">
<h1>
Profile
(<a onClick={ onToggleEdit }>Edit</a>)
</h1>

<label>Name:</label> { user.name }
<label>E-mail: { user.email }
</div>
);
}

}
const mapStateToProps = state => {
return {
userState: state.user
};
}
const mapDispatchToProps = dispatch => {
onFetch: () => dispatch(actions.fetch()),
onToggleEdit: () => dispatch(actions.toggleEdit()),
onChange: (...args) => dispatch(actions.change(...args)),
onUpdate: (...args) => dispatch(actions.update(...args))
};
export default connect(mapStateToProps, mapDispatchToProps)(UserForm);

That’s ~90 lines. Wohoo !! Here’s what has changed:

  1. All actions are replaced with results from calling to ReduxAC

ReduxAC holds features that create action creators. That is, “Action creator creators” to be exact. Each action creator can then be called from another action to create more complex actions in each case. Like for example:

export const onSubmit = user => async dispatch => {  try {
await dispatch(update(user));
} catch(e) {
dispatch(ReduxAC.error(USER_ERROR)(e));
}
dispatch(toggleEdit());
}

2. The reducer is built with helper functions.

The reducer is built with a set of types that trigger a set of pre-fabricated reducers for such processes. Possibly if you don’t like the way with createReducer then the reducer can be written in the usual way like this:

const initialState = EntityState.initialize();export default function user(state = initialState, action) {  switch (action.type) {
case FETCH_USER:
case UPDATE_USER:
return ReduxReducers.httpRequest(state, action);
case CHANGE_USER:
return ReduxReducers.state(state, action);
}
};

Simple and flexible, because all the features of ReduxReducers are reducers themselves, ie they receive state and action and return new state. ReduxReducers.createReducer therefore creates a new reducer that runs the given reducers on the given types of actions and otherwise returns state untouched.

3. Edit state toggle is moved to Redux

Using the mode metadata from Entity State, it can easily be toggled between edit view and plain.

4. Update is made with the same reusable function for HTTP requests as the one used on fetch. This also contains a number of other checks and error handling which is not the first example.

Scaling

The data structure in the above example is a single flat object. But as all frontend coders know, all of the logic for both display and data processing can become significantly more complex and ugly when one begins to deal with more complex structures. For example, you have downloaded a list of customers from the server (an array of company objects), and are going to edit one of them. There are several ways to do this.

  1. Separate copy

You place the object to be edited as a copy somewhere else in the application state, relate to it individually, and after you finish updating it, you can make a refetch on the original list. This is a fairly common way to solve such a case. You can probably imagine all the extra logic this entails.

2. Editing based on index

When you start editing, you can place an index that says which element in the array is being edited, eg in a component state variable. This allows the viewer to display the form when you are in the iterate loop for the objects (in a list or the like), while the rest is displayed in the usual way. The logic for this can quickly become a bit ugly, and unless you create a key-value object, only one element can be edited at a time.

3. EntityData iterate

Because EntityData is based on React Context, there is a solution that allows you to manually traverse the objects in the display code, and can easily retrieve and modify the editing mode (and other metadata relevant for each object and not the whole array). Here’s a simplified example:

import { EntityDataContext } from 'react-entity-data';class CompanyItem extends React.PureComponent {  static contextType = EntityDataContext;  render() {
return this.context.mode === 'edit' ?
<form>
...
</form>
:
<div>
...
</div>
);
}
}
class CompanyList extends React.PureComponent { render() {
return (
<EntityData state={ companyListState } iterate>
{ company => <CompanyItem company={ company } /> }
</EntityData>
);
}
}

There is a lot going on between the lines here so you won’t have to repeat the logic for this everywhere. For example, an entity state while editing one of the elements of the array retrieved from server will look something like this:

{
data: [
{ id: 123, name: 'Company 1' }
{ id: 234, name: 'Company 2' }
],
pathMode: {
1: 'edit'
}
}

This indicates that the data at path 1 (which is the second company object) is being edited. This entity state object (including the rest of the metadata) is thus the object that EntityData receives through the state property. But because iterate is set, the render function in children will receive one company at a time, and be called for all objects in the data set. And they will provide EntityDataContext to subcomponents as if they were enclosed by an EntityData that had received this:

{
data: { id: 234, name: 'Company 2' }
mode: 'edit'
}

This eliminates the need to know which key to read, one can only retrieve mode right from context to find out if the element to which you relate is in edit mode or not.

Deep paths

The way to indent in the structures shown above with iterate can also be done by giving any path. For example, take a structure like this:

const example = {  data: {
id: 1,
name: 'Our company'
location: {
address: 'The streets',
city: {
id: 45,
name: 'Example city
}
}
}
}

Here an EntityData context for forms or other views can be done this way:

<EntityData state={ example }>  <h1>Company</h1>   <EntityStringField label="Company name" path="name" />  <h1>Location</h1>

<EntityData path="location.city">
...
</EntityData>
</EntityData>

Everything within the innermost EntityData component will receive a state (read from the outer component through context, so you don’t have to provide it the state data) that only has the city object as data, and all path metadata in the context will be received as if this was the whole state.

Additional metadata provided to components wrapped with withEntityData

Similar to what was described in the first article, EntityData provide components wrapped with withEntityData with what is relevant for that path, so you don’t have to provide every single Input component with error, mode and all other path-based metadata from the state. Like in this example:

const state = {
data: {
name: 'John Doe',
email: 'john@doe.com'
},
pathChange: {
email: 'john@doe.email'
}
};
function StringField({ value, changed, error }) {
return (...);
}
const EntityStringField = withEntityData(StringField);function UserForm({ user }) {
return (
<EntityData state={ state }>
<EntityStringField label="Name" path="name" />
<EntityStringField label="E-mail" path="email" />
</EntityData>
);
}

If the component (StringField in the pseudo-example above) have features for indicating that a field value was changed, or that there is an error, these props (along with a few others) will be provided to the component because they are inside an EntityData component, for the given path. So in that case, the email field will be indicated as changed, something similar to what is shown in this screenshot:

Validation

I have also included the possibility of validating the data with Json Schema, and that the validation function inserts path errors in entity state so that the validation errors can be displayed directly under each field. Reusable field components (similar to the previous article example) can then display the error message for each field without having to receive any additional props from the form itself.

The goal of this standardization of data processing and logic surrounding them is to make the code simpler and reduce errors without limiting flexibility. At the time of writing, the libraries cover quite a few basic functions for data handling, and more will probably be added over time. Let’s work for the three S’s: Standardization, Scalability and Simplification!

--

--