Skip to content

Instantly share code, notes, and snippets.

@benjdlambert
Last active June 5, 2021 21:41
Show Gist options
  • Save benjdlambert/e9b7dff9e56e49ab108a09dcddb6d854 to your computer and use it in GitHub Desktop.
Save benjdlambert/e9b7dff9e56e49ab108a09dcddb6d854 to your computer and use it in GitHub Desktop.
Route -> Page -> Container -> Presentation

Route -> Page -> Container -> Presentation

What is this?

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.

Recommended File Structure

app/
├── routes/
│   └── routing.jsx
├── pages/
│   └── todos.jsx
├── containers/
│   └── todo-list.jsx
└── presentational/
    ├── item.jsx
    ├── header.jsx
    ├── content.jsx
    └── table.jsx

Routes

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>
  );
}
Things to note!
  • These components are stateless, functional React components.
  • They are responsible for hooking up routes to page components

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.

Things to note!
  • 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 and presentational components!

Containers

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'
      }
    }
  }
);
Things to note!
  • 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.

The Fetch Container

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.

Presentational

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>
  )
}
    

Things to note

  • 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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment