Will Goto

@wrgoto

Using Models with React

Introducing React Axiom

November 16, 2016 edits reflect API changes since the original publishing of this article.

At some point in a React application, describing state changes for application data becomes difficult at individual component levels and calls for a clean abstraction between business logic and presentational components. Redux is certainly an option at this point, but suppose the tradeoffs that Redux provides are unfavorable?

React Axiom is a lightweight (~12kb) way to use models with the React component tree. A basic React Axiom model looks like the following:

class ListItemModel extends ReactAxiom.Model {
  static defaultState() {
return {
id: null,
description: '',
completed: false
};
}
}

Model stores the argument object in this.state and automatically creates getter and setter functions: getId, setId, hasId for the id property, getDescription, setDescription, hasDescription for the description property, and isCompleted, setCompleted, hasCompleted for the completed property (note: this is different due to the completed property being a boolean). Defining a method of the same name on the class overwrites the getter or setter:

class ListItemModel extends ReactAxiom.Model {
  static defaultState() {
return {
id: null,
description: '',
completed: false
};
}
  getDescription() {
return this.state.description.toLowerCase();
}
}

When a React Axiom model is passed into a component, the component listens to state changes within the model and updates itself. The following is an example of a React component using a model passed as listItem below:

class ListItemComponent extends React.Component {
  render() {
const { listItem } = this.props;
return (
<li>
{listItem.getDescription()}
{listItem.isCompleted() ? null : this.renderButton()}
</li>
);
}
  renderButton() {
const { listItem } = this.props;
return (
<button onClick={() => listItem.setCompleted(true)}>
complete
</button>
);
}
}

Notice how the component calls setCompleted on the listItem model to update state. To put everything together:

const listItem = new ListItemModel({
id: '1',
description: 'Teach mom how to use Slack'
});
const ListItemSubscriber = ReactAxiom.subscribe(ListItemComponent);
ReactDOM.render(
<ListItemSubscriber listItem={listItem} />,
document.getElementById('app')
);

The higher order subscribe function wraps the ListItemComponent and returns a new ListItemSubscriber component. The ListItemSubscriber component will then subscribe to the listItem model and update itself if state changes. In the specific above example, clicking on the complete button will cause the button to disappear.

Getting Complex with References

State changes in React Axiom models occur through mutations. As a result, this design allows references to other objects, arrays, and models to operate fairly well in state. The following example adds a dependencies field to ListItemModel and some additional logic to complete a list item.

class ListItemComponent extends ReactAxiom.Model {
  static defaultState() {
return {
id: null,
description: '',
completed: false,
dependencies: [],
};
}
  complete() {
this.getDependencies().forEach(dependency => {
dependency.complete();
});
    this.setCompleted(true);
}
}

To recursively render list items, the ListItemComponent component can render the ListItemSubscriber component for each dependency:

class ListItemComponent extends React.Component {
  render() {
const { listItem } = this.props;
return (
<li>
{listItem.getDescription()}
{listItem.isCompleted() ? null : this.renderButton()}
<ul>
{this.renderDependencies()}
</ul>
</li>
);
}
  renderButton() {
const { listItem } = this.props;
return (
<button onClick={() => listItem.complete()}>
complete
</button>
);
}
  renderDependencies() {
const { listItem } = this.props;
return listItem.getDependencies().map(dependency => (
<ListItemSubscriber listItem={dependency} />
));
}
}
const ListItemSubscriber = ReactAxiom.subscribe(ListItemComponent);

Now, clicking on a complete button will mutate that model’s completed state value and all of its dependencies’ completed state value. In addition, any component that subscribed to any of the mutated list items will update and render the correct state.

Use with Server Rendering

One particular drawback with React Axiom is that the data store requires model names to be passed as part of the transferrable data. This is fine for server-to-client data transfer, however it is less flexible in terms of snapshotting and persisting state to a local storage should the model names change in any way. The following is an example of serializing and parsing models and model data with the React Axiom Store:

const listItem1 = new ListItemModel({
id: '1',
description: 'Teach mom how to use Slack'
});
const listItem2 = new ListItemModel({
id: '2',
description: 'Meditate',
dependencies: [listItem1]
});
ReactAxiom.Store.setModelRefs([ListItemModel]);
const serverStore = new ReactAxiom.Store({
title: 'Things to do',
createdAt: Date.now(),
listItems: [listItem1, listItem2]
});
const json = serverStore.stringify();
// Transfer the data to the client
// to hydrate the client store, and
// reinitialize the application.
ReactAxiom.Store.setModelRefs([ListItemModel]);
const clientStore = new ReactAxiom.Store();
clientStore.parse(json);

Notice how listItem2 contains a reference to listItem1 on the server store. By passing in classes to initialize the client store, it is able to rebuild this reference from the provided JSON string. In other words, listItem2.getDependences()[0] === listItem1 on clientStore.

When to Use Models

Like any design, there are tradeoffs. Redux, for example, requires plain objects to describe changes in the system and pure functions to describe the logic for handling those changes (what a lot of developers refer to as “boilerplate”) but gains a more flexible data store. React Axiom requires models to describe the changes in the system and describe the logic for handling those changes, but gains the following advantages:

  • Organizing and stubbing business logic becomes easier.
  • Interfaces between views, models, and data become more flexible.
  • Writing semantic and transparent code becomes easier.

For someone new to the React ecosystem, React Axiom might make a little bit more sense than Redux, but like any framework or design that is opinionated, only use it if solves a particular problem or fits the situation.

What is Next?

React Axiom is still in its infancy stage. I welcome any help to make this design paradigm a robust solution for large scale applications for the JavaScript community. Please feel free to get involved by checking out the GitHub repository or contacting me on Twitter. 🙏🏽

More by Will Goto

Topics of interest

More Related Stories