At Bitovi, we work with React on lots of client projects, and we like its straightforward API and using JSX for templates. Redux is common in the React community, and we know it’s not the right solution for every project.
For us, Redux doesn’t work well with our development workflow. We build apps as individual modlets that act as mini apps in and of themselves, that are assembled to make the larger app. Redux’s global store feels more like a monolithic meatball that controls the entire app.
We believe independently-testable ViewModels makes for more maintainable code. Last but not least, implementing lifecycle methods such as shouldComponentUpdate
in class components feels like a step backward because we’re used to CanJS intelligently updating the DOM when necessary.
To solve all of these problems, we decided to bring the best part of CanJS to React: its observables! Using react-view-model lets you create observable ViewModels for React components.
How to use react-view-model
Let’s look at an example of a functional React component that has a ViewModel.
First, we’ll import can-define to create an observable ViewModel:
import DefineMap from 'can-define/map/map';
export const ViewModel = DefineMap.extend({
count: 'number',
increment: function() {
return this.count++;
},
});
Each instance of our ViewModel
type will have a count
property and an increment
method. Notice how this ViewModel type is separate from our component code; we’ll discuss testing this code on its own in a little bit.
Next, we’ll import react, react-dom, and react-view-model, then pass react-view-model the ViewModel
type and a render function that uses the viewModel
instance passed to it:
import ReactDOM from 'react-dom';
import reactViewModel from 'react-view-model';
import { ViewModel } from './view-model.js';
const AppComponent = reactViewModel(ViewModel, (viewModel) => {
return (
<div onClick={ () => viewModel.increment() }>
Count: {viewModel.count} (Click Me)
</div>
);
});
Lastly, we’ll add our component to the DOM, which will create an instance of the view model and call the render function:
ReactDOM.render(
<AppComponent count={0} />,
document.body
);
With the component rendered to the DOM, we can click on the text and see the count increment. Whenever something that’s bound to in the view-model changes, the render function will be called again.
One of the beautiful things about this pattern is how our ViewModel is separated from the component code. This lets React focus on what it’s good at—the View—and give us the opportunity to create nice, testable instances of the ViewModel.
Testable ViewModels
With our ViewModel code separate from our component code, we can test it independently and reuse it across components.
import DefineMap from 'can-define/map/map';
import QUnit from 'qunitjs';
import { ViewModel } from './view-model.js';
QUnit.test('ViewModel increment', function( assert ) {
const viewModel = new ViewModel({count: 1});
assert.equal(viewModel.count, 1, 'Starting count is correct');
viewModel.increment();
assert.equal(viewModel.count, 2, 'Increment works');
});
In the example above, we have the same ViewModel
as before, and now we can unit test its functionality without having to import React or render our component to the DOM.
Further reading
We’ve barely scratched the surface of what’s possible when CanJS’s observables are paired with React. The react-view-model documentation has more info about this powerful combination.
Let us know on Twitter, Gitter, or the CanJS forums if you experiment with this new package. We also do React consulting and would be happy to help your team with your app or lead a training. We’re excited about the new possibilities and eager to hear about how you’re using them!