The Granger Component Taxonomy
By: Izaak Schroeder
Tags: JavaScript, React, Front End Development

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 .

or registered by if using . 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).

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. 🙏