Presentational vs Container Component

WHY, though?

What problem does it solve &

how does it help me exactly?

What's wrong with this code?

import React, { Component } from 'react';
import axios from 'axios';

export default class CommentList extends Component {
  state = {
    data: {
      comments: [],
      loading: false,
    },
  }

  onLoadData = () => {
    this.setState({ data: { loading: true } });
    axios.get('http://my-api.com/comments')
      .then(comments => this.setState({data: { comments, loading: false }}));
  }

  render() {
    if (this.state.data.loading) return <div>Loading, please wait ..</div>;
    return (
      <div>
        <ul>
          { 
            this.state.data.comments.map(({ id, body, author }) => 
                <li key={id}>{body} — {author}</li>) 
          }
        </ul>
        <button onClick={this.onLoadData} >Load data</button>
      </div>
    );
  }
}

What is it?

  • Single, giant component that does too many things
  • Imperative style instead of declarative
  • Logic is jumbled up with Template
  • Side effects is tightly coupled to the component

Why is it bad?

  • No Separation of Concerns
  • Hard to maintain & detect bugs
  • Difficult to read & collaborate with Designer
  • Low Re-usability

Ok, i'm sold. WHAT is it?

Teach me master!

Presentational

  • Also known as:
    • dumb / stateless / pure
  • Concerned on how things look
  • Does not manage its own state
  • Receive data via props
  • Renders markup
  • Written as functional stateless component

Container

  • Also known as:
    • smart / impure
       
  • Concerned on how things work
  • Manage its own state
     
  • Provide data as props
  • Talks to the store
  • Written as class syntax or generated via higher order components

Benefits of this Approach

  • Prevent abuse of `setState()` API, favoring props instead
  • Encourage reusable & modular code
  • Discourage giant, complicated components that do too many things.
  • Better performance by avoiding unnecessary checks & memory allocations.

HOW?

Enough theory, let's refactor the code!

Step 1: Pure & Container

<CommentListPure data={this.state.data} />

CommentContainer

  1. Fetch data and store result inside its state
  2. Pass the state into Pure via prop

CommentList

  1. Fetch data and store result inside its state
  2. Renders the comments himself

Step 1: Pure & Container

import React, { Component } from 'react';
import axios from 'axios';

const CommentListPure = ({ data: { comments }, onLoadData }) => (
  <div>
    <ul>
      { comments && comments.map(({ body, author }) => <li>{body}—{author}</li>) }
    </ul>
    <button onClick={onLoadData}>Load data</button>
  </div>
);

export default class CommentList extends Component {
    state = {
      data: {
        comments: [],
        loading: false,
      },
    }

    onLoadData = () => {
      this.setState({ data: { loading: true } });
      axios.get('http://my-api.com/comments')
        .then(comments => this.setState({ data: { comments, loading: false }}));
    }

    render() {
      if (this.state.loading) return <div>Loading data, please wait ..</div>;
      if (this.state.data.comments.length) return <div>No data has been loaded (yet).</div>;
      return <CommentListPure data={this.state.data} onLoadData={this.onLoadData} />;
    }
}

Step 2: Higher Order Component

withStateHOC

withDataHOC

<CommentListPure />

withHandlersHOC

withDataHOC />

Compose

Step 2: Higher Order Component

import React from 'react';
import axios from 'axios';
import { compose, withState, withHandlers } from 'recompose';

const CommentListPure = ({ data: { comments }, onLoadData }) => (
...
);

const dataState = withState(
  // state name
  'data', 
  
  // updater method name
  'setData', 

  // default state
  { comments: [], loading: false } 
);

const onLoadDataFromREST = (url) => withHandlers({
  onLoadData: ({ setData }) => () => {
    setData({ loading: false });
    axios.get(url)
      .then(comments => {
        setData({ comments, loading: true });
      });
  },
});

const withData = compose(
  dataState,
  onLoadDataFromREST('http://my-api.com/comments'),
);

export default withData(CommentListPure);

Step 3: Loading HOC

withHandlersHOC

withCommentsHOC

<CommentListPure />

withLoadingBar

withDataHOC />

Compose

withStateHOC

withUsersHOC

<UserListPure />

Step 3: Loading HOC

<CommentListPure />

RestContainer

APP 1

graphql HOC

APP 2

Step 4: Use a different backend

<CommentListPure />

redux Connect HOC

APP 3

<CommentListPure />

Step 4: Use a different backend

import React from 'react';
import gql from 'graphql-tag';
import { graphql } from 'react-apollo';
import { compose, withState, withHandlers, branch, renderComponent } from 'recompose';

const CommentListPure = ({ data: { comments }, onLoadData }) => (
...
);

const data = graphql(gql`
    query CommentsQuery {
        comments {
          id
          body
          author
        }
    }
`);

const Loading = () => (
...
);

const displayLoadingState = branch(
...
);

const withData = compose(
  data,
  displayLoadingState,
);

export default withData(CommentListPure);

Final step: Refactor into their own separate files!

Presentational vs Container

By Wan Mohd Hafiz

Presentational vs Container

  • 345