ViewModel for React Alpha

I’ve been working on a React version of ViewModel for a while and I’m very close to being done. Most of what’s left is documentation so I figure I’d share it as it is right now. Documentation isn’t very exciting so seeing people use it is an incentive to finish it.

What is this?
ViewModel for React is a thin layer on top of React to work with as little boilerplate and ceremonies as possible.

Who is this for?
If you like the “view model” paradigm (Angular, Knockout, Aurelia, etc.) then you’ll love ViewModel. If you like to do things “the react way” (are you plugged to the Twitter feeds?) then you’ll probably hate ViewModel.

Why React?
React does less than other frameworks so it’s easier to take JSX and build a DSL on top of it (removing all the boilerplate).

Will this pattern (view models) work for larger applications?
I’ll take the lazy route and just say this is the same way of developing applications as with Angular, Knockout, Aurelia, and a plethora of other frameworks. If it’s good enough for Microsoft and Google, I’m pretty sure it’s good enough for you.

Can I use it outside Meteor?
Absolutely, you can use it in any React project, Meteor or not.

Show me the code!
Okay, here’s a hello world component with ViewModel:

Hello({
  render(){
    <h1>Hello World</h1>
  }
});

Aren’t you missing a few things in there?
Nope, that’s all it takes to create a component.

Here’s the classic echo component from Angular:

Echo({
  message: '',
  render() {
    <div>
      <input b="value: message" />
      <label b="text: message"></label>
    </div>
  }
});

Save for a few exceptions it behaves pretty much the same as ViewModel for Blaze (but with JSX).

Setting it up.

Short version (if you have a React project):
Add viewmodel-react to your package dependencies
Add viewmodel-react-plugin to devDependencies
Add a .babelrc file with { "plugins": [ "viewmodel-react-plugin" ] }.

Longer version (starting from scratch)
As with any Meteor application you can use React exclusively or mix Blaze and React. You can even use ViewModel for Blaze and ViewModel for React in the same application. To keep things simple I’ll just use React.

meteor create demo
cd demo
npm install --save react react-dom viewmodel-react
npm install --save-dev viewmodel-react-plugin
meteor remove blaze-html-templates
meteor add static-html

Add a .babelrc file with

{ "plugins": [ "viewmodel-react-plugin" ] }

Change client/main.js to:

import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker'
import React from 'react';
import { render } from 'react-dom';
import { App } from '../imports/App';
import ViewModel from 'viewmodel-react';

// Use Meteor's dependency management
ViewModel.Tracker = Tracker; 

Meteor.startup(() => {
  render(<App />, document.getElementById('app'));
});

Change client/main.html to:

<body>
    <div id="app"></div>
</body>

Create the folder imports and then the file imports/App.js:

App({
  render(){
    <h1>Hello World</h1>
  }
});

You now have a hello world application with ViewModel. Let’s add a sub component:

Create the file imports/Person/Person.js:

Person({
  render() {
    <h2>Hi Person</h2>
  }
});

And modify imports/App.js to use Person:

App({
  render(){
    <div>
      <h1>Hello World</h1>
      <Person />
    </div>  
  }
});

Check out the (still in progress) documentation at: https://viewmodel.org/react

12 Likes

Nice, just waited for it :grinning:. Just one question: Is it possible to use the class syntax instead of the object syntax, f.e something like

class MyComponent extends ViewModel {}

I don’t think that’s going to be possible. the babel plugin just adds the missing pieces to make it a valid react component so there isn’t a master component (so to speak).

I think we’ve had this conversation before (about the Blaze version). what’s the problem you’re trying to solve?

1 Like

Oh, I think it’s more about personal preferences since React.createClass was replaced with the current ES6 syntax.

Some issue I had with the Blaze ViewModel was that I needed to create one ViewModel for 2 templates (web + mobile), but the second ViewModel had some different methods. Normally, I would write something like this:

class MobileVersion extends WebVersion

…while WebVersion extends from ViewModel. Without that syntax I have to go the old way - define an object, export and import it and write something like:

import obj from './obj';

_.extend(obj,{ ... overwrite some methods and properties .... });
MobileVersion(obj);

But wait, what about super() if I overwrite one method but also need to call the old one? I know that it is possible but the way is not so clean like the ES6 syntax.

One thing I miss within the doc is how I’ve to set the initial state. Do I have to use all methods of React.createClass like getInitialState() or is using React’s state in ViewModel an anti pattern? I’m just asking because on the Blaze version we wrote some variables to represent it’s state and it seems like it’s the same on the React version:

App({
    isVisible:false,
    name:"",
    age:0

    render() { ... }
 });

Normally, we would add all 3 variables to React’s initial state.

About classes, ViewModel for React is split in two: a transpiler and the runtime.

So when you write:

Contacts({
  return() {
    <div />
  }
})

It transpiles into:

import React from 'react';

export class Contacts extends React.Component {
  render() {
    return (
      <div />
    );
  }
}

About extending, it depends on what you’re trying to do. If you just want to override properties and methods you can do something like:

import vmBasedOnEnv from './vmObject';

TheComponent({
  load: vmBasedOnEnv
  render() {
    <div></div>
  }
})

Here’s 1 view model used by 2 “templates”:

import vmObject from './vmObject';

WebVersion({
  load: vmObject
  // Override properties and methods as needed
  render() {
    <div>Web</div>
  }
})

MobileVersion({
  load: vmObject
  // Override properties and methods as needed
  render() {
    <div>Mobile</div>
  }
})

Finally, ViewModel handles the state for you so you don’t need to worry about getInitialState. Just use properties like you’re used to.

Yeah, but what about calling the parent method and overriding it. In this case we would use super(). I think the main problem is that the React VM uses the syntax we’ve used for the Blaze VM. This may be good for Meteor users, but I don’t think that’s the case in the React world.

So I don’t know how I would tell a React user (someone who doesn’t use Meteor) the advantages of this code:

MyComp({
 firstName: 'John',
 lastName: 'Doe',
 getFullName() {
 return this.firstName() + " "+ this.lastName(); 
}

render() {
return <div>{this.getFullName()}</div>
}

});

The first question would be: Why do I have to call variables as methods? As a Meteor/Blaze user I know the reason, but in React it feels unnatural.

I don’t think that’s the case in the React world

This isn’t for people plugged to the Twitter and blog feeds, I don’t think I can convince any of them to even try it.

This is for people who care more about getting things done in a simple way. To them I’d just ask them to build something small with ViewModel and then do the same with whatever the React way is at the moment.

how I would tell a React user (someone who doesn’t use Meteor) the advantages of this

No Flux/Reflux/Redux/Delux pattern to learn?

I guess it comes down to what the person thinks is simple. To me that component is simple and “easy to reason about”. I don’t have to worry about wiring the application so that each component can get the state they need.

Why do I have to call variables as methods? As a Meteor/Blaze user I know the reason, but in React it feels unnatural.

Actually, it’s unnatural to anyone (including Meteor users). My answer would be “properties are transformed into functions because that allows very convenient things like sharing properties, passing them around by reference, each can carry its own validation, reset, etc. The advantages far outweigh the small quirk”.

1 Like

Congrats it’s finally out!

I am almost taking full control of Meteor / Redux / React after a few months, but it’s really mentally stressful to handle the heavy code boiler plate and noises.

Quick questions out of my head now:

  • Is React VM to include all functionalities of Meteor VM ? If not, what will be missing or impossible ?
  • Could React VM components be used from other React components and vice versa ? (I know it’s stupid, just asking to confirm)

I am sure to have much more questions to ask later :slight_smile:

I’m curious to learn: how does autorun work, if the App is not on Meteor ? Someone seemed to have isolated Tracker some time ago…

Is React VM to include all functionalities of Meteor VM ?

All batteries are included. There are minor changes in the way you do things because React doesn’t have the same scope rules as Blaze but it’s pretty much the same experience.

Could React VM components be used from other React components and vice versa ?

Yep, you can mix and match.

I’m curious to learn: how does autorun work, if the App is not on Meteor ?

I copied Tracker from Meteor and plugged it into ViewModel for React. When a dependency changes ViewModel signals to React that it should do a render. You don’t have to worry about shouldComponentUpdate. Only the components depending on the change will return true from that function.

3 Likes

A bit more technical question. In original JSX, I could have e.g. the following code for dynamically generating components:

{
   React.createElement( 
        myComponentMap[options.contentType],
        {options: options}
  )
}

What’s the syntax or how to use the “b” binding in createElement ?

Unfortunately that’s not going to be possible without breaking SSR. When you write <p b="if: show">Hi</p> the babel plugin transpiles it into { this.show() ? <p>Hi</p> : null }. The transpiler can’t do that with myComponentMap =(

1 Like

So if I got that correctly, b is just for VM flavoured JSX, but for React direct API like createElement, I could still access VM properties to manage dynamic children. Just without the binding. Correct ?

Echo({
  contentType: 'A',
  options: {},
  message: '',
  render() {
    <div>
      <input b="value: message" />
      <label b="text: message"></label>
     {
       React.createElement(myComponentMap[this.contentType(), {options: this.options() });
     }
    </div>
  }
});

That’s right. You can use any React/JSX code with a “VM” component.

2 Likes

I’m reading the VM doc.

Re the example of the Share Demo, from React’s point of view, the Person component has address state from VM share, and has name props passing down from People. React’s props are said to be immutable. Now the example is binding name to one of Person’s input. What does that mean? When the input value changes, does the name props mutate ?

It works the same as VM Blaze when you pass a parameter to a child component. The initial value is immutable but the component copies the value into its own state. After that the component is free to modify its own state.

2 Likes

This is super neat! I love the idea of using transpilation to make boilerplate more concise. It’s something that could be used in a lot of places I think :]

Hopefully one day we can add a GraphQL query somewhere in there and get automatic data loading as well!

3 Likes

Thanks for your hardwork manuel, viewmodel has been the best component ive worked with in meteor.

1 Like

Hi @manuel I followed the Longer Version (starting from scratch) example. Meteor runs ok, but the App component is not rendered as I get this error in the browser console:

Uncaught ReferenceError: App is not defined   
 install.js:153
    App.js                                     @ App.js:1
    fileEvaluate                            @ install.js:153
    require                                    @ install.js:82
    Mp.import                               @ runtime.js:70
    meteorInstall.client.main.js     @ main.js:1
    fileEvaluate                             @ install.js:153
    require                                    @ install.js:82
    (anonymous function)            @ app.js?hash=3dccb47…:48

So I’m not even seeing the Hello World string…

It has to be something small but I’ve scoured it. Is it a 1.4 thing ?

I guess you’re the first one to try it =)

You need to add a .babelrc file to the root:

{ "plugins": [ "viewmodel-react-plugin" ] }

That was my bad, I just updated the longer version.

2 Likes