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
- 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
- Fetch data and store result inside its state
- Pass the state into Pure via prop
CommentList
- Fetch data and store result inside its state
- 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
- 334