I rarely ever talk about lines of code when referring to a project. It’s usually a poor measure of productivity and substance. But it does say a lot about the potential complexity of a project.
At Minsilo, our core React application contains the following:
- ~42K lines of Javascript spread across 1,671 files.
- ~10K lines of CSS spread across 579 files.
- First commit in 2018.
- 1,565 commits, 240 tagged releases (more commits in practice, since we squash commits sometimes have larger pull requests).

This does not account for any other code that we may have (we also have a separate partner
application and authentication
client) on the frontend or in our monolithic backend.
So how do we keep it all organized?
More files
After many years of trial and error, I’ve find it’s better to keep smaller files that do one thing and rely on composition, rather than trying to do too many things in one file. It turns out that more files with less going on in each of them is generally better.
Better separation of concerns
React does not enforce any specific paradigm, unlike web frontends such as Angular. It’s not even a framework, but rather a library that is focused primarily on manipulating the DOM in a consistent and useful way. This is both a blessing and a curse.
This led to trouble for us early on in our core repository, since we did a poor job at separating concerns and setting ourselves up for code reuse.
Since then, we’ve done a lot of refactoring to clean our code up. The easy problems we had to fix were:
- Having too many components in a file – simple: create individual files for each component.
- Having spaghetti code (e.g. code that jumps all over the place and calls other code in a disorganized manner) – not a simple problem to solve, but it can be solved by using good code refactoring tools.
- Poorly named components, functions, and variables – naming is the hardest part about programming and there’s always a tradeoff to be had between moving fast and naming things (poorly).
For a lot of this refactoring, I ended up using the built-in tools that Webstorm has. I’ve heard that VSCode can do some of the same things now, but I haven’t checked it out for myself.
Using good folder structure
Our application is organized as such:
src
├── components
├── data
├── errors
├── hooks
├── images
├── lib
├── services
└── styles
Digging into our components
directory, we have three major subdirectories:
- BuildingBlocks – reusable frontend components, such as our drop-in comments component.
- Core – core functionality handling both view and data concerns, such as
Layout
andWebsockets
. - Features – “real world” functionality that contains useful capabilities for end users.
BuildingBlocks are essentially features in of themselves, with the notable exception being that we do not expose them independently of components under Features
. In other words, we use the BuildingBlocks
to add functionality to Features
.
Core contains an assortment of things. Here’s what’s in that directory:
src/components/Core
├── Auth
├── Context
├── Diagnostics
├── FeatureGating
├── Groups
├── Help
├── Layout
├── Offline
├── Profiles
├── Routing
├── Schedules
├── Search
├── Settings
├── Sidebars
├── SubscriptionsV2
├── Users
├── Versions
├── Websockets
└── WorkspaceSettings
Features are real-world, end-user facing components that provide our advertised feature set.
Creating new components
Most of our new components and features are created using generators. We utilize plop for our generators.
The main plop generator we utilize is called generateContextComponent
, which creates a new component with the following files:
- index.js
- component_name.module.css
Inside of the index.js
file is a basic component that looks like this:
import { useContext } from 'react'
import classes from './component_name.module.css'
const ComponentName = () => {
return <></>
}
export default ComponentName
This isn’t too fancy, but it takes 3-seconds to run and gives us a good boilerplate component to work with. More importantly, it also reduces the tendency to be lazy and overstuff a component, violating best practices around composability.
The component_name.module.css
is a CSS module (we use CSS modules primarily) that allows us to easily add styling to each component. Styles live at a component level exclusively, except for a couple of notable exceptions (which are caused by 3rd-party libraries).1
Creating component families
We use the aforementioned structure for individual components, but it doesn’t address the needs of a component family. Component families contain collections of components and provide a logical, usually business-case-specific separation.
For example, our Goals feature is one component family. Worksheets, Strategic Initiatives and Tasks are also component families.
Component families have the following structure:
.
├── Actions
├── API
├── Components
├── Contexts
├── Data
├── Errors
├── Helpers
├── Hooks
├── Providers
├── Reducers
└── State
These folders each have their own purpose:
- Actions: contain objects that have named actions (e.g.
FeatureActions.FETCH_DATA
) - API: contains one-file-per-API call. Files are named according to their HTTP method, such as
getStrategies.js
andpostStrategy.js
. - Components: our actual components live here. We use the atomic design pattern to organize our components (more on that in a minute).
- Contexts: boilerplate context files that correspond to a Provider. These are used within our components via the
React.useContext
hook. - Data: not strictly in every component family, but sometimes we have default states or other customizable data that is not dynamic but we don’t want to live in a singular component. For example, we store manifests that we consume in our factory components here.
- Errors: error messages are stored at this level.
- Helpers: any utility functions that are used in this component family but not elsewhere get stored here.
- Hooks: any React hooks that are used in this component family but not elsewhere are stored here.
- Providers: this is where 90% of our logic lives. We use providers to encapsulate all API functions, initialization behavior, and more. (read on, I’ll describe providers in a bit).
- Reducers: this is where state transformations occur.
- State: we initialize our the state of our React Context store using files stored here.
Components
Components are organized according to the atomic design pattern. This roughly means every component has 4-5 folders:
- Atoms: small components that do not contain any subcomponents and that do not wrap other components. Buttons are a common example.
- Molecules: slightly larger components that make up Organisms. These components may have subcomponents and may be higher order components/wrappers to other components.
- Organisms: large components that represent a single logical concept.
- Templates: reusable components that are mostly driven by configuration.
- Pages: components that are made available by a route in React Router.
How we deal with massive features
In some cases, we break component families down further by creating another layer of hierarchy. This can be done at two levels: either in the Components
folder directly or within the component family.
The decision to break it down at the Components
level or component family level is decided by:
- If there are a lot of separate providers and reducers, then we split it at the component family level.
- If there are a lot of components that share the same couple of providers and reducers, we split it at the
Components
level.
State Management
We manage state in Minsilo using React Context. Although some people complain about it being inferior to other state libraries, we’ve found it to be robust, stable and easy to use.
One of our key principles for state management is to keep all business logic outside of components. And we try at all costs to avoid multi-level prop drilling. It’s incredibly hard to follow what’s going on when you prop drill and can lead to a number of nasty side effects (and it’s just generally hard to maintain).
Providers
Most of our business logic exists in a provider. Let’s look at a really basic example:
import React from 'react'
import TestReducer from '../Reducers/TestReducer'
import InitialTestState from '../State/InitialTestState'
import TestActions from '../Actions/TestActions'
import TestContext from '../Contexts/TestContext'
export default function TestProvider({children}) {
const [state, dispatch] = React.useReducer(TestReducer, InitialTestState)
const crud = {
createTest: async () => {
// todo: call API
value.addTest(testContents)
},
destroyTest: async () => {
// todo: call API
value.removeTest(testId)
},
}
const value = {
...crud,
title: state.title,
description: state.description,
addTest: value => dispatch({type: TestActions.ADD_TEST, value}),
removeTest: value => dispatch({type: TestActions.REMOVE_TEST, value}),
setTitle: value => dispatch({type: TestActions.SET_TITLE, value}),
setDescription: value => dispatch({type: TestActions.SET_DESCRIPTION, value})
}
return <TestContext.Provider value={value}>
{children}
</TestContext.Provider>
}
Much of this is boilerplate, but there are some useful things we’ve learned over the past few years:
- The
value
object can become really long and unwieldy. We’ve found success in creating smaller objects that a focused on a single concern. In the above example, we have acrud
object that we spread into value (e.g....crud
).
This allows us to access the individual functions and data for a specific concern with ease, but without making it difficult to understand and modify the provider. - Avoid nesting. It’s much harder to modify nested state. For example, if we had stored
title
anddescription
in atest
object, that would make it harder for us to modify (in other words, usetitle
anddescription
instead oftest.title
andtest.description
) - Named action type constants are better than strings. It’s technically possible to use a string to discern which action occurred in a
dispatch
function, but it’s prone to error since our editor cannot validate the presence/correctness of that action name. - Separate your API calls from data manipulation. We frequently use websockets, which receive an “event” and then do something with the data provided by the websocket message.
For example, if a user adds a card to a page, then we will transmit that change over a websocket to all other viewers of the page and update what they’re seeing in real-time.
Keeping the function that appends an item to the list of cards, for example, separate from the function that works with the API or the websocket allows us to reuse code and reduce the potential confusion.
Conclusion
I hope this article inspires you to improve the maintainability of your React projects.
Writing maintainable and enjoyable to work with code is a continuous process. I’m sure looking back at this post in a couple of years, I will have made several changes to the way code is organized based on new learnings and changes in project scope.
Remember it’s okay to start small and refactor your code over time. If you have any questions, feel free to send me an email.
Footnotes:
- If you’re a fan of libraries like SemanticUI or TailwindCSS, you may be wondering if such heavy use of component-level styling leads to a lot of duplicative code. The answer is yes and no.
Yes, because it’s easy to be lazy and create CSS for each component individually rather than thinking about reusable styling.
No, because we can again use composition to create containers and primitive components that give us a consistent interface across implementations. We’re heavily invested in this approach and I think it’s more flexible than CSS-based styling because it also allows you to marry sensible functionality within interfaces (i.e. adding close buttons to a dialog).