Writing Scalable React Apps with the Component Folder Pattern

Discover how to organize your React components using the component folder pattern. It will help un-clutter your projects, and your life. It’s soon to be your new best friend.

Donavon West
Aug 28, 2017 · 7 min read

We’ve all seen them. The huge 400+ line god component behemoths. They can be so large, that it takes the better part of a day to get a high-level understanding of what they do.

In this article, we will take one of these god components and break it down into bite sized and logical units of work. Then we’ll apply the component folder pattern to compartmentalize the pieces.

Giphy search

src/
|- App.js
|- GiphySearch.js

It looks pretty simple, but everything (state, view, and data loading) are all in one component. This makes GiphySearch hard to maintain.

The before Giphy search app on CodeSandbox.io. It works, but it’s not pretty.

You can start to imagine the sub-components that we’re going to need. First we’ll need a controller that keeps the state (e.g. loading, error, and data). We’ll also need:

  • A SearchInput component that renders the input box and calls an onSearch handler when the user clicks “Search”.
  • A Loading component that displays a loading indicator.
  • A loadData module that sends a search query to the Giphy API and fetches the image URL.
  • An Image component that displays the image URL.
  • An Error component in case something goes wrong.

I’ll add one more component, a View component that selectively dispatches to the other render components. Our file system would now look something like this.

src/
|- App.js
|- GiphyView.js
|- GiphySearchInput.js
|- GiphyImage.js
|- GiphyLoading.js
|- GiphyError.js
|- giphyLoadData.js

A big problem with this approach is that as we add components, our src folder will start to fill up fast. There will be lots and lots of files, making it hard to find what you’re looking for. But to me the issue is that we are using a namespaced file structure. Guess what? That’s exactly the problem that folders solve.

Introducing the component folder pattern

src/
|- App.js
|- GiphySearch/
|- index.js
|- View.js
|- SearchInput.js
|- Image.js
|- Loading.js
|- Error.js
|- loadData.js

Nice, but what is that index.js file doing here and what does it do? Well notice that our GiphySearch.js file is now gone. We simply renamed it index.js.

The thing I like about this approach is that once you get your GiphySearch component working, collapse the folder. You’ll no longer be staring at all of those files, reducing visual clutter. Researchers at Princeton University Neuroscience Institute conducted a study that suggests reducing clutter can help you stay focused.

src/
|- App.js
|- GiphySearch/

Ahh… much better. Looks a lot like the file structure that we started out with, doesn’t it?

Some of you are asking, “Will I have to change my import statements with this new file structure? After all, the code for Giphy Search is inside of a folder and in a file with a different name.” The answer is… NO! That’s the beauty of the component folder pattern.

Your app still looks something like this, no matter if the module lives in GiphySearch.js or GiphySearch/index.js. Hurray for science!

import GiphySearch from './GiphySearch';const App = () => (
<GiphySearch initialQuery="dog"/>
);

Before we dive in and look at the code line-by-line, here is what our new Giphy search looks like.

The after version, using the injected SpinLoad. Try it out and see the code on CodeSandbox.io

Breaking it down

import View from './View';
import loadData from './loadData';
export default class extends Component {
state = {};
load = this.load.bind(this);
async load(...args) {
try {
this.setState({ loading: true, error: false });
const data = await loadData(...args);
this.setState({ loading: false, data });
} catch (ex) {
this.setState({ loading: false, error: true });
}
}
render() {
return (
<View {...this.props} {...this.state} onLoad={this.load} />
);
}
}

Both props, state, and an onLoad callback, are passed down to the View component. How we get the data is handled by the loadData module. This is because neither have anything to do with state.

In fact, look closely. Nothing about this component has anything to do with a Giphy search. It is abstract in about every way possible. Something you could use over and over again. IMO, this is simplistic elegance.

“A component should do one thing, and do it well”

Next is the View component. It will determine what to render. It’s really more of a view controller, as it passes along to other render components to do the actual rendering ﹣all based on state passed in as props from index.js.

import SearchInput from './SearchInput';
import Image from './Image';
import Loading from './Loading';
import Error from './Error';
const View = ({
loading, error, data, initialQuery, onLoad,
RenderSearchInput, RenderImage, RenderLoading, RenderError,
}) => (
<div>
<RenderSearchInput
initialQuery={initialQuery}
onSearch={onLoad}
/>
<section>
{do {
if (loading) {
<RenderLoading />
} else if (error) {
<RenderError error={error}/>
} else {
<RenderImage src={data} />
}
}}
</section>
</div>
);
View.propTypes = {
...
};
View.defaultProps = {
RenderSearchInput: SearchInput,
RenderImage: Image,
RenderLoading: Loading,
RenderError: Error,
};

Notice that we use component injection to render based on loading, error, and data conditions (RenderLoading, RenderError, and RenderImage respectively). Defaults are provided if none are specified.

Next is the loadGiphy file. It uses fetch to hit the Giphy REST endpoint, converts the JSON to an object, and extracts the image URL. It’s rather standard, so I won’t show it here.

The rest of our render components, Loading, Error, and Image are simple stateless functional components, so I won’t waste column inches on them either. However, the complete source code (as well as a live running example) can be found on CodeSandbox.

Reference implementation pattern

This allows the consumer of your component to optionally pass in a render component. However, if they don’t, some sort of default or “reference” component will perform the rendering.

Let’s override the Loading component and make it look a bit nicer than the default reference implementation by passing the RenderLoading prop.

import GiphySearch from './GiphySearch';
import SpinLoad from './SpinLoad';
const App = () => (
<div>
<h1>Giphy Search</h1>
<GiphySearch initialQuery="dog" RenderLoading={SpinLoad} />
</div>
);

Notice that we are not overriding the RenderSearchInput, RenderImage, or RenderError components. They will use the default reference implementations.

Here’s what the SpinLoad component, built using styled-components, looks like.

import styled, { keyframes } from 'styled-components';const rotate360 = keyframes`
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
`;
const Spinner = styled.div`
color: #333;
font-size: 18px;
font-family: sans-serif;
&:before {
display: inline-block;
content: '';
width: 18px;
height: 18px;
border-radius: 50%;
border: solid 2px #ccc;
border-bottom-color: #66c;
animation: ${rotate360} 1s linear infinite;
margin-right: 6px;
vertical-align: bottom;
}
`;
const Loading = () => (
<Spinner>Loading</Spinner>
);
export default Loading;

While the reference implementation pattern may not always make sense in your component, it’s a powerful pattern to be aware of. This can aid with testing of your component, as you can inject a test implementation.

Conclusion

You can read more of my React based articles (including some on Hooks in React 16.7, {…❤️} Spread Love, and React Best Practices) on the AmericanExpress.io Technology Blog.

Update

Notice the unfortunate side effect where a lot of tabs are named index.js. Fortunately, Joshua Comeau has a solution ﹣ if you’re using Atom anyway. He’s written a plug-in called nice-index that shows the name of the folder instead.

“We shouldn’t have to change the way we code to accommodate our tools. Get better tools.”

So now when you have multiple index tabs open, you can tell which one is which. I have it installed now and can’t believe that I’ve lived without it for so long.

I also write for the American Express Engineering Blog. Check out my other works and the works of my talented co-workers at AmericanExpress.io. You can also follow me on Twitter.

💅 styled-components

Visual primitives for the component age.