7

I want to make an app using React.js. I want it to be easily customizable from the outside world (e. g. by writing userscripts). The idea I attempted to use is to make some special properties in the root element state (like sidebarItems or playlistCreatedHooks) so addon developers could add something there. My questions are: is this a good way to do so, is there the Right Way™ to achieve something similar to my goal and, finally, how will addon developers access these props?

Ale
  • 1,998
  • 19
  • 31

2 Answers2

8

One option is observables. Basically it's an object you can listen to changes on, and create a change on. You could also emit other events like an 'add' event on data.playlists to create the api you want to provide.

// data.js
var data = {
  sidebarItems: Observable([]),
  playlists: Observable([])
};

// app.js
var App = React.createComponent({
  mixins: [data.sidebarItems.mixin("sidebar")],
  render: function(){
    return this.state.sidebar.map(renderSidebarItem);
  }
});

/// userscript.js

// causes the view to update
data.sidebarItems.set(somethingElse); 

// run when someone does data.playlists.set(...)
data.playlists.on('change', function(playlists){

});

// an event you could choose to emit with data.playlists.emit('add', newPlaylist)
data.playlists.on('add', function(newPlaylist){

});

Here's an example (untested) implementation of Observable used above, with an extra function for generating the react component mixin.

var events = require('events'); // or some other way of getting it
var Observable = function(initialValue){
  var self = new events.EventEmitter();
  var value = initialValue;

  self.get = function(){ return value };
  self.set = function(updated){
    value = updated;
    self.emit('change', updated);
  };
  self.mixin = function(key){
    var cbName = Math.random().toString();
    var mixin = {
      getInitialState: function(){ var o = {}; o[key] = value; return o },
      componentDidMount: function(){
        self.on('change', this[cbName]);
      },
      componentWillUnmount: function(){
        self.removeListener('change', this[cbName]);
      }
    }
    mixin[cbName] = function(){
      var o = {}; o[key] = value; this.setState(o);
    };
    return mixin;
  }

  return self;
}
Brigand
  • 84,529
  • 20
  • 165
  • 173
1

Here is my solution. Thanks to this Observable the React components' state are automatically updated (with its consequences, like re-render the component) and you can even listen for changes outside react thanks to the .on method.

var eventEmitter = {
    _JQInit: function() {
        this._JQ = jQuery(this);
    },
    emit: function(evt, data) {
        !this._JQ && this._JQInit();
        this._JQ.trigger(evt, data);
    },
    once: function(evt, handler) {
        !this._JQ && this._JQInit();
        this._JQ.one(evt, handler);
    },
    on: function(evt, handler) {
        !this._JQ && this._JQInit();
        this._JQ.bind(evt, handler);
    },
    off: function(evt, handler) {
        !this._JQ && this._JQInit();
        this._JQ.unbind(evt, handler);
    }
};

var Observable = function(initialValue, name) {
    var self = eventEmitter;
    var name = name;
    var obj = {
        value: initialValue,
        ops: self
    };

    self.get = function() {
        return obj.value
    };

    self.set = function(updated){
        if(obj.value == updated)
            return;

        obj.value = updated;
        self.emit('change', updated);
    };

    self.mixin = function() {
        var mixin = {
            getInitialState: function() {
                var obj_ret = {};
                obj_ret[name] = obj;

                return obj_ret;
            },
            componentDidMount : function() {
                self.on('change', function() {
                    var obj_new = {};
                    obj_new[name] = obj;

                    this.setState(obj_new);
                }.bind(this));
            }
        };

        return mixin;
    };

    return self;
};

example (using it for showing an alert on another component): //Observable init alert_msg = Observable('', 'alertmsg');

var ConfirmBtn = React.createClass({
    mixins: [alert_msg.mixin()],
    handleConfirm: function(e) {
        e.preventDefault();

        this.state.alertmsg.ops.set(null);

        if(! $('#cgv').is(':checked')) {
            this.state.alertmsg.ops.set('Please accept our terms of conditions');
            return;
        }
    }
}

var AlertPayment = React.createClass({
    mixins: [alert_msg.mixin()],

    render: function() {
        var style = (this.state.alertmsg === null) ? {display: 'none'} : {display: 'block'};

        return (
            <div style={style} className="alertAcceptTerms">
                {this.state.alertmsg.value}
            </div>
        );
    }
});

Hope it helps

Bertuz
  • 2,390
  • 3
  • 25
  • 50