Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using Flux to build an edit form, who actually POSTs data to the server: actions, stores, views?

I have found a lot of resources, blogs, and opinions on how to fetch data for React and Flux, but much less on writing data to the server. Can someone please provide a rationale and some sample code for the "preferred" approach, in the context of building a simple edit form that persists changes to a RESTful web API?

Specifically, which of the Flux boxes should call $.post, where is the ActionCreator.receiveItem() invoked (and what does it do), and what is in the store's registered method?

Relevant links:

  • Should the action or store be responsible for transforming data when using React + Flux?
  • Should flux stores, or actions (or both) touch external services?
  • Where should ajax request be made in Flux app?
like image 519
Scott Stafford Avatar asked Jul 22 '15 21:07

Scott Stafford


People also ask

Which building blocks of flux is used to grab the updated state from the store?

Node's event emitter is used to update the store and broadcast the update to view. The view never directly updates the application state. It is updated because of the changes to the store. This is only part of Flux that can update the data.

Which of the following correctly describes the data flow of flux?

Flux's slogan is “unidirectional data flow”.

What is the use of flux in React?

Flux is a pattern for managing how data flows through a React application. As we've seen, the preferred method of working with React components is through passing data from one parent component to it's children components. The Flux pattern makes this model the default method for handling data.

Which of the following correctly describes the data flow of flux in React?

In Flux application, data flows in a single direction(unidirectional). This data flow is central to the flux pattern. The dispatcher, stores, and views are independent nodes with inputs and outputs. The actions are simple objects that contain new data and type property.


2 Answers

Short answer

  • Your form component should retrieve its state from Store, create "update" action on user inputs, and call a "save" action on form submit.
  • The action creators will perform the POST request and will trigger a "save_success" action or "save_error" action depending on the request results.

Long answer via implementation example

apiUtils/BarAPI.js

var Request = require('./Request'); //it's a custom module that handles request via superagent wrapped in Promise
var BarActionCreators = require('../actions/BarActionCreators');

var _endpoint = 'http://localhost:8888/api/bars/';

module.exports = {

    post: function(barData) {
        BarActionCreators.savePending();
        Request.post(_endpoint, barData).then (function(res) {
            if (res.badRequest) { //i.e response returns code 400 due to validation errors for example
                BarActionCreators.saveInvalidated(res.body);
            }
            BarActionCreators.savedSuccess(res.body);
        }).catch( function(err) { //server errors
            BarActionCreators.savedError(err);
        });
    },

    //other helpers out of topic for this answer

};

actions/BarActionCreators.js

var AppDispatcher = require('../dispatcher/AppDispatcher');
var ActionTypes = require('../constants/BarConstants').ActionTypes;
var BarAPI = require('../apiUtils/VoucherAPI');

module.exports = {

    save: function(bar) {
        BarAPI.save(bar.toJSON());
    },

    saveSucceed: function(response) {
        AppDispatcher.dispatch({
            type: ActionTypes.BAR_SAVE_SUCCEED,
            response: response
        });
    },

    saveInvalidated: function(barData) {
        AppDispatcher.dispatch({
            type: ActionTypes.BAR_SAVE_INVALIDATED,
            response: response
        })
    },

    saveFailed: function(err) {
        AppDispatcher.dispatch({
            type: ActionTypes.BAR_SAVE_FAILED,
            err: err
        });
    },

    savePending: function(bar) {
        AppDispatcher.dispatch({
            type: ActionTypes.BAR_SAVE_PENDING,
            bar: bar
        });
    }

    rehydrate: function(barId, field, value) {
        AppDispatcher.dispatch({
            type: ActionTypes.BAR_REHYDRATED,
            barId: barId,
            field: field,
            value: value
        });
    },

};

stores/BarStore.js

var assign = require('object-assign');
var EventEmitter = require('events').EventEmitter;
var Immutable = require('immutable');

var AppDispatcher = require('../dispatcher/AppDispatcher');
var ActionTypes = require('../constants/BarConstants').ActionTypes;
var BarAPI = require('../apiUtils/BarAPI')
var CHANGE_EVENT = 'change';

var _bars = Immutable.OrderedMap();

class Bar extends Immutable.Record({
    'id': undefined,
    'name': undefined,
    'description': undefined,
    'save_status': "not saved" //better to use constants here
}) {

    isReady() {
        return this.id != undefined //usefull to know if we can display a spinner when the Bar is loading or the Bar's data if it is ready.
    }

    getBar() {
        return BarStore.get(this.bar_id);
    }
}

function _rehydrate(barId, field, value) {
    //Since _bars is an Immutable, we need to return the new Immutable map. Immutable.js is smart, if we update with the save values, the same reference is returned.
    _bars = _bars.updateIn([barId, field], function() {
        return value;
    });
}


var BarStore = assign({}, EventEmitter.prototype, {

    get: function(id) {
        if (!_bars.has(id)) {
            BarAPI.get(id); //not defined is this example
            return new Bar(); //we return an empty Bar record for consistency
        }
        return _bars.get(id)
    },

    getAll: function() {
        return _bars.toList() //we want to get rid of keys and just keep the values
    },

    Bar: Bar,

    emitChange: function() {
        this.emit(CHANGE_EVENT);
    },

    addChangeListener: function(callback) {
        this.on(CHANGE_EVENT, callback);
    },

    removeChangeListener: function(callback) {
        this.removeListener(CHANGE_EVENT, callback);
    },

});

var _setBar = function(barData) {
    _bars = _bars.set(barData.id, new Bar(barData));
};

BarStore.dispatchToken = AppDispatcher.register(function(action) {
    switch (action.type)
    {   

        case ActionTypes.BAR_REHYDRATED:
            _rehydrate(
                action.barId,
                action.field,
                action.value
            );
            BarStore.emitChange();
            break;

        case ActionTypes.BAR_SAVE_PENDING:
            _bars = _bars.updateIn([action.bar.id, "save_status"], function() {
                return "saving";
            });
            BarStore.emitChange();
            break;

        case ActionTypes.BAR_SAVE_SUCCEED:
            _bars = _bars.updateIn([action.bar.id, "save_status"], function() {
                return "saved";
            });
            BarStore.emitChange();
            break;

        case ActionTypes.BAR_SAVE_INVALIDATED:
            _bars = _bars.updateIn([action.bar.id, "save_status"], function() {
                return "invalid";
            });
            BarStore.emitChange();
            break;

        case ActionTypes.BAR_SAVE_FAILED:
            _bars = _bars.updateIn([action.bar.id, "save_status"], function() {
                return "failed";
            });
            BarStore.emitChange();
            break;

        //many other actions outside the scope of this answer

        default:
            break;
    }
});

module.exports = BarStore;

components/BarList.react.js

var React = require('react/addons');
var Immutable = require('immutable');

var BarListItem = require('./BarListItem.react');
var BarStore = require('../stores/BarStore');

function getStateFromStore() {
    return {
        barList: BarStore.getAll(),
    };
}

module.exports = React.createClass({

    getInitialState: function() {
        return getStateFromStore();
    },

    componentDidMount: function() {
        BarStore.addChangeListener(this._onChange);
    },

    componentWillUnmount: function() {
        BarStore.removeChangeListener(this._onChange);
    },

    render: function() {
        var barItems = this.state.barList.toJS().map(function (bar) {
            // We could pass the entire Bar object here
            // but I tend to keep the component not tightly coupled
            // with store data, the BarItem can be seen as a standalone
            // component that only need specific data
            return <BarItem
                        key={bar.get('id')}
                        id={bar.get('id')}
                        name={bar.get('name')}
                        description={bar.get('description')}/>
        });

        if (barItems.length == 0) {
            return (
                <p>Loading...</p>
            )
        }

        return (
            <div>
                {barItems}
            </div>
        )

    },

    _onChange: function() {
        this.setState(getStateFromStore();
    }

});

components/BarListItem.react.js

var React = require('react/addons');
var ImmutableRenderMixin = require('react-immutable-render-mixin')
var Immutable = require('immutable');

module.exports = React.createClass({

    mixins: [ImmutableRenderMixin],

    // I use propTypes to explicitly telling
    // what data this component need. This 
    // component is a standalone component
    // and we could have passed an entire
    // object such as {id: ..., name, ..., description, ...}
    // since we use all the datas (and when we use all the data it's
    // a better approach since we don't want to write dozens of propTypes)
    // but let's do that for the example's sake 
    propTypes: {
        id: React.PropTypes.number.isRequired,
        name: React.PropTypes.string.isRequired,
        description: React.PropTypes.string.isRequired
    }

    render: function() {

        return (
            <li> //we should wrapped the following p's in a Link to the editing page of the Bar record with id = this.props.id. Let's assume that's what we did and when we click on this <li> we are redirected to edit page which renders a BarDetail component
                <p>{this.props.id}</p>
                <p>{this.props.name}</p>
                <p>{this.props.description}</p>
            </li>
        )

    }

});

components/BarDetail.react.js

var React = require('react/addons');
var ImmutableRenderMixin = require('react-immutable-render-mixin')
var Immutable = require('immutable');

var BarActionCreators = require('../actions/BarActionCreators');

module.exports = React.createClass({

    mixins: [ImmutableRenderMixin],

    propTypes: {
        id: React.PropTypes.number.isRequired,
        name: React.PropTypes.string.isRequired,
        description: React.PropTypes.string.isRequired
    },

    handleSubmit: function(event) {
        //Since we keep the Bar data up to date with user input
        //we can simply save the actual object in Store.
        //If the user goes back without saving, we could display a 
        //"Warning : item not saved" 
        BarActionCreators.save(this.props.id);
    },

    handleChange: function(event) {
        BarActionCreators.rehydrate(
            this.props.id,
            event.target.name, //the field we want to rehydrate
            event.target.value //the updated value
        );
    },

    render: function() {

        return (
            <form onSubmit={this.handleSumit}>
                <input
                    type="text"
                    name="name"
                    value={this.props.name}
                    onChange={this.handleChange}/>
                <textarea
                    name="description"
                    value={this.props.description}
                    onChange={this.handleChange}/>
                <input
                    type="submit"
                    defaultValue="Submit"/>
            </form>
        )

    },

});

With this basic example, whenever the user edits a Bar item via the form in BarDetail component, the underlying Bar record will be maintained up to date locally and when the form is submitted we try to save it on the server. That's it :)

like image 57
Pierre Criulanscy Avatar answered Oct 11 '22 03:10

Pierre Criulanscy


  1. Components/Views are used to display data and fire events
  2. Actions are tied to the events (onClick, onChange...) and are used to communicate with resources and dispatch events once the promise has been resolved or failed. Make sure you have at least two events, one for success and one for ajax failed.
  3. Stores are subscribed to the events dispatcher is dispatching. Once data is received stores are updating the values which are stored and emitting changes.
  4. Components/Views are subscribed to the stores and are re-rendering once the change has happened.

Should flux stores, or actions (or both) touch external services? approach is what seems natural to me.

Also there are cases when you need to trigger some action as a result of some other action being triggered, this is where you can trigger actions from a relevant store, which results store and views being updated.

like image 44
gor181 Avatar answered Oct 11 '22 03:10

gor181