Select Page

Here is a cheat-sheet-style summary of the component structure described in The Granger Component Taxonomy post, as written and imagined by Izaak Schroeder and Neal Granger.

For the “why” of this post, see the Summary & Disclaimer at the bottom.

Folder Structure

/src
/component
/base
/debug
/partial
/root
/static
/util
/view

Base Components

  • Focused on brand and often reference brand-specific variables.
  • Give UI re-use.
  • MUST not access application state.
  • MAY access visual state (e.g. react context providing theme, spacing)
  • MUST be composed of util components and other base components.
  • MUST encapsulate styling from rest of the app.
  • MUST have props with only enumerable values.
Example base component: <Button />
import styled from 'styled-components';

const Button = styled.button`
appearance: none;
border: solid 2px ${({theme}) => theme.myBrandRed};
padding: 0.2em 0.4em;
`;

export default Button;

View Components

  • Typically include many partial components or sub-views that build out the main functionality of the view.
  • Typically include one layout component defining the layout of the view (i.e. headers and footers).
  • MAY be composed of any class of components.
  • MUST not contain cross references to sibling or parent views.
Example view component: <CartView />
import * as React from 'react';

import MainLayout from '/component/layout/MainLayout';
import Button from '/component/base/Button';
import ShoppingList from '/component/partial/ShoppingList';

import CartDetailsSubView from './CartDetails';

class CartView extends React.PureComponent {
render() {
return (
<MainLayout>
Cart
<ShoppingList />
<CartDetailsSubView />
<Button type="cta">Checkout</Button>
</MainLayout>
);
}
}

Root Components

  • Typically include application routing.
  • Typically provide global data or state to the rest of the component tree.
  • MUST be mounted somewhere in code by ReactDOM.render or registered by AppRegistry.registerComponent if using react-native.
  • MUST be composed of view components.
  • MAY include provider components (e.g. redux’s Provider).
Example root component: <AppRoot />
import * as React from 'react';
import {ThemeProvider} from 'styled-components';
import {Switch, Route} from 'react-router';

import HomeView from '/component/view/HomeView';
import SettingsView from '/component/view/SettingsView';
import NotFoundView from '/component/view/NotFound';


import theme from '/config/theme.config';

class AppRoot extends React.PureComponent {
static domNodeId = "app";

render() {
return (
<Router>
<ThemeProvider theme={theme}>
<Switch>
<Route
exact
path="/"
component={HomeView}
/>
<Route
path="/settings"
component={SettingsView}
/>
<Route component={NotFound} />
</Switch>
</ThemeProvider>
</Router>
);
}
}

Partial Components

  • Presentational and behavioural.
  • Give UX re-use.
  • MUST be composed of base components or other partial components
  • MUST be re-used by multiple views (otherwise it belongs with the view).
  • MAY contain some local, very purpose-specific styles.
  • MAY be connected to state (reduxapollo, etc.).
Example partial component: <SearchResults />
import * as React from 'react';
import {Query} from 'react-apollo';
import gql from 'graphql-tag';

import List from '/component/base/List';

class SearchResults extends React.PureComponent {
  render() {
    return (
      <List>
        {results.map((result) => (
          <List.Item key={result.id}>
            {result.productName}
          </List.Item>
        )}
      </List>
    );
  }
}

const PRODUCT_SEARCH = gql`
  query search($searchQuery: String!) {
    search(searchQuery: $searchQuery) {
      id
      productName
    }
  }
`;

const ConnectSearchResults = ({searchQuery}) => (
  <Query query={PRODUCT_SEARCH} variables={{searchQuery}}>
    {({data}) => (
      <SearchResults results={data.results} />
    )}
  </Query>
);

Utility Components

  • Give behavioural re-use.
  • MUST be behavioural, not presentational.
  • MUST not render branding/content-based visual elements.
  • MAY return other visual elements in rare cases.
Example utility component: <OnClickOutside />
import * as React from 'react';

class OnClickOutside extends React.PureComponent {
  container = React.createRef();
  isTouch = false;

  componentDidMount() {
    document.addEventListener('touchend', this.handle, true);
    document.addEventListener('click', this.handle, true);
  }

  componentWillUnmount() {
    document.removeEventListener('touchend', this.handle, true);
    document.removeEventListener('click', this.handle, true);
  }

  handle = (e) => {
    if (e.type === 'touchend') this.isTouch = true;
    if (e.type === 'click' && this.isTouch) return;
    const {onClickOutside} = this.props;
    const el = this.container.current;
    if (el && !el.contains(e.target)) {
      onClickOutside(e);
    }
  };

  render() {
    return this.props.children(this.container);
  }
}

Debug Components

  • Provide value to the development team, not to the product owner.
  • MAY be excluded from the production build or certain environments.
  • MUST be composed of base components or partial components.
Example debug component: <DebugInfo />
import * as React from 'react';

import pkg from '/../package.json';

class DebugInfo extends React.PureComponent {
  render() {
    return (
      <div>
        Version {pkg.version}
      </div>
    );
  }
}

Static Components

  • MUST function correctly as markup (i.e. the behaviour of renderToString is well defined for the component).
  • MUST not access the DOM or use component lifecycle methods.
Example static component: <Page />
import * as React from 'react';

class Page extends React.PureComponent {
  render() {
    const {
      rootElementId, 
      assets, 
      markup, 
      state, 
      head,
    } => this.props;
    return (
      <html lang="en">
        <head>
          <meta charSet="utf-8" />
          <meta httpEquiv="x-ua-compatible" content="ie=edge" />
          {styles(assets.index)}
        </head>
        <body>
          <div
            id={rootElementId}
            className="root"
            dangerouslySetInnerHTML={{__html: markup}}
          />
          {typeof state !== 'undefined' && (
            <script
              type="text/json"
              id="state"
              dangerouslySetInnerHTML={{__html: serialize(state)}}
            />
          )}
          {scripts(assets.index)}
        </body>
      </html>
    );
  }
};

Layout Components

  • Purely presentational.
  • MUST be consumed by view components.
Example layout component: <MainLayout />
import * as React from 'react';

import Header from '/component/partial/Header';
import Footer from '/component/partial/Footer';

class MainLayout extends React.PureComponent {
  render() {
    return (
      <div>
        <Header />
        {children}
        <Footer />
      </div>
    );
  }
}

Summary & Disclaimer

This post was created as a short-form reference in appreciation of all of the wonderful work from Izaak and Neal.

I highly advise reading the original article as it explains in detail the reasoning and implementation of each of these components.