This document outlines a method of structuring React
components, in a clean, efficient, managable way. Its based loosely around https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0, but gives a much more indepth description on how everything should fit together.
It outlines what each component type is responsible for, and how they are supposed to be structured.
app/
├── routes/
│ └── routing.jsx
├── pages/
│ └── todos.jsx
├── containers/
│ └── todo-list.jsx
└── presentational/
├── item.jsx
├── header.jsx
├── content.jsx
└── table.jsx
So, routes, are the simplest components you can have. They are a definition of a route
combined with a page
component.
Routes typically look something like below: (this example uses react-router
but the principles can be applied anywhere)
// app/routes/routing.jsx
import React from 'react';
import { Router, Route, browserHistory } from 'react-router';
import TodoPage from '../pages/todos';
export default function Routes() {
return (
<Router history={browserHistory}>
<Route path="/todos" component={TodoPage} />
</Router>
);
}
- These components are stateless, functional
React
components. - They are responsible for hooking up routes to
page
components
Page components are responsible for taking any of the routing specific logic, and converting this into props
for other components.
They look something like this:
// app/pages/todos.jsx
import React from 'react';
import TodoList from '../containers/header';
import Content from '../presentational/content';
import Title from '../presentational/title';
export default function TodosPage(props) {
return (
<div>
<Content>
<Title>Todos!</Title>
<TodosList />
</Content>
</div>
);
}
If you had some route parameters, this is where you take that route parameter, and convert it into props
for other components or containers.
- These components are used to map route parameters to
props
onto other components or containers - These components are stateless, functional components. No classes here!🌟
- They can nest
containers
andpresentational
components!
Containers, or the principles behind them, is stolen straight from relay
. It's the idea of removing data
requirements from components.
And instead of having to go fetch data on componentWillMount
and then having internal state, move that out into something called a higher order component
.
This deals with the data dependencies for you, and will automagically push them onto your component as props
.
Let's say I have an endpoint to go and get my todos. Its on /api/v1/todos/all.json
. Heres how it would all fit together.
// app/containers/todo-list.jsx
import React from 'react';
import fetchContainer from 'container';
import Table from '../presentational/table.jsx';
class TodoList extends React.Component {
constructor(props) {
super(props);
this.state = {
selected: [],
}
}
onClick = () => {
//do something
}
render() {
if (!this.props.items) {
return null;
}
return <Table items={this.props.items} onItemClick={(item) => this.onClick(item)} />
}
}
export default fetchContainer(
TodoList, {
data() {
return {
items: '/api/v1/todos/all.json'
}
}
}
);
- These components can have, but might not always have state.
- They always export a container component. Which has data passed in, it doesn't fetch itself.
- They then can mutate this data, before passing it onto presentational components
- Containers can embed multiple containers if they wish.
This is kind of the pièce de résistance. It's the thing I've kind of built to take away the data requirements out of views. A really simple, one time fetch, looks somethng like this:
import React from 'react';
import 'whatwg-fetch';
export default function create(ReactClass, dataRequirements) {
return class Container extends React.Component {
constructor(props) {
super(props);
this.state = {
data: {},
dataRequirements: dataRequirements.data(props),
};
}
handleResponse = (response) => {
if (response && response.status === 200) {
return response.json();
}
const error = new Error(response.statusText);
error.code = response.status;
throw error;
}
createFetch = (url) => {
const requestArguments = {
credentials: 'same-origin',
headers: {
accept: 'application/json',
},
};
return fetch(url, requestArguments)
.then(this.handleResponse);
}
componentDidMount() {
// Fetch data for the endpoints needed
Promise.all(
Object.keys(this.state.dataRequirements)
.map((propKey) => {
const url = this.state.dataRequirements[propKey];
return this.createFetch(url).then(
(body) => (this.state.data[propKey] = { body, error: null })
).catch(
(error) => (this.state.data[propKey] = { body: null, error })
);
})
).then(() => this.forceUpdate());
}
render() {
return <ReactClass {...this.props} {...this.state.data} />;
}
};
}
And it is used by following the example above.
Presentation components are the last, but not the least. They are where you translate this data that you've got from high up at a service,
into div
elements on a page.
For example:
// app/presentational/table.jsx
import React from 'react';
import Item from './Item';
export default function Table(props) {
return props.rows.map((row) => <Item {...row} onItemClick={props.onItemClick} />);
}
// app/presentational/item.jsx
import React from 'react';
export default function Item(props) {
return (
<tr onClick={props.onItemClick>
<td>{props.id}</td>
<td>{props.name}</td>
<td>{props.due}</td>
</tr>
)
}
- These components are majority of the time stateless.
- They only have state when dealing with view state, like is button clicked. etc...
- Only nest other presnetational components (Never come across a case where you nest containers in presentational components, but there might be a legit case).