NAV
React-MVx 2.0
React MVVM framework

Getting started

Overview

React-MVx is the MVVM JS application framework built upon three technologies:

Contrary to the popular React approaches, React-MVx does not try to avoid the distributed mutable application state. Instead, it is focused on bringing stateful components capabilities to their maximum.

React-MVx is built around the idea of universal state management featuring the same technique to manage the local component state, application page state, and the global application state. Core building blocks of the application architecture are:

React-MVx Component uses the Record class to manage its local state. Record can consists of other records and collections, describing the data structure of arbitrary complexity. All records are serializable by default, has deeply observable changes, and supports the declarative validation. The behavior of record's attributes and component state/props elements is controlled with declarative type annotations.

React-MVx extends React namespace and should be used instead of react. All class Component definitions must be preceeded with the @define decorator.

import React, { define } from 'react-mvx'

@define class HelloApp extends React.Component {
    static state = {
        count : 0
    };

    render(){
        const { state } = this;
        return (
            <h1 onClick={ () => state.count++ }>
                Hi there! { state.count }
            </h1>;
        );
    }
}

Key features

Examples

Babel + Webpack boilerplate

The place to start your project.

Checklist tree

Hierarchical checklist with simple group selection rules. Demonstrates:

TodoMVC

Classic TodoMVC example.

UsersList

Editable list of users. Demonstrates the state synchronization pattern.

Flux Comparison

Detailed design of the "flux comparison" example with explanation of the unidirectional data flow.

Installation & Requirements

Supported browsers: Chrome, Firefox, Safari, IE10+.

Requires react, prop-types, and type-r as peer dependencies. Installation (assuming that React is installed):

npm install react react-mvx type-r prop-types --save-dev

The best way to start is to use the boilerplate which already has babel, webpack, and the minimal set of required dependencies configured.

TypeScript is unsupported (yet) but may work.

Developer tools

The modified version of the React Developer Tools with fixes required for React-MVx is available in the Chrome Web Store.

Design patterns

Stateful Component

Inline state declaration

@define class Test extends React.Component {
    static state = {
        counter : 0
    }

    render(){
        const { state } = this;
        return (
            <div onClick={ () => state.counter++ }>
                { state.counter }
            </div>
        );
    }
}

External state declaration

@define class TestState extends Record {
    static attributes = {
        counter : 0
    }
}

@define class Test extends React.Component {
    static State = TestState;

    render(){
        const { state } = this;
        return (
            <div onClick={ () => state.counter++ }>
                { state.counter }
            </div>
        );
    }
}

Transactional state changes

@define class Test extends React.Component {
    static state = {
        counter1 : 0,
        counter2 : 0
    }

    onClick = () => {
        this.state.transaction( state => {
            state.counter1++;
            state.counter2++;
        });
    }

    render(){
        const { state } = this;
        return (
            <div onClick={ this.onClick }>
                { state.counter1 + state.counter2 }
            </div>
        );
    }
}

I/O

Persistent UI state

@define class TestState extends Record {
    static endpoint = localStorageIO( 'myApp' );

    static attributes = {
        id : 'TestPage'
        counter : 0
    }
}

@define class TestPage extends React.Component {
    static State = TestState;

    componentWillMount(){
        this.state.fetch();
    }

    componentWillUnmount(){
        this.state.save();
    }

    render(){
        const { state } = this;
        return (
            <div onClick={ () => state.counter++ }>
                { state.counter }
            </div>
        );
    }
}

Initial loading indicator...

@define class Page extends React.Component {
    static state = {
        someCollection : Collection,
        loading : true
    }

    onLoad = () => this.state.loading = false;

    componentWillMount(){
        this.state.collection.fetch()
            .then( onLoad ).catch( onLoad );
    }

    render(){
        const { state } = this;
        return state.loading ? (
            <div>Loading...</div>
        ) : (
            <div>
                Loaded { state.someCollection.length } items.
            </div>
        );
    }
}

Component's loading indicator...

@define class List extends React.Component {
    static props = {
        collection : Collection
    }

    render(){
        const { collection } = this.props;
        return collection.hasPendingIO() ? (
            <div>Loading...</div>
        ) : (
            <div>
                Collection has { collection.length } items.
            </div>
        );
    }
}

Fetching multiple objects...

@define class TestState extends Record {
    static endpoint = attributesIO();

    static attributes = {
        coll1 : Some.Collection,
        coll2 : Other.Collection
    }
}

@define class TestPage extends React.Component {
    static State = TestState;

    componentWillMount(){
        this.state.fetch();
    }

    render(){
        //...
    }
}

Application Examples

Data binding and forms

Tutorial: two-way data binding and forms

In this tutorial, we will learn the state management and two-way data binding basics on the example of the simple user info editing form.

Application, sources.

Input Validation

Tutorial: State Definition and Form Validation

In this tutorial, we will add input validation to the user editing form from the previous example. That’s the client-side “as-you-type” validation preventing the user from submitting invalid data while giving him hints.

Application, sources.

Editable lists

Illustrates state synchronization pattern.

Application, sources.

id-references and stores

Demonstrates local stores and id-references.

Applicationm sources

Component

React-MVx extends React namespace and should be used instead of react. All class Component definitions must be preceeded with the @define decorator.

All features of the Component are controlled through the unified property and attribute declarations.

import React, { define } from 'react-mvx'

@define class Hello extends React.Component {
    static props = { // instead of `propTypes` and `defaultProps`
        propName : `propDef`,
        ...
    }

    static context = { // instead of `contextTypes`
        propName : `propDef`,
        ...
    }

    static childContext = { // instead of `childContextTypes`
        propName : `propDef`,
        ...
    }

    static state = { // instead of "this.state = " in the constructor.
        attrName : `attrDef`,
        ...
    }

    static store = { // store
        attrName : `attrDef`,
        ...
    }

    render(){...}
}

Type annotations summary

The majority of React-MVx features are controlled with declarative props, state, store, and context type annotations.

all (state/store/props/context)

Type annotations below represents the run-time type assertions.

Annotation Description
Ctor element has specified type
Ctor.isRequired element is required
Ctor.has.check(...) custom validation check

state, store, props

You can specify the default value for an attribute or prop, and reactions on its change.

Annotation Description
Ctor.value( defaultValue ) element has default value
defaultValue element has default value
Ctor.has.watcher(...) custom reaction on element's change
Ctor.has.events(...) listen to custom events from the element
Ctor.has.changeEvents(...) update on element's changes

state, store

You have an an attribute-level control of the serialization and ownership for the state, store, and records attributes.

Annotation Description
Record.shared attribute holds the reference to the record
Collection.Refs collection of references to the records
Record.from(...) reference to the record from the specified collection
Collection.subsetOf(...) collection of references to the records from the specified collection
Ctor.has.parse(...) custom parse hook
Ctor.has.toJSON(...) custom serialization hook
Ctor.has.get(...) attribute read hook
Ctor.has.set(...) attribute write hook
Ctor.has.metadata(...) attach custom metadata to the attribute

props

Component static props declaration replaces standard React's propTypes and defaultProps.

static props = { name : propDef, ... }

Declare component props. Declaration is an object

import React, { define } from 'react-mvx'

@define class PropsDemo extends React.Component {
    static props = {
        name : Declaration,
        ...
    }
}

static pureRender = true

Prevents subsequent render calls in case if props were unchanged. It's known as "pure render" optimization.

Inner changes of records and collections are detected and taken into account. Thus, it works properly with mutable records and collections.

static props declaration is required for pureRender to work. Only declared props will be tracked and compared.

propDef name : Constructor

Checks if component prop is an instance of the Constructor and puts the warning to the console if the prop type is not compatible.

propDef name : Constructor.isRequired

Mark property as required.

import React, { define } from 'react-mvx'
@define class PropsDemo extends React.Component {
    static props = {
        // Simple form of annotation is just the constructor function designating the type.
        optionalArray: Array,
        optionalBool: Boolean,
        optionalFunc: Function,
        optionalNumber: Number,
        optionalObject: Object,
        optionalString: String,
        optionalSymbol: Symbol,   

        // Anything that can be rendered: numbers, strings, elements or an array
        // (or fragment) containing these types.
        optionalNode: React.Node,

        // A React element.
        optionalElement: React.Element,

        // You can also declare that a prop is an instance of a class.
        optionalMessage: Message,

        // You can chain any of the above with `isRequired` to make sure a warning
        // is shown if the prop isn't provided.
        requiredFunction : Function.isRequired
    };
    ...
}

propDef name : Constructor.value( defaultValue )

Assign default property value.

propDef name : defaultValue

Assign default property value. The the type will be inferred from the defaultValue.

Any function in props annotation is treated as a constructor. Thus, Function.value( defaultValue ) must be used to specify the defaults for functions.

import React, { define } from 'react-mvx'

@define class PropsDemo extends React.Component {
    static props = {
        withDefault : String.value( 'defaultValue' ),
        anotherDefault : 'defaultValue'
    }
}

propDef name : Constructor.has.watcher( 'componentMethodName' )

propDef name : Constructor.has.watcher( function( newValue, name ){ ... } )

Watcher is the function which is called when the particular prop is assigned with new value.

Watcher is called after componentWillMount, and may be called during componentWillReceiveProps if the property is changed. Watcher is executed in the context of the component.

import React, { define } from 'react-mvx'

@define class PropsDemo extends React.Component {
    static props = {
        first : String.has.watcher( 'onFirstChange' ),
        second : Number.has.watcher( second => console.log( 'Received new prop:', second ) )
    }

    onFirstChange( newValue, name ){
        console.log( 'First prop is about to change:', newValue );
    }
}

propDef name : RecordOrCollection.has.changeEvents( true )

Observe internal changes of the record or collection and update the component in case of changes.

propDef name : EventSource.has.changeEvents( 'event1 event2 ...' )

Update the component in case if property triggers any of the listed events.

import React, { define } from 'react-mvx'
import { Record } from 'react-mvx'

@define class PropsDemo extends React.Component {
    static props = {
        // Render on every change
        trackInnerChanges : Record.has.changeEvents( true ),
        // Render when record is added to or removed from the collection
        anotherDefault : Collection.has.changeEvents( 'add remove' )
    }
}

propDef name : EventSource.has.events({ event : handler, ... })

Subscribe for events from the component property. handler can either be the name of the component's method, or the function handling the event. Handler is executed in the context of the component.

state

Component's state is modeled as Type-R Record. Record is created before the call to componentWillMount() and disposed after the call to componentWillUnmount().

All changed inside of the state record are observed, and the component is updated in case of change.

static State = RecordConstructor

Define stateful component with the state Record declared externally.

static state = { name : attrDef, ... }

Inline definition of the state record with a given attributes declaration. All declarations working on props works for the state as well. Refer to the Record documentation for the attributes declaration syntax.

component.state

Holds an object of the Record subclass.

Do not use component.setState(). Use direct assignments to modify the state

this.state.x = 5;

Refer to the Record documentation for the complete list of methods.

component.transaction( fun )

Group the sequence of state (and props) updates in the single transaction leading to single UI update. Read more about transactions in Record's manual.

this.transaction( state => {
    state.a++;
    state.b++;
    this.props.collection.reset();
});

store

Stores in Type-R are internally similar to the Record and used to resolve one-to-many and many-to-many relationships by id. Stores must not be used to store UI state; they are intended to hold the shared domain state which is cross-referenced by id.

There may be multiple stores in Rect-MVx. There is the single default store (Store.global) which is used to cache the data that must be accessible across the pages.

Specifying the store for the top-level component sets this store as the primary one for all the internal state of the current component subtree.

static store = existingStore

Expose the existingStore to the component subtree. Update component on store changes.

static store = StoreConstructor

Creates the local store on component's mount and dispose it when component is unmounted.

Expose the store to the component subtree. Update component on store changes.

static store = { attrName : attrDef, ... }

Implicitly create the Store subclass from the given attribute spec.

Accepts the same attrDef as the state and Record.

component.store

When the static store is defined, provide the access to the store in component.

Store is not directly accessible to the subcomponents; you have to pass values down as props.

context

Static context and childContext declarations replaces React's standard contextTypes and childContextTypes.

static context = { name : propDef, ... }

Replacement for standard contextTypes.

static childContext = { name : propDef, ... }

Replacement for standard childContextTypes.

getChildContext() function is required to create the context as in raw React.

propDef name : Constructor

Checks whenever the value is an instance of the Constructor and puts the warning to the console if the prop type is not compatible.

propDef name : Constructor.isRequired

Value is required.

Methods

component.linkAt( 'key' )

Create the link for the state member key. Same as component.state.linkAt( 'key' ).

component.linkAll()

Create links for all (or specified) the state members. Same as component.state.linkAll().

Direct access to the links cache. Can be used in event handlers to access the links created during the last render().

All links created for records (and for the component's state) are being cached. They are recreated only in case when their value has been changed.

component.asyncUpdate()

Safe version of the forceUpdate(). Gracefully handles component disposal and UI update transactions.

Shall be used in place of every manual call to forceUpdate().

Events mixin (7)

Component implements Events interface from the Type-R framework, thus it's able to trigger and subscribe for events.

Link

Overview

Link is an intermediate object used to implement the two-way data binding. It acts like a transport for the value, the callback to modify it, its validation error, abstracting out UI controls from the data representation in the state container and from the state container per se.

Links to the record's attributes are commonly created with linkAt and linkAll methods of records. Record is automatically validated upon the links creation, and link encloses the validation error related to the particuler attribute.

// Data bound control for the semantic form markup
const Input = ({ valueLink, ...props }) => (
    <div className={`form-control ${ valueLink.error ? 'error' : '' }`}>
        <input {...props}
            value={ valueLink.value }
            onChange={ e => valueLink.set( e.target.value ) }
        />
        <div className="validation-error">{ valueLink.error || '' }</div>
    </div>
);

Links makes it possible to create the semantic markup for the form elements, encapsulating the all the required styles and validation error indication.

Here are the most important link members. All the link members are read-only and should not be modified directly.

// Link's shape
{
    value : /* the value */,
    set( newValue ){ /* the function to change it */},
    error : /* validation error */
}

Holds linked value. This value is immutable.

Holds the validation error (typically the text error message) which might be consumed and displayed by data-bound countrol.

An error is populated automatically on link creation when using linkAt() or linkAll() methods, and is produced by declarative validators from .has.check() attributes annotations.

Tells the state container to update the value.

<button onClick={ () => boolLink.set( !boolLink.value ) } />

Update link value using the given value transform function.

<button onClick={ () => boolLink.update( x => !x ) } />

All records and collections may create links to its elements. Component has the shorthand methods to create links to its state record elements.

You can create custom link object encapsulating complex data binding logic with Link.value.

record.linkAt( attr )

Create the link to the record's attribute. Semantically it's the reference to the attribute.

const nameLink = user.linkAt( 'name' );

collection.linkAt( prop )

Create the link to the custom collection property. Property's setter must modify some record's attributes or change the collection.

component.linkAt( key )

Create the link to the attribute of the conponent's state. Works similar to component.state.linkAt( key ).

record.linkAll()

Link all (or listed) records' attributes.

// Link all attributes...
const { name, email, age } = user.linkAll();

// Link specified attributes...
const { name, email } = user.linkAll( 'name', 'email' );

component.linkAll()

Link all (or listed) attributes of the component's state. Works similar to component.state.linkAll().

collection.linkContains( record )

Create the boolean link which is true whenever the record is contained in the collection.

Setting the link to false will remove record from the collection, setting it to true will add it.

Create custom link with the given value and set function. Use the link.check method to populate the validation error.

Create boolean equality link which value is true whenever link.value === value, and false otherwise.

When an equality link is assigned with true the parent link value is set with value, and with null otherwise.

<Checkbox checkedLink={ stringLink.equals( 'optionX' ) } />
<Checkbox checkedLink={ stringLink.equals( 'optionY' ) } />

Useful for radio groups.

Converts the link to the standard { value, onChange } props to be easily consumed by standard <input /> control.

<input type="text" {...nameLink.props} />

Convert the link to the UI event handler event => void which will transform the link using the given function.

link.action takes transform function, and produce a new function which takes single event argument. When it's called, event and link value are passes as transform parameters, and link will be updated with returned value.

// simple click event handler...
<button onClick={ boolLink.action( x => !x ) } />

// manual binding to input control:
const setValue = ( x, e ) => e.target.value;
...
<input  value={ link.value }
        onChange={ link.action( setValue ) } />

This is particularly useful in (but not restricted to) UI event handlers.

Checks whenever the predicate is truthy on linked value, and if it's not, assigns link.error with errorMsg. Does nothing if link.error is already populated.

This method may be used for additional validation in render(), and to populate the validation error for the custom links created with Link.value().

You typically don't need link.check for links created with linkAt() methods, because the validation happens inside of records.

It's rather unusual scenario that you hold complex raw JS data as a part of your state, because typically the state is defined as a superposition of nested records and collections.

Links can be used to make purely functional updates of the objects and arrays in records attributes. It's done with the help of at-links, which points to the elements of linked objects and arrays.

Create an at-link to the member of array or object.

Whenever an at-link is updated, it will lead to proper purely functional update (with shallow copying) of the container (array or object).

// Update this.state.array[ 0 ].name
this.linkAt( 'array' ).at( 0 ).at( 'name' ).set( 'Joe' );

Map and filter through the linked array or object to produce an array. Mapping function receives at-link to the corresponding element. Whenever it returns undefined, the corresponding element is be skipped.

// Render the linked array...
var list = stringArrayLink.map( ( itemLink, index ) => {
    if( itemLink.value ){ // Skip empty elements
        return (
            <div key={ index }>
                <Input valueLink={ itemLink } />
            </div>
        );
    }
});

Update linked object or array.

Plain objects and arrays are shallow copied already, thus it's safe just to update the value in place.

// Update the linked object
<button onClick={ () => objLink.update( obj => {
                                obj.a = 1;
                                return obj;
                            }) } />

Creates action to update enclosed object or array.

Plain objects and arrays are shallow copied already, thus it's safe just to update the value in place.

Remove element with a given key from the linked object or array.

Remove element with a given key from the linked object or array.

Object-specific methods

Create at-links to the object's members with designated keys, and wrap them in an object.

// Bulk create at-links for the linked object
const { name, email } = objLink.pick( 'name', 'email' );

Array-specific methods

linkToArray.splice() : void

Similar to Array.splice() method, but performs purely functional update.

linkToArray.push() : void

Similar to Array.push() method, but performs purely functional update.

linkToArray.unshift() : void

Similar to Array.unshift() method, but performs purely functional update.

linkToArray.contains( element )

Creates the boolean link to the presence of value in array.

Resulting link value is true whenever element is present in array, and false otherwise. Whenever resulting link is assigned with new value, it will flip element in the array.

Useful for the large checkbox groups.

Record and Collection

React-MVx relies of Type-R framework to manage a multi-layer application state.

Data binding

Ad-hoc data binding

<tag { ...link.props } />

Bind the linked value to any standard UI control expecting value and onChange props.

Text controls

Bind input or textarea to the linked string:

// String
<input type="text" {...link.props} />
<textarea {...link.props} />

Checkboxes

Bind the checkbox to the linked boolean:

<input type="checkbox" {...boolLink.props } />

Bind the checkbox to the presence of value in the array:

// array = [ 'optionA' ]
<input type="checkbox"  {...arrayLink.contains( 'optionA' ).props } /> // Checked
<input type="checkbox"  {...arrayLink.contains( 'optionB' ).props } /> // Unchecked

Bind the checkbox to the presence of the record in the collection:

<input type="checkbox" {...collection.linkContains( record ).props } />

Radio groups

Bind radio group to the single linked value:

// Radio
<input type="radio" {...link.equals( 'optionA' ).props } />
<input type="radio" {...link.equals( 'optionB' ).props } />

Select list

Bind select list to the linked value:

// Select
<select {...link.props}>
    <option value="optionA"> A </option>
    <option value="optionB"> B </option>
</select>

Linked UI controls

Linked control is the custom React component taking the link property instead of value/onChange props pair. It uses the link to extract the value and validation error, and to modify the value.

Linked controls makes it possible to create the semantic form markup encapsulating inline validation and form layout styling. Not just form controls, but the most of the UI can benefit of this technique.

// Custom data-bound control
const Input = ({ link, ...props }) => (
    <div className={`form-row ${ link.error ? 'has-error' : '' } `}>
        <input type="text" {...props} { ...link.props } />
        <div className="error-placeholder">{ link.error || '' } </div>
    </div>
);

// Another simple data bound control
const Input = ({ link, ...props }) => (
    <input {...props}
            value={ link.value }
            onChange={ e => link.set( e.target.value ) } />
);

There are the set of pre-defined linked UI controls in react-mvx/tags modules. Inline error indication is rather project-dependent, thus this file is intended to be used as a reference and starting boilerplate for your controls.

import { Input } from 'react-mvx/tags'

Text input controls

tags.jsx contains wrappers for standard <input> and <textarea> tags, which consume linked strings. These wrappers add invalid class to enclosed HTML element if an error is present in the link, and required class if isRequired validator is the failing one.

import { Input, TextArea } from 'react-mvx/tags'
...
<Input type="text" valueLink={ link } />
<TextArea valueLink={ link } />

Its implementation is rather straightforward.

Numeric input

In some cases you can use the wrong input rejection instead of (or in addition to) the validation. The most popular example is the numeric-only input control. It guarantees that the linked value will only be updated with the valid number, completely encapsulating all related checks and mechanics.

The challenge here is that when number in not an integer it has to go through the sequence of intermediate invalid states during the editing process. Like "" -> "-" -> "-0" -> "-0." -> "-0.5".

The proper implementation of wrong input rejection might be tough. tags.jsx contains the cross-browser numeric input tag. It has following differences compared to the regular <Input>:

<NumberInput> validates its intermediate state and adds invalid class to enclosed input element if it's not a number.

import { NumberInput } from 'react-mvx/tags'

<NumberInput valueLink={ link } />
<NumberInput valueLink={ link } integer={ true }/>
<NumberInput valueLink={ link } positive={ true }/>

Checkboxes

There are different ways how you can handle the checkbox components. The problem of the standard checkbox control, though, is that it's not that easily styled.

tags.jsx contains the custom checkbox and radio components implemented using the plain <div /> element which toggles selected class on click. By default, it has checkbox CSS class, which can be overridden by passing className prop. It passes through anything else, including children.

<Checkbox checkedLink={ booleanLink } />
<Checkbox checkedLink={ arrayLink.contains( 'option' ) } />

Radio Groups

There are two different ways how you can approach the data binding for the radio groups. First option is to pass the value of the particular option along with the link. Link this:

<label>
    A: <Input type="radio" valueLink={ flagLink } value="a" />
</label>
<label>
    B: <Input type="radio" valueLink={ flagLink } value="b" />
</label>

Alternatively, you can use link.equals( value ) method to produce the boolean link which is specially designed to create radio groups, as it's illustrated by the custom <Radio /> tags from the tags.jsx.

Internally, it's <div> element which always sets its link to true on click. And whenever the link value is true, it adds selected class to the div.

By default, it has radio CSS class, which can be overridden by passing className prop. It passes through anything else, including children.

<label>
    A: <Radio checkedLink={ flagLink.equals( 'a' ) } />
</label>
<label>
    B: <Radio checkedLink={ flagLink.equals( 'b' ) } />
</label>

Release Notes

2.0.0