The Granger Component Taxonomy

Thine one true path to component organization.

Izaak Schroeder
Bootstart
13 min readDec 4, 2018

--

Although there are countless ways to organize your components, choosing how you separate them can have important implications to your development process and your final product. This article will provide a brief overview of a very specific set of categorizations, examples and usage of each, and, where applicable, links to additional information.

Relationships between the component types with arrows denoting a “has a” relationship.

For lack of a better title, I will henceforth refer to this pattern as The Granger Taxonomy™, seeing as it was largely inspired and developed through work I’ve done with Neal Granger over the past few years. The folder structure looks a little something like this:

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

Since the questions will invariably arise: the rationale behind the folder naming convention, (why component instead of components or why util instead of utilities), can be explained through “The Principle of Least Typing”; the use of imports that look like /component/base/Foo are documented in “Using Custom Resolvers Properly”.

The following component categories are listed in order of “essentialness” to an application, from most essential to least essential.

Base Components

Base components are the foundation upon which everything else is built. They are focused on the product brand and provide visual consistency for the entire project. Base components form a “UI kit” of sorts and include things like grids, containers, buttons, form controls, icons, progress meters, flyouts, text, alerts, spacing components and modals.

Example (incomplete) set of base components.

This class of components is fundamentally the most important. Giving your project a set of base components provides the necessary building blocks to quickly build designs to specification, avoid code duplication and ensure visual fidelity is kept high.

All the parameters your design team specifies for the product brand are encoded in base components. This includes things like: colors, fonts, spacing amounts, the iconography, and any container/grid sizes.

Examples of parameters that should be encoded in base components.

You should be able to mock the above design up entirely with base components and should NOT see any references to styles or classes; the code might look something like this:

<Grid>
<Grid.Col span={4}>
<SpacedContent size="small">
<Text.H>Mango Shop</Text.H>
<Text.P>I like mangos...</Text.P>
<Button type="cta">Order Ice Cream</Button>
</SpacedContent>
</Grid.Col>
<Grid.Col span={8}>
<Image src={...} alt="Tasty" />
</Grid.Col>
</Grid>

Base components do NOT provide any data connectivity nor do they provide any business logic. You can think of the result of using base components as a mapping between a given Sketch file (or InVision link, Zeplin project, etc.) and some output in the browser/device.

Base components let you do this transformation effectively and accurately.

Because all of the spacing, colors, fonts, etc. are encoded in your base components, the result from using them should match up exactly with the design; if the result is off, it’s an indicator of one of two things:

  1. The base components are wrong or are missing a parameter. Maybe you used size="small" when you should have used size="large"; maybe the specific button type="outline" is missing and needs to be added to the Button component.
  2. The design is wrong or inconsistent. This happens a lot when teams iterate quickly and forget to update their design files. What was #deadbeef one day is #feedbeef the next. You can help force your design team to be consistent by pointing out that the "small" spacing in one design is not the same "small" spacing in another.

To help ensure this high level of consistency, base components should also only accept enumerable parameters. For example, say you have a base Button component and you can set its color. One possible naive approach might be to allow things like:

<Button color="#36384d">Click Me</Button>
Which of these is the “right” button?

Do NOT do this. You cannot enumerate (list) all possible values for color here. Instead, define a list of possible colors and have the button ONLY accept those values:

/* in some config file */
const colors = {
darkMidnight: "#36384d"
};
/* in some component */
<Button color="darkMidnight">Click Me</Button>

Generally, this rule applies to EVERY property (other than perhaps a very select few like children) on your base components: spacing, size, color, etc. If you can’t enumerate the values for a property you can’t test it, and you can’t ensure consistency across your application. For a much deeper discussion on this approach see “Theming Base Components”.

Note that it’s also possible to integrate this base components pattern into an existing UI framework like bootstrap, basscss, material-ui, semantic-ui, etc. To do so, have a look at “Base Components & Existing UI Kits”.

Defining characteristics of 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 <Button /> base component:

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;

For the rationale behind using styled-components as the basis for styling components, please see “Styled Components: Unequivocally Better”. However, you can use any styling system you like, including SASS, Less, vanilla CSS, or another CSS-in-JS solution.

View Components

View components represent complete app sections or pages or screens the user interacts with. They are a “complete” block of functionality and have a cohesive purpose or goal. Typically only one view component is active at any given time. If the design team gives you a complete page in Sketch (or InVision, Zeplin, etc.), this is probably a view.

Two example views sharing a common header.

NOTE: If you’re building a new app and don’t want to adopt this taxonomy wholesale, then having base and view is still enough to set you up for success.

Very complex or large views are sometimes split into smaller sub-views, but the key distinction here is that the sub-views are ONLY ever referenced by their parent view, never by another sibling view or any other component type. This means that views should form a forest (collection of trees), not a structure like a directed acyclic graph or directed cyclic graph.

Valid view hierarchies. This is a forest of trees.
Invalid view hierarchies. This is a collection of DAGs.

This is different from all other classes (base components, partial components, util components, etc.) which CAN contain cross references. If you are using webpack and you put each view component into its own chunk, you can use a tool like webpack analyze to see these reference relationships:

This is a result you DON’T want. The ONLY parent the non-main entries should have is “0”.

Because views are this cohesive unit of functionality and do not have circular or cross references they are very easy to reason about. If you’re being on-boarded to a project and you get a ticket saying “the payment page needs copy change”, your first stop is to the view folder, and you can begin drilling down from there since you know everything to do with the payment page will likely be in PaymentView.

Views are also prime targets for chunk splitting. Logic and components contained in one view are not present in any other view and so you’ll be able to get significant bundle size savings by doing things like this:

import Loadable from 'react-loadable';
import LoadingSpinner from '/component/base/LoadingSpinner';

export default Loadable({
loader: () => import('./MyView'),
loading: Loading,
});

Tools like react-loadable make this straightforward, but you can always use your own Loadable util component. The above example is also another reason for why “You Should be Using Folder Components”.

Because mapping page routes to views is also a very common strategy, most view components are referenced solely by the routing system somewhere in a root component. You can see examples of this in the root component section.

Defining characteristics of 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 <CartView /> view component:

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

Root components are used as the top-most level of the component hierarchy. Each root component has an associated target node in the DOM and represents an instance of a react component tree. If you were to export your entire application as a library to be used by others, this would be a reasonable place to do it.

Root component rendered into its target DOM node.

An application has at least one root component. In most cases for an SPA you will have your main AppRoot and potentially an ErrorRoot (see “Fail-First Error Handling for the Frontend” for more info on this latter component). If you are progressively upgrading an existing legacy application and have components injected into specific parts of your app (e.g. ReactDOM.render is part of a backbone view), or you have several entry points, then you may have more root components defined.

If you’re doing hot module reloading, or have global styles for your entire app, then AppRoot is the place these things are included:

import {hot} from 'react-hot-loader';
import './globalStyles';
// ...export default hot(module)(AppRoot);

Defining characteristics of 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 <AppRoot /> root component:

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

And then in an application entry point (e.g. client/client.js:

import * as ReactDOM from 'react';const element = document.getElementById(AppRoot.domNodeId);
ReactDOM.render(<AppRoot />, element);

Partial Components

Partial components are reusable blocks composed of base components that are consumed by view and layout components. As your app grows you will likely find common code between multiple view components, and this should be abstracted into a partial component. Partials are simply the natural evolution of wanting to DRY up your code. Common partials include things like: headers and footers, information panels/cards, and search result lists.

A key defining difference between base components and partial components is the consumption of state. No base component should access state or have side-effects beyond what is provided to it as props. Similarly, partial components should not have their own styles, since defining visual style is the responsibility of base components.

Defining characteristics of partial components:

  • Presentational and behavioral.
  • 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 (redux, apollo, etc.).

Example <SearchResults /> partial component:

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

Utility components are used to define common behaviour that does not represent visual or interactive UI elements. This means utility components are quite often either higher-order or they have function children (the latter of which is generally preferable). Common utility components often include: pluralizers, common key or mouse behaviour (e.g. handling the pressing of the escape key), timer components and accessibility wrappers, like those for visually hidden content.

import * as React from 'react';import VisuallyHidden from '/component/util/VisuallyHidden';const Error = ({message}) => (
<div>
<VisuallyHidden>Error: </VisuallyHidden>
{message}
</div>
);

Because utility components don’t have anything to do with branding and are purely behavioural, they can quite often be carried over from project to project (or even published to npm). These components rarely involve any business logic and rarely access application state. Just like base components allow UI re-use, and partials allow UX re-use, utility components allow behavioural re-use.

The “click outside of component” behaviour is a one that gets re-used regularly in most apps.

Because of their focus on behaviour, util components should not return styled or visible elements. There are very occasional exceptions to this; for example a pluralization component could add commas between its children and those would be visible elements.

Defining characteristics of 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 <OnClickOutside /> utility component:

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

Debug components help developers and QA testers do their job. A lot of times a new feature is being developed or a new API endpoint is being used and you would like an easy way to be able to toggle certain settings without having to rebuild the app or inject specific code into the build; debug components fulfill this need by providing access to application internals in a more user-friendly manner. Common debug components include things like configurators, theme adjustors and build information display panels.

Example set of debug components.

In many cases, because these components provide access to application internals, you may wish to disable them in production builds or on specific environments. Assuming you’re using folder components, you can simply adjust the index.js file to conditionally include your component in the build:

import BuildInformation from './BuildInformation';const index = (process.env.NODE_ENV === "production") ?
() => null : BuildInformation;
export default index;

Defining characteristics of 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 <DebugInfo /> debug component:

import * as React from 'react';import pkg from '/../package.json';class DebugInfo extends React.PureComponent {
render() {
return (
<div>
Version {pkg.version}
</div>
);
}
}

Static Components

Static components are used by an external process to render static parts of the page outside of the root component target nodes. This is typically just the page markup itself, but could include smaller static markup required for third-party code or for doing last-ditch error handling. For more information on how to do error handling with static components see “Fail-First Error Handling for the Frontend”.

If you have a React app without server-side rendering or without a page build process then you won’t have any static components. If you would like more information on either of these processes (building a server-side React app or generating static markup) then have a look at “React, Webpack the Server and You”.

Defining characteristics of 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 <Page /> static component:

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

Layout components are responsible for generating the outermost markup of the page. Layout components are designed to be rendered by view components and should contain any markup and styles that is common among a set of multiple views. This typically includes things like headers and footers. For smaller apps sometimes people prefer to move the header and footer into the AppRoot itself, but in other cases people prefer to keep AppRoot focused choosing which components to load for the app and providing data to the app.

Defining characteristics of layout components:

  • Purely presentational.
  • MUST be consumed by view components.

Example <MainLayout /> layout component:

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

The One True Path?

This isn’t the one true path, and, in fact, most projects require other useful categorizations; sometimes error for complex global error messaging components, sometimes context or connect for when you need to inject app-wide state further down the component tree. Ultimately, whatever structure you decide on should make it easier to understand how you application fits together and should promote high cohesion and loose coupling. And it turns out that this taxonomy does a really good job of achieving those goals, and has been in use extensively on projects I’ve been a part of over the past couple of years.

Organize your components for great justice and even greater productivity. Give the Granger Taxonomy™ a try on your next project. 🙏

--

--