Hao
Hi, I am Hao (đź‘‹): a coder, a woodworker, a blogger, and a father.
A simple React framework based on Pub/Sub
November 10, 2015

For the development of my latest project WordMark, I developed a new but simple framework based on React. The whole idea is based on Redux, while it is much simpler than that. Besides PubSubJS, there is no any other dependencies. I will introduce this framework based on a simple markdown editor/previewer page.

Index

The structure of the project is as follows:

Project
|-- components
    |-- Editor
    |-- Preview
|-- actions
    |-- ContentActions.js
|-- stores
    |-- ContentStores.js
|-- entry.js
|-- Root.js

Looks simple. entry.js is the entry file.

// entry.js
import React from 'react';  
import Root from './Root.jsx';
React.render(
    <Root/>, document.getElementById('container')
);

There are many new concepts and modules in Redux which I don’t really like. Taking store for example, you will need createStore() and storeConfiguration. In this simple framework, Root.js works as the store. Its state is all the status and data for the entire app.

// Root.js
export default React.createClass({
    getInitialState() {
        return {
            markdown: '',
            preview: ''
        };
    },
    render() {
        return (
            <Editor state={this.state}/>
            <Preview state={this.state}/>
        );
    };
});

state of Root.js is the initial state. Let’s initial two variables: markdown and preview. The markdown is to store raw text and the preview is to store the HTML generated by markdown engine.

In render() part, we just pass state to sub-modules, which are like “smart components” of Redux. They share the global state.

Also, anothing very important thing is:

// Root.js
componentDidMount() {
    PubSub.subscribe('STATE_UPDATE', this.handleSubscribe);
},
handleSubscribe(type: String, state) {
    this.setState(state);
}

It means that the Root will subscribe any updates to any changes happend to state, and handle the updates in handleSubscribe() function. They are core parts of the framework.

Now let’s do some actions:

// Editor.jsx
import ContentActions from '../actions/ContentActions.js';

...
render() {
    return (
        <div className="Editor">
            <textarea onChange={this.handleTextareaChange} value={this.props.state.markdown}/>
        </div>
    );
},
handleTextareaChange(e) {
    ContentActions.changeMarkdown(this.props.state, e.target.value);
}
...

In Editor, there is a <textarea/> as the text editor. Any changes will be handled by handleTextarea(), in which current state and the value of markdown will be passed to actions functions. Also, the value of <textarea/> comes from props.

In ContentActions:

// ContentActions.js
import ContentStores from '../stores/ContentStores.js';
import marked from 'marked';

export default {
    changeMarkdown(state: Object, markdown: String) {
        let newState = {...state, markdown: markdown, preview: marked(markdown)};
        return ContentStores.updateState(newState);
    }
};

In this article, there is a very good explanation about why we use immutable.

In ContentStores:

import PubSub from 'pubsub-js';
updateState(state) {
    PubSub.publish('STATE_UPDATE', state);
}

Therefore, any changes in <textarea/> will emit a data flow like this:

Editor -> ContentActions -> ContentStores -> Root -> Editor/Preview

The whole mess is for the “single-direction data flow”.

We can use any data in state:

// Preview.js
...
render() {
    return (
        <div className="Preview">
            <div dangerouslySetInnerHTML=/>
        </div>
    );
}

This framework also works for async requests. Putting all async requests in action, and using Promise to control.

My new Electron based Mac app WordMark is entirely based on this framework. Feel free to take a look.