At Yeti, we create a large number of apps from the ground up due to the number of startups we work with. Every new app gives us the opportunity to improve our tech stack and one if its important pieces - folder structure.
Why does folder structure matter?
Imagine a scenario in which we have simple requirements, which warrants a simple app and a simple folder structure. Makes sense. However, as we start scaling our app, this simple folder structure becomes an issue. Let's say we place all of our components in a folder called
components. Every component that lives under the
components directory seems to have equal weighting, leaving
sub components of other
components and components that are really
screens on the same level. Any developer trying to understand how everything connects will have a difficult time seeing the shape of the app.
Our redux code becomes a mess too. Our files containing our actions become monoliths. Our initial slices of redux state no longer represent what's inside of them. We're now unnecessarily digging for logic while holding too many files in our working memory. One tap on the shoulder from an unassuming colleague and all of the connections we've been forming between pieces of code and logic in our app evaporate.
Dealing with these problems has led us to create a set of folders that hold specialized purposes in our apps, allowing easier maintenance and less reliance on tribal knowledge.
The Five Folders
To kick off the most important folder, there are two things to keep in mind - verticality and horizontality. Folders that have many files or folders within them one level deep are very horizontal, while folders that have many nested subfolders are very vertical. It is important to keep a balance between these two attributes since going too far in either direction will cause confusion and increase the complexity of the mental model of the application.
If we keep everything in one folder called
components, for example, the shape of the application and how the code relates to each other will not be apparent. However, if we create too many nested folders and start sharing information between subparts of the application at too many different levels, we over-abstract and actually make the app more complex than necessary.
Each module represents a certain section or theme of the app. Common modules that can be frequently prevelant across apps are 'authentication', 'profile', and 'onboarding'. Used wisely, thematic modules like these can simplify the understanding the application and provide sufficient sandboxing from other, unrelated parts of the app.
Within each module, we only create three more folders:
dux , a spin off of ducks modular redux where we export our reducer and actions,
screens , which correspond to the different screens the router will be displaying, and a third for action creators or other side effects. For
redux-thunk, we call it
thunks , for
redux saga ,
sagas , and for
redux observable ,
Tip: Reducing Boilerplate
In order to reduce boilerplate and avoid the necessity to use numerous spread operators in our reducers, we use immer reducer. It also supports TypeScript and automatically creates actions.
The first thing you'll notice here is that there's an underscore. Why's that? To me, it's more readable than
reduxSetup. It's also not a
At its simplest, this folder houses two files:
store . To those familiar with redux, these should be pretty self explanatory. Our
rootReducer combines all of our main reducers from our
modules and our
store creates the central brain for redux.
Tip: Side Effect Setup
If we're working with any side effect libraries like
redux saga, we'll need somewhere to put their setup boilerplate. This is a good place.
Services follow the same paradigm as modules in that they are modular. Each service is meant to provide specific functionality or business logic. Examples of services that we frequently use are an
Http service and
Navigation service, which work well in both our react native and react web projects. Our
Http service is a base class, which is helpful for creating additional services like
Backend that extend
Http and connect with a backend like
Fun fact: Before we were a React shop, we primarily developed in Angular, which is where the inspiration for how we utilize services comes from. The difference being we just import our services directly as ES6 modules instead of using dependency injection.
Things that go in here have usually elevated from the ranks of single use and are now ready to be sent across the entire application. Some good examples of what to put in here are
styles, typically with
colors acting as files within, and
components , with things like
text . A good rule of thumb is that components should only go in here if we know that the design uses this element in multiple places or that they are not
sub components of a
screen . Another worthy mention is a
utils folder or file, for those one off functions that have unique business logic.
We use TypeScript for all of our projects, so this folder acts as the single source of truth for our types. Even if you are using
PropTypes, don't forget that those can be abstracted as well!
We've experimented with keeping types scattered throughout the other folders before, but have found that developers not acquainted with the code base struggle to find where types live. Types then start to become duplicated in multiple places. Having a code editor with Intellisense helps with both of these problems, but only when you're familiar with the names.
We actually create folders in the
types folder that are very similar to Five Folders. These are
state , and
state is just an abstraction over the different
modules. This allows us to still keep the same mental model as the
src folder, reducing cognitive load. This also has the increased benefit of knowing that importing from
types/.. will always get you where you need to go.
The Real World
All of this idealistic philosophy is great, but does it actually scale?
In short, yes. We've created multiple projects that have scaled to over 100 components using this folder structure.
Is it modular?
Yup. One of the most common occurrences during a refactor is to repurpose a component to be shared. Since everything is in a folder, tests included, we simply just need to drag and drop it into the
shared/components folder. Technically, it's actually not that simple, since we need to change the imports from where we're calling that component, but if we're using TypeScript the code won't even compile before those are fixed, so we'll be safe.
Is it extendable?
Yes. When adding functionality, the biggest pieces are usually new sections / flows within the app and integrating with new third party code. Our
services folder can easily accommodate new services, as well as our modules, since we can just add a new module that also encompasses a new slice of state.
Want an example of this philosophy in action? Check out Glamper, a starter kit that we use at Yeti to scaffold new projects.
Kevin O'Leary is a Developer / Product Strategist at Yeti. He specializes in the Front-End and can also design. When not at work, Kevin loves watching soccer, making music, and building out a minimalist wardrobe.