Everything is a Component

“gray sand turtle” by Gary Bendig on Unsplash

Directory structure is the new semicolon.

Since Prettier solved the “semicolon or not” debate, we now like to endlessly debate the structure of our JavaScript applications. Is it scalable? Is it semantic? Does it matter?

There are many flavours of application architecture, each derived from personal preference as much as efficiency or practicality. We’ve separated files by “type”, by “feature” and a bit of both. Just search “javascript directory structure” to see the plethora of opinions.

Ultimately, when designing directory structure, we should ask ourselves: is it simple and does it work?

As Dan Abramov eloquently puts it:

move files around until it feels right

Centralised architecture

Most of the current approaches to application architecture have one problem in common: centralisation.

Malte Ubl wrote a comprehensive article about his experience of “designing very large applications”.

The best advice I can give: Don’t let your applications get very large.

Most of his lessons relate to the centralisation of configuration. This can also be extended to the centralisation of components.

Do you have a single, root-level config.js or constants.js file? Get rid of it. Do you have a folder of “reusable” components? Destroy it.

By removing these centralised dependencies, we linearise the application’s dependency tree. UI components no longer import generalised, shared components or config — instead they are built with specific, local components and maintain their own slice of config (or, state).

One of Malte’s main messages is to “make it easier to delete code” — when adding a function or a component to an application, we should consider whether the code can be removed without impacting other files. Decentralising dependencies makes this a piece of cake as the amount of code shared across the application is minimal.

Abstract with care

Abstraction is both a blessing and a curse. Sometimes, our components grow “too big” and we split them up into many smaller components. Other times, a “utility” function is used (or could be useful) in multiple files so we hoist it up to be shared across the application. It’s at these points we have to be careful to make the right abstraction.

Over-abstraction of seemingly reusable components is a classic problem in applications that use component-based UI frameworks, like React. The initial intention is always good: “This form could be used everywhere! Now we have one form to maintain rather than loads”. A form may look similar based on UI designs, it may seem to have similar behaviours but quite often this is coincidence. What we usually end up with is: “This form needs to accommodate loads of uses! Now we have one form that is difficult to maintain”. We have fallen into The Pit of Conditionals.

const ThePitOfConditionals = props => {
return (
<div>
{props.shouldWeExplore ? (
<GoForWalkInFieldOfGreatPower>
{props.isThatATree ? (
<OhNoItWasJustGreatResponsibilty>
{props.hasThisGoneFarEnough ? (
<Never!>
{props.shouldWePeerIntoThatHoleOverThere ? (
<Falling>
{props.isThatAnEscapeHatch ? (
<SetTimeout />
) : (
<Falling>
{props.hasThisGoneFarEnough ? (
<ThePitOfConditionals />
) : (
<ThePitOfConditionals />
)}
</Falling>
)}
</Falling>
) : <SVGFill />}
</Never!>
) : (
<Br />
)}
</OhNoItWasJustGreatResponsibilty>
) : <Image src="tree-mirage.jpg" />}
</GoForWalkInFieldOfGreatPower>
) : (
<Component />
)}
</div>
)
}

What if components only did one thing?

This tweet holds a lot of truth:

Primitive components +1

Components should be discrete; they should only do one thing. This is sometimes confused with the amount of code in a component — as mentioned earlier, when components grow in LOC we tend to see this an abstraction point. Single-purpose (or, “primitive”) components will tend to be smaller but size shouldn’t be used as an indication of complexity.

Take this generalised “Profile” component. It renders a profile image and a username. This profile view is shown in multiple places across the application. It has been abstracted to avoid repeating the same code. However, the component has different requirements when used in different parts of the UI so needs a sprinkling of conditional rendering:

const ProfileComponent = props => {
return (
<div>
{props.showAvatar && (
<img src={props.src} />
)}
      {props.showUsername && (
<p>{props.username}</p>
)}
</div>
)
}

It’s pretty dark and narrow in The Pit of Conditionals — making it hard to see how a component should behave based solely on its own code.

We can contrast that with primitive Image and Text components that can be used to compose other components, on demand. These primitive components could also have some application-specific styles or theme applied to them to ensure a consistent UI.

const Image = props => {
return (
<img src={props.src} />
)
}
const Text = props => {
return (
<p>{props.children}</p>
)
}
const Avatar = props => {
return (
<Image src={props.avatar} />
)
}
const Header = props => {
return (
<header>
<Image src={props.avatar} />
      <Text>{props.username}</Text>
</header>
)
}

Primitive components give us narrowly scoped abstraction. Pieces of our codebase can still be abstracted but with the safety of simple dependency trees. The abstractions are focused on a specific purpose and so have a narrow scope — ultimately making them easy to delete.

Primitive components can live anywhere in your directory structure – near to where they are relevant: in root-level “type” directories like styles/ or in domain-level “feature” directories like ProfilePage/. We can continue to categorise components by type: style, computation, network requests, routes and so on. More on this later.

Oh, and remember when “colocation” was cool — let’s bring it back. Its fine to mix GraphQL queries with CSS-in-JS and JSX in a single component — as long as that component does one thing — like render a username.

const usernameTextStyles = css`
color: #f08f74;
`
const usernameQuery = gql`
currentUser {
id
username
}
`
const Username = () => {
return (
<Query query={usernameQuery}>
{user => (
<p {...styles(usernameTextStyles)}>{user.username}</p>
)}
</Query>
)
}

What if everything was a component?

In frameworks like React, “components” are first-class citizens — they are the primary (if not only) building block of the application. So why separate them into their own areas of the directory structure, like generalised components/ folders? They should be at the core of every level and area of the application’s architecture. They should be everywhere.

If we classify components by their type, we could have “utility components”, “query components”, “style components”. A lot of projects already do this with “container components” and, increasingly, “context components”.

Let’s take a utility component as an example. Usually, utilities are functions that take some data as an input, process it according to some rules and return the processed data:

const getAvatar = userId => `images/avatars/${userId}.jpg`
// Meanwhile, elsewhere in the application...
import getAvatar from 'utils/getAvatar'
const Avatar = props => {
return (
<img src={getAvatar(props.userId)} />
)
}

By turning this utility function into a component, our code becomes easier to reason about:

const GetAvatar = props => {
return props.children(`images/avatars/${props.userId}.jpg`)
}
const Avatar = props => {
return (
<GetAvatar userId={props.userId}>
{avatar => <img src={avatar} />}
</GetAvatar>
)
}

The main benefits of making everything a component are:

  • Applications have a single building material: composition. This simplifies the UI’s API — components just use other components to render elements to the DOM. No more obscure utility functions, over-use of lifecycle methods to compute state or risk of side-effects (like functions in render).
  • Memoization for free. By leveraging built-in tools like shouldComponentUpdate and PureComponent we don’t need to worry about memoizing our computed data functions.
  • Standard patterns. Familiar conventions for writing components like render props and HOCs can help us to reason about our code.
  • Testing. As components are completely serialisable as JSON, testing most of the units in the application will be as simple as using a Jest snapshot.

We are still free to structure our applications in whatever way works for our teams and call folders whatever names make sense to us. But when doing so, we’ve seen how it could be useful to minimise the centralisation of dependencies and prefer (primitive) components for all units of your applications, from presentation to business logic.

Let’s keep moving things around until they feel right!


The Everything is a Component Checklist:

Next time you write a piece of code, ask:

  • What would the code look like as a component?
  • Does the component do one thing?
  • Could the component be composed of other primitive components?
  • Does abstracting parts of the component reduce or introduce complexity?