How to structure large React apps
App structure design is like aerodynamics of Formula 1 cars. You don’t need it if you are not going to pass corners at 200+ km/h. This article is a set of advices on how to structure codebase of large applications (ecma script and React in particular) being developed by medium and big size teams.
Principles described here formed the approach to structuring React apps, which is well described at https://github.com/gvidon/reacto#component-structure. We apply this approach at ottofeller in all our js apps.
Stick to your rules
Less rules is better than more rules, strict following rules is better than creating exceptions.
These principles can be applied to almost every aspect of software development. Simple general rules and strict following them will lead to explicit, scalable and easy to navigate code structure. When I say “simple” I also mean one which doesn’t need docs for ambiguous parts of codebase, which are exceptions.
As debugging and refactoring often take valuable amount of developers time, explicitness of codebase is the key to quick iterations and fast delivery.
Be paranoid about names
When there are tons of interconnected parts in your beloved system, and dozen of persons moving them from here to there every week you cannot overestimate the value of meaningful and relevant names. Properly given and grammatically correct name can save you from questions and your teammates from confusions.
Files, directories, variables, functions names and parameters — everything deserve its name to correspond to its purpose.
And don’t forget that naming is the most creative and the hardest part of software development, so always think three times before introducing new entities (like variables or functions or methods). Inline the code if it is applied just once (or even twice), don’t go too far in moving code into functions and variables, this will make reading a code similar to reading the novel across multiple books.
Exploit the file system
Your code starts with the hierarchy of directories and before opening a file one should find it. So, needless to say that while being well organised and reflecting the same hierarchy as your business logic model it can save time in navigation across codebase.
You are not limited to just directories in your structuring. Split code between files with no hesitation!
Don’t put multiple entities into single file, you will end up with messy tightly coupled codebase. Put one entity per one file.
It will improve your navigation experience. Files list will become kind of index of your codebase. For example, it is more convenient to have single function in
helpers/validators/is-integer.js than twenty functions (even small) in huge
helpers/validators.js. Your navigation to particular function will consist of single step — just find a file, no need to go through the file content in search for required entity.
Did I also mention code refactors? Moving files across directories is much simpler than moving parts of codebase between files. Use rm, mv and sometimes cp (hope not often) right in your terminal.
Keep your tools close to relevant business logic
There will always be repeating code in any app. It is separated as libraries, utilities or helpers — these are your tools, which help in building unique components of business logic.
Tools should not impact the general structure of the app.
Tools must be clearly separated from business logic. Don’t spread them over the codebase too much. Most of the time you work with business logic abstractions, not with tools. And placing tools here and there will add distraction and ambiguity to the process of navigating across codebase.
But don’t go too far in the process of separation. Single directory with all tools, with time will grow up significantly. If two modules use same tool put this tool at the level of closest common parent of these modules. According to this rule you will have multiple equally named directories with tools in your app.
Don’t import tools from sibling directories. Good rule is to have only one direction imports (from top to bottom) across the app, which makes code maintenance simpler.
isEmail() required on user profile page SignUp/
isEmail() also required in sign up process
Become best friends with decomposition
Avoid swiss knifes whenever possible and break big parts of a code into smaller adjustable pieces. If your function or React component tend to grow up in number of params or properties it is a sign of bad decomposition.
Small independent functions with few params and clear logic is always better than single fat function with the body describing whole universe.
The smaller units of code, the more flexible you become at scaling them up to more complicated tasks. A good example is POSIX. There are no commands for finding js or css files in current directory and writing the list into file, but you can easily construct them:
$ ls | grep ".js" | cat > js
$ ls | grep ".css" | cat > css
The same can be applied to React components. Take a look at monolithic generic component which renders details of some shopping store item. It has header with image, navigation, some description and finally list of related items in the sidebar:
Now lets apply decomposition:
It has a bit more code (compare LoC to monolithic example) but now it is very flexible, scalable and more declarative.
Business is above all other abstractions
Your app structure must reflect your business logic. Frameworks or tools you use should not have a crucial impact to hierarchy of your directories. Business logic must have a priority over everything else.
Every person will be more familiar with business logic coming from real life examples, rather than with framework abstractions.
Of course you can’t avoid an impact of framework or tools to your app, but try to minimise it. Whenever it is acceptable isolate business logic from frameworks and tools abstractions.
When we were starting our first react/redux app we were researching best practices in codebase structuring. At that time component/container approach was pretty popular, but we didn’t like it:
- I personally still don’t see the clear difference between containers and components
containers/directories make it difficult to reflect business logic models to directories hierarchy
- deep nesting is not well covered.
This approach popped up more questions than answers and could potentially lead to longer code reviews and slower iterations just because not everyone understand it “clearly”.
We came to own approach in which hierarchy of directories is fully based on business logic models. It does not differentiate components by logical or presentational type but rather states that:
- all components are equal and any component can and should include both logic and presentation.
- any component can have nested components
Let me show an example of calendar app which has day, week and month grids, header and navigation between next and previous dates, and also date picker:
Date picker and header with navigation are common to all grids, so they are moved into
generic/ folder. Other components are unique, they shape the structure exactly according to business logic.
What is the reward for well designed app structure?
Imagine new developer joins the team developing Calendar app from last example and his first task is to change the month grid item — adding event participants list there. He doesn’t have to dig too much, he navigates directly to Month/GridItem, adds new component Month/GridItem/Participants and do all the work just there. Pull request diff will include changes to Month/GridItem and nothing else. Component is isolated and encapsulates logic, presentation and all the facilities it needs, such like redux calls.
So, the reward is fast collaboration between developers and smooth learning curve to newcomers. Less docs and less of personal conversations. You get faster iterations with lower maintenance cost.
Ottofeller is the software development company specialising in front end of complex web applications and promising cutting edge technologies.