Created
August 2, 2015 19:20
-
-
Save nicolashery/fb9df874ba2e4015dde0 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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