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.
--
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
For our example, Iāll be using a Giphy search app whose file structure looks like this.
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.
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 anonSearch
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
What can we do to logically structure all of these files? What if we placed our related files together into a single GiphySearch
folder? This is what we do for traditional web development with an index.html
and its supporting files. It would look like this.
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.
Breaking it down
Letās look at our completed GiphySearch component and what is inside of each file. Weāll start with index.js
. Itās sole reason for existence is to maintain state. Nothing else.
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
Thereās one more technique that Iād like to point out. Whenever you use a render callback to perform your renderingļ¹£whether that be a Function as Child (donāt you dare!), Function as Prop (aka render prop), or component injection (what I use almost exclusively)ļ¹£it can sometimes be advantageous to provide a default if none is specified. I refer to this as the 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
I hope you get the opportunity to take the disciplines and patterns that youāve read about here today and apply them to your own code. The component folder pattern un-clutters your project, while the reference implementation pattern allows you to provide reasonable defaults to render callbacks. Both are my faithful companions when writing React applications.
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
It was pointed out that using the component folder pattern could make it so that tabs in your editor look like this.
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.