Using Flux to create an editing form, who actually runs the POST data on the server: actions, stores, views?

I found a lot of resources, blogs and opinions on how to retrieve data for React and Flux, but much less when writing data to the server. Can someone explain the rationale and some sample code for the “preferred” approach in the context of creating a simple editing form that saves the changes to the RESTful web API?

In particular, which of the Flux fields should call $.post , where is ActionCreator.receiveItem() called (and what does it do), and what is in the registered storage method?

Relevant links:

+5
source share
2 answers

Short answer

  • The form component must retrieve its state from the Repository, create an “update” at the user’s inputs and invoke the “save” action in the submit form.
  • The creators of the action will execute the POST request and initiate the save_success or save_error action depending on the results of the request.

Long answer through an example implementation

apiUtils / BarAPI.js

 var Request = require('./Request'); //it 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) { //ie 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 }); }, }; 

shops / 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 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 do that for the example 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 in a Link to the editing page of the Bar record with id = this.props.id. Let assume that 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> ) }, }); 

In this basic example, whenever a user edits a Bar element through a form in the BarDetail component, the basic Bar record will be kept up to date locally, and when the form is submitted, we will try to save it to the server. What is it:)

+4
source
  • Components / views are used to display data and fire events.
  • Actions are attached to events (onClick, onChange ...) and are used to communicate with resources and dispatch events after a promise has been resolved or not fulfilled. Make sure you have at least two events, one for success and one for ajax.
  • Stores subscribe to the event manager. After receiving the storage data, the values ​​that are saved and emit changes are updated.
  • Components / views are subscribed to stores and re-displayed after the change.

If thread stores or actions (or both) relate to external services? the approach is what seems natural to me.

There are also cases where you need to initiate an action as a result of starting some other action, so you can start actions from the corresponding repository, as a result of which the result and presentation repositories are saved.

+1
source

All Articles