data handling in react: callbacks, flux, and redux
TRANSCRIPT
REACT
CALLBACKS, FLUX AND REDUX
ABOUT ME
REACT INTRO
> React is a view library
A COMPONENT
var Comment = React.createClass({ render: function() { return ( <p> <span>Comment: </span> <span>{this.props.body}</span> </p> ) }});
var CommentList = React.createClass({ render: function() { return ( <div className="commentList"> <h1>Comments</h1> <Comment body="try React!" /> </div> ) }});
jsfiddle demo
REACT MYTHS
> MYTH: React requires ES6> TRUTH: ES5 will work fine> MYTH: I have to learn Flux
> TRUTH: There's no need to start with Flux and many other patterns can be used
instead
BUT WHAT ABOUT THE DATA?
This talk will cover 3 different patterns to handle data in React
CALLBACKS
> Best way to get started> Great for Hierarchical applications
> Great if few clientside changes
SHOW CALLS AND TEXT MESSAGES
<Application > <CallList> <Call id=1 /> <Call id=2 /> <Call id=3 /> </CallList> <TextMessageList> <TextMessage id=1 /> <TextMessage id=2 /> </TextMessage></Application>
> Top level Application component manages data> (read only) data is passed down component hierarchy
> See simple-callback branch
I WANT TO TEXT SOMEONE WHO CALLED ME
var Application = React.class({ ...
newText: function(textInfo) { this.setState({ messages: _.concat(this.state.messages, { id: this.state.messages.length + 1, from: textInfo.from, number: textInfo.number, status: "draft" }) }); }
render: function() { return ( <div> <CallList calls={this.state.calls} sendText={this.newText} /> <TextMessageList messages={this.state.messages} /> </div> ) }});
> This impacts two independent parts of the applicaiton> Must touch every component between Application and Call to provide a callback
> see text-from-call Pull Request
I WANT TO INTEGRATE THIS INTO A LARGER APPLICATION
<Application> <Directory> <Contact /> <Contact /> </Directory> <OtherImportantStuff /> <Communications > <CallList /> <TextMessageList /> </Communications ></Application>
> I didn't even attempt this one..
FLUX
> Flux is a pattern, NOT an implementation> Consistent way for data to flow throughout
the application
OVERVIEW OF FLUX
OVERVIEW OF FLUX
> One way flow> A single event is asynchronously dispatched
> Data only changed by the Store> Doesn't have to be for React
ACTION (CREATORS)
let actions = { addText(textInfo) { // example of error checking if ( textInfo.from ) { this.dispatch(constants.DRAFT_TEXT, textInfo) } },
fetchCalls() { var result = API.getCalls(); this.dispatch(constants.FETCH_CALLS, result); },
fetchMessages() { var result = API.getMessages(); this.dispatch(constants.FETCH_MESSAGES, result); }};
> dispatch an action to the dispatcher> multiple or 0 stores can listen
> may or may not have data> Good spot for business logic and error handling
> Can query stores or interface with API> See flux Pull Request
CALLING AN ACTION CREATOR FROM REACT COMPONENT
import React, { Component } from 'react';import { ListGroupItem, Button, Row, Col } from 'react-bootstrap';import { Icon } from 'components/Icon';
export class Call extends Component {
textBack = () => { this.props.flux.actions.addText(this.props); };
render() { return ( <ListGroupItem header={this.props.from} > <span>{this.props.number}</span> <Button bsStyle="success" onClick={this.textBack} className="text-caller" > <Icon type="envelope"/> </Button> </ListGroupItem> ); }}
STORES
var MessageStore = Fluxxor.createStore({ initialize() { this.messages = [];
this.bindActions( constants.FETCH_MESSAGES, this.onMessagesFetched, constants.DRAFT_TEXT, this.onDraftText ); },
getMessages() { return this.messages },
onMessagesFetched(messages) { this.messages = messages; this.emit('change'); },
onDraftText(textInfo) { this.messages.push({ id: this.messages.length + 1, from: textInfo.from, number: textInfo.number, status: "draft" });
this.emit('change'); }});
> Is NOT the same as a model, concerned with single applicaiton domain> Likely to have many
> emit is change events notifies views (components)> Any component anywhere can "listen" for data changes and read directly form it
> See flux Pull Request
USING THE STORE IN A REACT COMPONENT
export class TextMessageList extends Component {
constructor(props) { super(props); this.state = { messages: [] }; }
componentDidMount() { this.props.flux.store('MessageStore').on('change', this.updateMessages) }
componentWillUnmount() { this.props.flux.store('MessageStore').removeListener('change', this.updateMessages); }
updateMessages = () => { this.setState({ messages: this.props.flux.store('MessageStore').getMessages() }); };
messageNodes = () => { return this.state.messages.map( (message) => { return <TextMessage key={message.id} {...message} flux={this.props.flux} /> }); };
render() { ... }}
REDUX
> A very popular alternative to Flux, which focuses on developer experience
> Single Store> No Dispatcher
> Store consists of a reducer (pure function with no side effects)
THE REDUCER
function messages(state = [], action) { switch(action.type) { case 'DRAFT_TEXT': return state.concat([{ id: state.length + 1, from: action.from, number: action.number, status: "draft" }]); case 'FETCH_MESSAGES': return action.messages default: return state }}
> "updates" data> pure function to calculate next state from current state and an action
> must not mutate state> see redux pull request
COMPOSING REDUCERS
// sub reducerfunction calls(state = [], action) { if (action.type === 'FETCH_CALLS') { return action.calls else { return state }}
// main reducerfunction communcationsApp(state = {}, action) { return { messages: messages(state.messages, action), calls: calls(state.calls, action) }}
> A reducer can be composed of multiple sub reducers> redux has a helper combineReducers
> see redux pull request
STORE?
> The Store just takes a reducer to calculate state, and stores that value
> It is the only object required, similar to flux instance
REACT COMPONENT GETTING STATE FROM REDUX STORE
export class TextMessageList extends Component {
constructor(props) { super(props); this.state = { messages: [] }; }
componentDidMount() { this.unsubscribe = this.props.store.subscribe(this.updateMessages); }
componentWillUnmount() { this.unsubscribe(); }
updateMessages = () => { this.setState({ messages: this.props.store.getState().messages }); };
render() { ... }}
see redux pull request
ACTION CREATORS
export function addText(textInfo) { // example of error checking if ( textInfo.from ) { return Object.assign({}, textInfo, { type: 'DRAFT_TEXT'} ) } else { return {} }}
export function fetchCalls() { // not making async for simplicity return { type: 'FETCH_CALLS', calls: API.getCalls() }}
export function fetchMessages() { // not making async for simplicity return { type: 'FETCH_MESSAGES', messages: API.getMessages() }}
> Action Creators do not dispatch directly> They are shortcuts to return a properly formatted action object
> see redux pull request
USING THE ACTION CREATOR IN A COMPONENT
import React, { Component } from 'react';...import { addText } from 'myRedux/actions';
export class Call extends Component {
textBack = () => { this.props.store.dispatch(addText(this.props)); };
render() { return ( <ListGroupItem header={this.props.from} > <span>{this.props.number}</span> <Button bsStyle="success" onClick={this.textBack} className="text-caller" > <Icon type="envelope"/> </Button> </ListGroupItem> ); }}
see redux pull request
REACT COMPONENT TYPES
> Presentational Components> All behaviours are passed as props
> Only knows how to render UI> Container Components
> Connect with Stores> specify behavior
> Minimal or no markup
RESOURCES> React Ecosystem Overview (Pete Hunt)
https://github.com/petehunt/react-howto> React Transform Boilerplate
https://github.com/gaearon/react-transform-boilerplate
> Source Code from Talk https://github.com/droberts84/react-flux-
redux-talk
FLUX RESOURCES
> Official Flux page https://facebook.github.io/flux
> Alt http://alt.js.org/
> Fluxxor http://fluxxor.com/
REDUX RESOURCES
> Egghead.io course (Dan Abramov) https://egghead.io/series/getting-started-
with-redux> Offical Redux Page
http://redux.js.org/