Go With The Flow

Introduction to data management in React applications

React is not a framework.

Freedom is not free

  • how do I store and manage data?
  • how do I do AJAX?
  • routing?
  • animations?
  • tests?
  • deploy?
  • ...

Introduction to data management

Agenda:

  • root component's state
  • container components
  • Flux
  • Redux

Root component's state

class App extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            comments: []
        };
    }
    componentDidMount() {
        DB.get({ success: this.updateData });
    }
    updateData = comments => {
        this.setState({ comments });
    };
    handleCommentSubmit = comment => {
        DB.put({ data: comment, success: this.updateData });
    };
    render() {
        return (
            <div className="commentBox">
                <h1>{'Comments'}</h1>
                <CommentList comments={this.state.comments} />
                <CommentForm onCommentSubmit={this.handleCommentSubmit} />
            </div>
        );
    }
}

Example

Root component's state

Pros and cons

simple

not scaleable

Root component's state

hard to use with deep components tree

only for prototypes and very small apps

Container (Smart) Components

// Root component can be used just for composing container components
function App() {
  return (
    <div className="app">
      <Header>
        <RandomCat />
      </Header>
      <CommentBox />
      <ContactInfo />
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById('app'));

No logic in root anymore

Container Components

Container Components

class CommentBox extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      comments: []
    };
  }
  componentDidMount() {
    DB.get({ success: this.updateData });
  }
  updateData = comments => {
    this.setState({ comments });
  };
  handleCommentSubmit = comment => {
    DB.put({ data: comment, success: this.updateData });
  };
  render() {
    return (
      <div className="commentBox">
        <h1>{'Comments'}</h1>
        <CommentList comments={this.state.comments} />
        <CommentForm onCommentSubmit={this.handleCommentSubmit} />
      </div>
    );
  }
}

CommentBox now just a sub-app

Container Components

// each Container Component is responsible for its own data
class RandomCat extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      cat: ''
    };
  }

  componentDidMount() {
    DB.getCat({ success: this.updateData });
  }

  updateData = cat => {
    this.setState({ cat });
  };

  render() {
    return (
      <div className="random-cat">
        <img src={this.state.cat} />
      </div>
    );
  }
}

RandomCat is another sub-app

Container Components

class ContactInfo extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      contacts: []
    };
  }

  componentDidMount() {
    DB.getContacts({ success: this.updateData });
  }

  updateData = contacts => {
    this.setState({ contacts });
  };

  render() {
    return (
      <div className="contact-info">
        <ContactList contacts={this.state.contacts} />
      </div>
    );
  }
}

There can be many of them

Container Components

Presentational (Dumb) Components

function Header({ children }) {
  return <div className="header">{children}</div>;
}

function Contact({ contact }) {
  return (
    <div className="contact">
      <h3 className="contact-type">{contact.type}</h3>
      <i className="contact-value">{contact.value}</i>
    </div>
  );
}

function Comment({ author, children }) {
  return (
    <div className="comment">
      <h2 className="commentAuthor">{author}</h2>
      {children}
    </div>
  );
}

Presentational (Dumb) Components

Container Components

function CommentList({ comments }) {
  return (
    <div className="commentList">
      {comments.map(comment => {
        return (
          <Comment author={comment.author} key={comment.id}>
            {comment.text}
          </Comment>
        );
      })}
    </div>
  );
}

function ContactList({ contacts }) {
  return (
    <div className="contact-list">
      {contacts.map(contact => <Contact key={contact.type} contact={contact} />)}
    </div>
  );
}

Presentational (Dumb) Components

Container Components

Container Components

Pros and cons

still simple

more or less scaleable

Container Components

ok to use with deep component tree

problems when components need the same data in different places

data flow is hard to manage and reason about

hard to debug

Flux

fluxus — “flow” in Latin

One-way data flow

Flux

It's simpler than you think

Flux

Data Hub

View

“do something”

“done”

React Component

 

Store

 

Emitting event:

Dispatching action:

Flux in a nutshell

Flux

Dispatcher

+

Event emitter

+

The idea of one-way data flow

Dispatcher

Flux

// create dispatcher instance
const AppDispatcher = new Flux.Dispatcher();
class CommentBox extends React.Component {
  // ...
  handleCommentSubmit = comment => {
    comment.id = Date.now();

    // create an action
    AppDispatcher.dispatch({
      actionType: 'NEW_COMMENT',
      comment
    });

    // ...
  }
  // ...
}

action

Action creators

Flux

class CommentBox extends React.Component {
  // ...
  handleCommentSubmit = comment => {
    ActionCreators.addComment(comment);
  }
  // ...
}
const ActionCreators = {
  addComment(comment) {
    comment.id = Date.now();

    AppDispatcher.dispatch({
      actionType: 'NEW_COMMENT',
      comment
    });

    DB.putComment({
      data: comment,
      success(comments) {
        AppDispatcher.dispatch({
          actionType: 'COMMENTS_UPDATED',
          comments
        });
      }
    });
  }
};

Store

Flux

class CommentStore {
  onDispatch = payload => {
    // ...
  };
}

const commentStore = new CommentStore();

AppDispatcher.register(commentStore.onDispatch);
class CommentStore extends EventEmitter {
  onDispatch = payload => {
    switch (payload.actionType) {
      case 'NEW_COMMENT':
        this.emit('NEW_COMMENT', payload.comment);
        break;
    }
  };
}

const commentStore = new CommentStore();

AppDispatcher.register(commentStore.onDispatch);

Store

Flux

class CommentStore extends EventEmitter {
  constructor() {
    super();

    this.comments = [];
  }
  getComments() {
    return this.comments;
  }
  onDispatch = payload => {
    switch (payload.actionType) {
      case 'NEW_COMMENT':
        this.emit('NEW_COMMENT', payload.comment);
        break;

      // update data in the store and notify components
      case 'COMMENTS_UPDATED':
        this.comments = payload.comments;
        this.emit('COMMENTS_UPDATED');
        break;
    }
  };
}

Component: closing the circle

Flux

class CommentBox extends React.Component {
  // ...
  componentDidMount() {
    commentStore.on('NEW_COMMENT', this.addComment);
    // ...
  }
  addComment = comment => {
    this.setState({
      comments: this.state.comments.concat([ comment ])
    });
  };
  handleCommentSubmit = comment => {
    ActionCreators.addComment(comment);
  };
  // ...
}

1. create an action

2. update the state only when that action made a full circle

Component: data from the store

Flux

class CommentBox extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      comments: commentStore.getComments()
    };
  }
  componentDidMount() {
    // ...
    commentStore.on('COMMENTS_UPDATED', this.updateData);

    ActionCreators.getComments();
  }
  updateData = () => {
    this.setState({
      comments: commentStore.getComments()
    });
  };
  // ...
}

1. create an action

2. update the state

Pros and cons

good for middle-size and big applications

easy for components to get the data they need

might be hard to understand

unclear how to store data

no common patterns -> no ecosystem

Flux

data-flow is predictable

server-rendering is hard

hard to debug

Redux

* unofficial logo

*

Three principles of Redux

Redux

  • Single source of truth
    The whole state of your application is stored just in one place
     
  • State is read-only
    You can not change it from outside directly
     
  • Changes are made with pure-functions
    They called reducers and they react to the actions sent by components to change the state

Actions: creating

Redux

// turn data into action
const ActionCreators = {
  addComment(comment) {
    return {
      type: 'NEW_COMMENT',
      comment
    };
  }
};
// dispatching is somewhere outside of action creators
store.dispatch(ActionCreators.addComment(comment));

Actions: dispatching

Redux

// function dispatching created action
function addComment(comment) {
  return store.dispatch(ActionCreators.addComment(comment));
}
// we are passing this function to the component
function CommentBox() {
  return (
    <div className="commentBox">
      <h1>{'Comments'}</h1>
      <CommentList comments={this.props.comments} />
      <CommentForm onCommentSubmit={addComment} />
    </div>
  );
}
// component calling this function
class CommentForm extends React.Component {
  handleSubmit = e => {
    // ...
    this.props.onCommentSubmit({ author, text });
  },
  // ...
}

Reducers

Redux

// transforming the state depending on dispatched action, data, etc.
function commentsReducer(state = [], action) {
  switch (action.type) {
    case 'NEW_COMMENT':
      return state.concat(action.comment);
    case 'COMMENTS_UPDATED':
      return action.comments;
    default:
      return state;
  }
}
// each reducer is responsible only for its piece of data
function rootReducer(state = {}, action) {
  return {
    comments: commentsReducer(state.comments, action)
  };
}

Store: creating

Redux

// store is a singletone with two arguments: root reducer and initial data
const store = Redux.createStore(rootReducer, {
  comments: [
    {
      author: 'Pete Hunt',
      id: 1388534400000,
      text: 'Hey there!'
    },
    {
      author: 'Paul O’Shannessy',
      id: 1420070400000,
      text: 'React is *great*!'
    }
  ]
});

Store: subscribing

Redux

// every time the state changes
let unsubscribe = store.subscribe(function() {
  console.log(store.getState())
)

// dispatch some actions
store.dispatch(ActionCreators.addComment({ author: 'mom', text: 'hi' }));
// > Object { comments: Array[3] }
store.dispatch(ActionCreators.addComment({ author: 'dad', text: 'bye' }));
// > Object { comments: Array[4] }


// Stop listening to state updates
unsubscribe()

Data flow

Redux

React

Component

Action

Creator

// dispatching action
{
  type: 'NEW_COMMENT',
  comment
}
comment

Root
Reducer

Store

// current state
{
  comments: Array[3]
}

Comment

Reducer

// action
{
  type: 'NEW_COMMENT',
  comment
}

// part of the state
// (comments)
Array[3]
// changed part
// of the state
Array[4]
// changed state
{
  comments: Array[4]
}
// to subscribers
{
  comments: Array[4]
}

1

2

2

3

4

5

6

+

Connecting to React

Redux

React: Container -> Presentational

Redux

React: passing the store

Redux

import { Provider } from 'react-redux';

const store = Redux.createStore(app, {
  comments: [
    // ...
  ]
});

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('app')
);

React: container component

Redux

import { connect } from 'react-redux';

// combine state selectors and map them to props
const mapStateToProps = state => {
  return {
    comments: state.comments
  };
};

// create functions for dispatching actions and map them to props
const mapDispatchToProps = (dispatch, ownProps) => {
  return {
    onCommentSubmit(comment) {
      dispatch(ActionCreators.addComment(comment));
    }
  };
};

// generate Container component
const AppCommentBox = connect(
 mapStateToProps,
 mapDispatchToProps
)(CommentBox);

React: presentational component

Redux

// doesn't know about Redux
class CommentBox extends React.Component {
  render() {
    return (
      <div className="commentBox">
        <h1>{'Comments'}</h1>


        <CommentList comments={this.props.comments} />


        <CommentForm onCommentSubmit={this.props.onCommentSubmit} />
      </div>
    );
  }
}
// part of the state is coming through props
// function for dispatching an action

Perf optimizations

Redux

class SomeComponent extends React.Component {
  shouldComponentUpdate(nextProps) {
    // props will never change by reference
    // because redux store is immutable
    if (nextProps.data === this.props.data) {
        return false;
    }

    return true;
  }
  render() {
    // ...
  }
}

Dev-tools and time travel

Redux

Pros and cons

good for middle-size and big applications

easy for components to get the data they need

might be hard to understand

clear patterns on how data should be stored

HUGE ecosystem (middleware, debug tools, etc.)

Redux

data-flow is predictable

server-rendering is easy

very easy to debug

Links

Thanks!

Go With The Flow

By Denis Koltsov

Go With The Flow

Introduction to data management in React applications

  • 857