React Flux structure for a shopping app

Posted on

Problem

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;

Solution

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.

Leave a Reply

Your email address will not be published. Required fields are marked *