Skip to content

Instantly share code, notes, and snippets.

@nicolashery
Created August 2, 2015 19:20
Show Gist options
  • Save nicolashery/fb9df874ba2e4015dde0 to your computer and use it in GitHub Desktop.
Save nicolashery/fb9df874ba2e4015dde0 to your computer and use it in GitHub Desktop.
// Flux actions called by views (components) are meant to be "fire and forget":
// the view will get an update after the dispatcher has updated the store.
//
// But sometimes, for view state that only really matters to the mounted
// component (like a loading indicator), it might be simpler to have
// a "done" callback in the action creator. This is considered a Flux
// anti-pattern, but if you don't actually pass data to the callback, you
// make sure not to break the "store = single-source of truth" principle.
// For example, let's say we have a widget that allows the user to add places
// to lists she creates (think of Airbnb or Amazon "wish lists").
/*
+----------------------------------+
| Add "Hipster Coffee House" to: |
| |
| +------------------+ +---------+ |
| | New list name... | | Create | |
| +------------------+ +---------+ |
| |
| +-+ |
| |X| My favorite places |
| +-+ |
| |
| +-+ |
| | | Places to discover |
| +-+ |
| |
+----------------------------------+
*/
// A list must have a unique name, so when creating a new list we want to
// actually wait for the server response before updating the lists below with
// the new one. But we don't necessarily want to create a whole store with
// action handlers for that.
// So our action creator could look like:
function createList(name, done = _.noop) {
return (dispatch) => {
// Notice that we still dispatch request, failure, and success actions:
// the "done" callback does not, and should not, replace them
dispatch({type: CREATE_LIST_REQUEST, name});
api.createList({name}, (error, list) => {
if (error) {
dispatch({type: CREATE_LIST_FAILURE, name, error});
done(error);
return;
}
dispatch({type: CREATE_LIST_SUCCESS, list});
// Notice that we don't past the "list" object to the callback:
// the single source of truth is still the store
done();
});
};
}
// Our reducer for the store (we are using Redux to manage app state):
function listsById(state = {}, action) {
switch (action.type) {
case CREATE_LIST_SUCCESS:
let list = action.list;
state = _.clone(state);
state[list.id] = list;
return state;
default:
return state;
}
}
// And our component:
function select(state) {
return {lists: _.values(state.listsById)};
}
var propTypes = {
// Provided by parent component
venueId: React.PropTypes.string.isRequired,
// Provided by Redux store
lists: React.PropTypes.array.isRequired,
dispatch: React.PropTypes.func.isRequired
};
class SaveToList extends React.Component {
constructor(props) {
super(props);
this.state = {
name: null,
// Sometimes it's simpler to have view state just be in the view
// (vs. a separate store), especially if it's only used by that component
// and is tied to it's lifecyle (component mount/unmount).
// This is the case here, we want to reset "isCreating" and "error"
// whenever the component mounts/unmounts, and React does it for us.
// If it were in a store, we would need to make sure to do it manually,
// which is prone to error and bugs.
isCreating: false,
error: null
};
}
render() {
return (
<div>
{this.renderNewList()}
{this.renderLists()}
</div>
);
}
renderNewList() {
var isButtonDisabled = (
!(this.state.name && this.state.name.length) || this.state.isCreating
);
var buttonText = this.state.isCreating ? 'Creating...' : 'Create';
var error;
if (this.state.error) {
error = <div style={{color: 'red'}}>{this.state.error}</div>;
}
return (
<form>
<input
id="name"
ref="name"
placeholder="New list name..."
value={this.state.name}
onChange={e => this.setState({name: e.target.value})} />
<button
type="submit"
disabled={isButtonDisabled}
onClick={this.handleCreateList.bind(this)}>
{buttonText}
</button>
{error}
</form>
);
}
handleCreateList(e) {
e.preventDefault();
var list = {
name: this.state.name,
venues: [this.props.venueId]
};
// This is were we make use of the "done" callback in our "createList"
// action creator.
this.setState({
isCreating: true,
error: null
});
this.props.dispatch(createList(list, error => {
// Notice that we don't hold the "lists" state, and don't try to update
// it: that is all handled by the actions and store, and passed to us
// via props.lists.
// Here we're only concerned with waiting for the server to give the
// green light, or display an appropriate message to the user if there
// is a problem.
if (error) {
return this.setState({
isCreating: false,
error: (
error.status === 409 ?
'A list already exists with that name' :
'Error creating list'
)
});
}
this.setState({
name: null,
isCreating: false
});
}));
}
renderLists() {
return (
<div>
{_.map(this.props.lists, list => (
<label key={list.id}>
<input
type="checkbox"
value={list.id}
checked={_.indexOf(list.venues, this.props.venueId) > -1}
onChange={this.handleCheckList.bind(this)} />
{list.name}
</label>
))}
</div>
);
}
handleCheckList(e) {
var venueId = this.props.venueId;
var listId = e.target.value;
// For adding/removing from existing lists, we use the "traditional
// fire-and-forget", because we are not immediately concerned by the server
// response (if there is an error, it could be handled by an app-wide error
// reducer in the store, for example).
if (e.target.checked) {
this.props.dispatch(addVenueToList(venueId, listId));
} else {
this.props.dispatch(removeVenueFromList(venueId, listId));
}
}
}
SaveToList.propTypes = propTypes;
SaveToList = connect(store)(SaveToList);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment