08 Jul

Structuring React applications

Today's post lays out my current thinking and approach to structuring React applications: the folder structure I use, my naming conventions, where I write tests, and so on.

One of the best features of React is that it doesn’t force much convention and leaves a lot of decisions up to the developer. This is different from say, EmberJS or Angular, which provide more out of the box for you, including conventions on where and how different files and components should be named.

My personal preference is the React approach as I like the control, but there are many benefits to the Angular approach too. This comes down to what you and your team prefer to be working with.

Over the years I’ve been working with React I’ve tried many different ways of structuring my applications. Some of these ideas turned out to be be better than others, so in today’s post I am going to share all the things that have worked well for me and hopefully they will help you too.

This is not written as the “one true way” to structure your apps: feel free to take this and change it to suit you, or to disagree and stick to what you’re working with. Different teams building different applications will want to do things differently.

It’s important to note that if you loaded up the Thread frontend, you would find places where all of these rules are broken! Any “rules” in programming should be thought of as guidelines - it’s hard to create blanket rules that always make sense, and you should have the confidence to stray from the rules if you think it’s going to improve the quality of what you’re working on.

So, without further ado, here’s all I have to say on structuring React applications, in no particular order.

Don’t worry too much

This might seem like an odd point to get started on but I genuinely mean it when I say that I think the biggest mistake people make is to stress too much about this. This is especially true if you’re starting a new project: it’s impossible to know the best structure as you create your first index.jsx file. As it grows you should naturally end up with some file structure which will probably do the job just fine, and you can tweak it as pain points start to arise.

If you find yourself reading this post and thinking “but our app doesn’t do any of these!” that’s not a problem! Each app is different, each team is different, and you should work together to agree on a structure and approach that makes sense and helps you be productive. Don’t worry about changing immediately how others are doing it, or what blog posts like this say is most effective. My tactic has always been to have my own set of rules, but read posts on how others are doing it and crib bits from it that I think are a good idea. This means over time you improve your own approach but without any big bang changes or reworks 👌.

One folder per main component

The approach I’ve landed on with folders and components is that components considered to be the “main” components of our system (such as a <Product> component for an e-commerce site) are placed in one folder called components:

- src/
  - components/
    - product/
      - product.jsx
      - product-price.jsx
    - navigation/
      - navigation.jsx
    - checkout-flow/
      - checkout-flow.jsx

Any small components that are only used by that component live within the same directory. This approach has worked well because it adds some folder structure but not so much that you end up with a bunch of ../../../ in your imports as you navigate. It makes the hierarchy of components clear: any with a folder named after them are big, large parts of the system, and any others within exist primarily to split that large component into pieces that make it easier to maintain and work with.

Whilst I do advocate for some folder structure, the most important thing is that your files are well named. The folders are less important.

Nested folders for sub components if you prefer

One downside of the above is that you can often end up with a large folder for one of these big components. Take <Product> as an example: it will have CSS files (more on those later), tests, many sub-components and probably other assets like images, SVG icons, and more, all in the one folder.

I actually don’t mind that, and find that as long as the file is named well and is discoverable (mostly via the fuzzy finder in my editor), the folder structure is less important.

If you would like more structure though it’s easy to simply move the sub-components into their own respective folders:

- src/
  - components/
    - product/
      - product.jsx
      - ...
      - product-price/
        - product-price.jsx

Tests alongside source code

Let’s start the points with an easy one: keep your test files next to your source files. I’ll dive into more detail on how I like to structure all my components so their code is next to each other, but I’ve found my preference on tests is to name them identically to the source code, in the same folder, but with a .test suffix:

  • auth.js
  • auth.test.js

The main benefits of this approach are:

  • it’s easy to find the test file, and easy at a glance to see if there are even tests for the file you’re working on
  • all imports that you need are easier: no navigating out of a __tests__ directory to import the code you want to test. It’s as easy as import Auth from './auth'.

If we ever have any test data that we use for our tests - mocking an API call, for example - we’ll put it in the same folder too. It feels very productive to have everything you could ever need available right in the same folder and to not have to go hunting through a large folder structure to find that file you’re sure exists but can’t quite remember the name of.

CSS Modules

I’m a big fan of CSS Modules and we’ve found them great for writing modularised CSS in our components.

I’m also a big fan of styled-components, but found at work with many contributors using actual CSS files has helped people feel comfortable working with them.

As you might have guessed, our CSS files go alongside our React components, too, in the same folder. It’s really easy to jump between the files and understand exactly which class is doing what.

The broader point here is a running theme through this blog post: keep all your component code close to each other. The days of having individual folders for CSS, JS, icons, tests, are done: they made it harder to move between related files for no apparent gain other than “organised code”. Co-locate the files that interact the most and you’ll spend less time folder hopping and more time coding 👌.

We even built a strict CSS Modules Webpack loader to aid our developer workflow: it looks to see what classnames are defined and sends a loud error to the console if you reference one that doesn’t exist.

Mostly one component per file

In my experience people stick far too rigidly to the rule that each file should have only one React component defined within it. Whilst I subscribe to the idea that you don’t want too large components in one file (just think how hard it would be to name that file!), there’s nothing wrong with pulling a small component out if it helps keep the code clear, and remains small enough that it makes little sense to add the overhead of extra files.

For example, if I was building a <Product> component, and needed a tiny bit of logic for showing the price, I might pull that out:

const Price = ({ price, currency }) => (
  <span>
    {currency}
    {formatPrice(price)}
  </span>
)

const Product = props => {
  // imagine lots of code here!
  return (
    <div>
      <Price price={props.price} currency={props.currency} />
      <div>loads more stuff...</div>
    </div>
  )
}

The nice thing about this is you don’t create another file and you keep that component private to Product. Nothing can possibly import Price because we don’t expose it. This means it’ll be really clear to you about when to take the step of giving Price its own file: when something else needs to import it!

Truly generic components get their own folder

One step we’ve taken recently at work is to introduce the idea of generic components. These will eventually form our design system (which we hope to publish online) but for now we’re starting small with components such as <Button> and <Logo>. A component is “generic” if it’s not tied to any part of the site, but is considered a building block of our UI.

These live within their own folder (src/components/generic) and the idea behind this is that it’s very easy to see all of the generic components we have in one place. Over time as we grow we’ll add a styleguide (we are big fans of react-styleguidist) to make this even easier.

Make use of import aliasing

Whilst our relatively flat structure limits the amount of ../../ jumping in our imports, it’s hard to avoid having any at all. We use the babel-plugin-module-resolver to define some handy aliases to make this easier.

You can also do this via Webpack, but by using a Babel plugin the same imports can work in our tests, too.

We set this up with a couple of aliases:

{
  components: './src/components',
  '^generic/([\\w_]+)': './src/components/generic/\\1/\\1',
}

The first is straight forward: it allows any component to be imported by starting the import with components. So rather than:

import Product from '../../components/product/product'

We can instead do:

import Product from 'components/product/product'

And it will find the same file. This is great for not having to worry about folder structure.

That second alias is slightly more complex:

'^generic/([\\w_]+)': './src/components/generic/\\1/\\1',

We’re using a regular expression here to say “match any import that starts with generic (the ^ ensures the import starts with “generic”), and capture what’s after generic/ in a group. We then map that to ./src/components/generic/\\1/\\1, where \\1 is what we matched in the regex group. So this turns:

import Button from 'generic/button'

Into:

import Button from 'src/components/generic/button/button'

Which will find us the JSX file of the generic button component. We do this because it makes importing these components really easy, and protects us from if we decide to change the file structure (which we might as we grow our design system).

Be careful with aliases! A couple to help you with common imports are great, but more and it’ll quickly start causing more confusion than the benefits it brings.

A generic “lib” folder for utilities

I wish I could get back all the hours I spent trying to find the perfect structure for all my non-component code. I split them up into utilities, services, helpers, and a million more names that I can’t even remember. My approach now is much more straightforward: just put them all in one “lib” folder.

Long term, this folder might get so large that you want to add structure, but that’s OK. It’s always easier to add extra structure than remove superfluous structure.

Our lib folder at Thread has about 100 files in it, split roughly 50/50 between tests and implementation. And it hasn’t once been hard to find the file I’m looking for. With fuzzy file finders in most editors, I can just type lib/name_of_thing and I’ll find exactly what I want nearly every time.

We’ve also added an alias to make importing easier: import formatPrice from 'lib/format_price'.

Don’t be afraid of flat folders with lots of files in. It’s often all you need.

Hide 3rd party libraries behind your own API so they are easily swappable

I’m a big fan of Sentry and have used it many times across the backend and the frontend to capture and get notified of exceptions. It’s a great tool that has helped us become aware of bugs on the site very quickly.

Whenever I implement a 3rd party library I’m thinking about how I can make it easy to replace should we need to. Often we don’t need to - in the case of Sentry we are very happy - but it’s good to think about how you would move away from one service, or swap it for another, just in case.

The best approach for this is to provide your own API around the underlying tool. I like to create a lib/error-reporting.js module, which exposes an reportError() function. Under the hood this uses Sentry, but other than in lib/error-reporting.js, there is no direct import of the Sentry module. This means that swapping Sentry for another tool is really easy - I change one file in one place, and as long as I keep the public API the same, no other files need know.

A module’s public API is all the functions it exposes, and their arguments. This is also known as a module’s public interface.

Always use prop-types (or TypeScript/Flow)

Whenever I’m programming I’m thinking about the three versions of myself:

  • Past Jack, and the (questionable at times!) code he wrote
  • Current Jack, and what code I’m writing right now
  • Future Jack, and how I can write code now that makes his life as easy as possible later on

This sounds a bit silly but I’ve found it a useful way to frame my thinking around approaches: how is this going to feel in six months time when I come back to it?

One easy way to make current and future versions of yourself more productive is to document the prop-types that components use! This will save you time in the form of typos, misremembering how a certain prop is used, or just completely forgetting that you need to pass a certain prop. The eslint-react/prop-types rule comes in handy for helping remind us, too.

Going one step further: try to be specific about your prop-types. It’s easy to do this:

blogPost: PropTypes.object.isRequired

But far more helpful if you do this:

blogPost: PropTypes.shape({
  id: PropTypes.number.isRequired,
  title: PropTypes.string.isRequired,
  // and so on
}).isRequired

The former will do the bare minimum of checks; the latter will give you much more useful information if you miss one particular field in the object.

## Don’t reach for libraries until you need them

This advice is more true now with the release of React hooks than it ever has been before. I’ve been working on a large rebuild of part of Thread’s site and decided to be extra particular about including 3rd party libraries. My hunch was that with hooks and some of my own utilities I could get pretty far down the road before needing to consider anything else, and (unusually! 😃) it turned out that my hunch was correct. Kent has written about this in his post “Application State Management with React” but you can get a long way these days with some hooks and React’s built in context functionality.

There is certainly a time and a place for libraries like Redux; my advice here isn’t to completely shun such solutions (and nor should you prioritise moving away from it if you use it at the moment) but just to be considered when introducing a new library and the benefits it provides.

Avoid event emitters

Event emitters are a design pattern I used to reach for often to allow for two components to communicate with no direct link between them.

// in component one
emitter.send('user_add_to_cart')

// in component two
emitter.on('user_add_to_cart', () => {
  // do something
})

My motivation for using them was that the components could be entirely decoupled and talk purely over the emitter. Where this came back to bite me is in the “decoupled” part. Although you may think these components are decoupled, I would argue they are not, they just have a dependency that’s incredibly implicit. It’s implicit specifically because of what I thought was the benefit of this pattern: the components don’t know about each other.

It’s true that if this example was in Redux it would share some similarities: the components still wouldn’t be talking directly to each other, but the additional structure of a named action, along with the logic for what happens on user_add_to_cart living in the reducer, makes it easier to follow. Additionally the Redux developer tools makes it easier to hunt down an action and where it came from, so the extra structure of Redux here is a benefit.

After working on many large codebases that are full of event emitters, I’ve seen the following things happen regularly:

  1. Code gets deleted and you have emitters sending events that are never listened to.
  2. Or, code gets deleted and you have listeners listening to events that are never sent.
  3. An event that someone thought wasn’t important gets deleted and a core bit of functionality breaks.

All of these are bad because they lead to a lack of confidence in your code. When developers are unsure if some code can be removed, it’s normally left in place. This leads to you accumulating code that may or may not be needed.

These days I would look to solve this problem either using React context, or by passing callback props around.

Make tests easy with domain specific utilities

We will end with a final tip of testing your components (PS: I wrote a course on this!): build out a suite of test helper functions that you can use to make testing your components easier.

For example, I once built an app where the user’s authentication status was stored in a small piece of context that a lot of components needed. Rather than do this in every test:

const wrapper = mount(
  <UserAuth.Provider value=>
    <ComponentUnderTest />
  </UserAuth.Provider>
)

I created a small helper:

const wrapper = mountWithAuth(ComponentUnderTest, {
  name: 'Jack',
  userId: 1,
})

This has multiple benefits:

  • each test is cleaned up and is very clear in what it’s doing: you can tell quickly if the test deals with the logged in or logged out experience
  • if our auth implementation changes I can update mountWithAuth and all my tests will continue to work: I’ve moved our authentication test logic into one place.

Don’t be afraid to create a lot of these helpers in a test-utils.js file that you can rely upon to make testing easier.

In conclusion

In this post I’ve shared a bunch of tips from my experiences that will help your codebase stay maintainable and more importantly enjoyable to work on as it grows. Whilst every codebase has its rough edges and technical debt there are techniques we can use to lessen the impact of it and avoid creating it in the first place. As I said right at the start of this post, you should take these tips and mould them to your own team, codebase, and preferences. We all have different approaches and opinions when it comes to structuring and working on large apps. I’d love to hear other tips you have: you can tweet me on @Jack_Franklin, I’d love to chat.

If you enjoyed this post, signup to the newsletter to be notified when there's new content, new courses and exclusive offers.

If you enjoyed this article please feel free to share it on Twitter.