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.
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.
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
.
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:
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.
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. 🙏🏽