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:
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.
Flux's slogan is “unidirectional data flow”.
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.
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.
Short answer
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 :)
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With