3
\$\begingroup\$

We have just started up a new app and we are using React with the Flux architecture. We have done our best to piece together what we think is the best structure for the app and would appreciate any feedback, suggestions etc. The component is essentially a settings page in a much larger system, it basically gets and updates shop details which consist of a name, URL and country code. The other settings pages will have a similar functionality and wanted to make sure that this was a sound approach before we get going with the rest.

Component

var React = require('react');
var ShopDetailsStore = require('../Stores/ShopDetailsStore.js');
var Dispatcher = require('../../../../../Dispatchers/AppDispatcher.js');
var Actions = require("../Actions/ShopDetailsActions.js");

function getState(){
    return {
        shopDetails: ShopDetailsStore.getShopDetails(),
        errors: ShopDetailsStore.getErrors()
    };
}

var ShopDetails = React.createClass({
    getInitialState:function(){
        return getState();
    },
    componentDidMount: function (){
        Actions.getShopDetails();
        ShopDetailsStore.addChangeListener(this._onChange);
    },
    componentWillMount: function(){
        ShopDetailsStore.removeChangeListener(this._onChange);
    },
    updateShopDetails:function(e){
        var shopName = this.state.shopDetails.ShopName;
        var siteURL = this.state.shopDetails.SiteURL;
        var countryCode = this.state.shopDetails.CountryCode;
        Actions.putShopDetails({ShopName: shopName, SiteURL: siteURL, CountryCode: countryCode});
    },
    handleChangeShopName:function(e){
        this.setState({shopDetails: {ShopName: e.target.value, SiteURL: this.state.shopDetails.SiteURL, CountryCode: this.state.shopDetails.CountryCode}});
    },
    handleChangeSiteURL:function(e){
        this.setState({shopDetails: {ShopName: this.state.shopDetails.ShopName, SiteURL: e.target.value, CountryCode: this.state.shopDetails.CountryCode}});
    },
    handleChangeTradingCountry:function(e){
        this.setState({shopDetails: {ShopName: this.state.shopDetails.ShopName, SiteURL: this.state.shopDetails.SiteURL, CountryCode: e.target.value}});
    },
    render: function(){
        var errors = this.state.errors.map(function(error){
            return (<span className="error" key={error.readyState}>{error.status} : {error.statusText}</span>);
        });

        return (<div>
                    <div>{errors}</div>
                    <input type="text" value={this.state.shopDetails.ShopName} onChange={this.handleChangeShopName}/>
                    <input type="text" value={this.state.shopDetails.SiteURL} onChange={this.handleChangeSiteURL}/>
                    <select value={this.state.shopDetails.CountryCode} onChange={this.handleChangeTradingCountry}>
                        <option value="GB">United Kingdom</option>
                        <option value="US">United States</option>
                    </select>
                    <input type="button" onClick={this.updateShopDetails} value="Update Details"/>
                </div>);
    },
    _onChange: function(){
        this.setState(getState());
    }
});

module.exports = ShopDetails;

Actions - You'll notice we are doing some weird stuff in the success callback of the putShopDetails request, basically the api returns a success even if it doesn't update anything so it's just a fudge to cover that scenario.

var ShopDetailsConstants = require('../Constants/ShopDetailsConstants.js');
var AppDispatcher = require('../../../../../Dispatchers/AppDispatcher.js');
var $ = require('jquery');

var ShopDetailsActions = {
    putShopDetails:function(shopDetails){
        $.ajax({
            url: "/api/settings/shopdetails",
            type: 'PUT',
            data: shopDetails,
            success: function(data){
                if(data.RowsAffected <= 0){
                    AppDispatcher.dispatch({
                        actionType: ShopDetailsConstants.PUT_SHOPDETAILS_FAILURE,
                        shopDetails: shopDetails,
                        error: { readyState: 1, statusText: data.Status.StatusCode, status: 500 }
                    }); 
                } else {
                    AppDispatcher.dispatch({
                        actionType: ShopDetailsConstants.PUT_SHOPDETAILS_SUCCESS,
                        shopDetails: shopDetails
                    }); 
                }
            },
            error: function(error){
                AppDispatcher.dispatch({
                    actionType: ShopDetailsConstants.PUT_SHOPDETAILS_FAILURE,
                    error: error,
                    shopDetails: shopDetails
                });
            }
        });
    },
    getShopDetails:function(){
        $.ajax({
            url: "/api/settings/shopdetails",
            type: 'GET',
            success: function(data){
                AppDispatcher.dispatch({
                    actionType: ShopDetailsConstants.GET_SHOPDETAILS_SUCCESS,
                    shopDetails: data
                });
            },
            error: function(error){
                AppDispatcher.dispatch({
                    actionType: ShopDetailsConstants.GET_SHOPDETAILS_FAILURE,
                    error: error
                });
            }
        });
    }
};

module.exports = ShopDetailsActions;

Store

var AppDispatcher = require('../../../../../Dispatchers/AppDispatcher.js');
var ShopDetailsConstants = require('../Constants/ShopDetailsConstants.js');
var EventEmitter = require('events').EventEmitter;
var assign = require('object-assign');

var CHANGE_EVENT = "change";

var _shopDetails = {};
var _errors = [];

var ShopDetailsStore = assign({}, EventEmitter.prototype, {
    emitChange:function(){
        this.emit(CHANGE_EVENT)
    },
    addChangeListener:function(callback){
        this.on(CHANGE_EVENT, callback)
    },
    removeChangeListener:function(callback){
        this.removeListener(CHANGE_EVENT, callback)
    },
    getShopDetails:function(){
        return _shopDetails;
    },
    getErrors:function(){
        return _errors;
    }
});

AppDispatcher.register(function(action){

    _errors = [];

    switch(action.actionType){
        case ShopDetailsConstants.GET_SHOPDETAILS_SUCCESS:
            _shopDetails = action.shopDetails;
        break;
        case ShopDetailsConstants.GET_SHOPDETAILS_FAILURE:
            _shopDetails = action.shopDetails;
            _errors.push(action.error);
        break;
        case ShopDetailsConstants.PUT_SHOPDETAILS_SUCCESS:
            _shopDetails = action.shopDetails;
        break;
        case ShopDetailsConstants.PUT_SHOPDETAILS_FAILURE:
            _shopDetails = action.shopDetails;
            _errors.push(action.error);
        break;
        default:
    }

    ShopDetailsStore.emitChange();
});

module.exports = ShopDetailsStore;
\$\endgroup\$
1
\$\begingroup\$

Generally your code is fine, a couple of comments:

  1. Consider that components will re-render on emitChange, and at the moment you do call this in the store for all actions. If you have several components on the same page which listen to this store then it may cause performance issues. So it might be a good idea to have several stores to reduce re-rendering, i.e. one for errors, one for settings, etc.

  2. At the moment you are doing your API calls within your actions. This is fine in itself but if you want a nicer separation you may want to have them in a utility file like the official chat example.

\$\endgroup\$
  • \$\begingroup\$ Welcome to Code Review! Good job on your first answer. \$\endgroup\$ – SirPython Sep 26 '15 at 21:11
  • \$\begingroup\$ @Kash many thanks for the answer, some good points, generally we have a one to one relationship with stores and components so this hasn't been an issue for us but I can certainly see your point. With the API calls we were torn on where to put them at first, there are a lot of different implementations and takes on flux so it was hard to know what to do for the best but we have found this works pretty well, although we may look at abstracting them further as the app grows. Thanks again \$\endgroup\$ – bencrinkle Sep 28 '15 at 10:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.