This post is based on the series of posts: Modernizing a jQuery frontend with React. If you want to get a better overview of the motivation for this post we recommend you first read our initial post.
Nowadays, it is very easy to set up a small React application, or to start one from scratch. Especially if you use create-react-app. Most projects probably just need a few dependencies (for example, for state management and for internationalization) and a src
folder with at least a components
folder. I guess this is how most React projects start. Typically, though, as the project grows the number of dependencies, components, reducers and other shared utilities tends to increase in a, sometimes, not so controlled manner. What do you do when it is no longer clear why some dependencies are needed or how they work together? Or when you have so many components it becomes difficult to find the one you need? What do you do when you want to find a component but you don’t really remember its name?
These are just some examples of the questions we came across while rebuilding our frontend at Karify. We knew that the number of dependencies and components was eventually going to grow out of hand. That meant we needed a plan that would be scalable enough to keep up with future development. This plan involved defining conventions for our file and folder structure, code quality and also defining the overall architecture. Most importantly, all this should be easy to pick up by new developers without requiring them to have much insight into all our dependencies and code style.
At the moment of writing, we have about 1200 JavaScript files of which 350 are components with 80% of unit test coverage. Since we still believe in the architecture and conventions we created we thought it would be a good idea to share them. In the following sections, I’ll go through how we set up our project and also some of the lessons we learned.
We went through multiple phases until we figured out how we wanted to organize our React frontend. Initially, we thought of placing it in the same repository as our jQuery frontend. However, the imposed folder structure of our backend framework made this option undesirable. Next, we thought of moving it into a separate repository. This worked well at first, but with time we started thinking about creating other frontends, such as a React Native frontend, which motivated the need for a component library. This led us to split this new repository into two separate repositories: one for the component library and one for the new React frontend. Even though this seemed like a good idea, it resulted in a very complex review process. The relationship between changes in the two repositories became unclear. Finally, we chose to bring them together again into one repository, but this time as a monorepo.
We chose a monorepo because we wanted to create a separation between our component library and our frontend applications. The difference between our monorepo and others out there is that we don’t really need to publish the packages inside of it. For us, the packages serve only as a means for modularity and separation of concerns. It is especially useful to have different packages for every different application since we can specify different dependencies and scripts for each one of them.
We set up our monorepo using yarn workspaces with the following configuration in our root package.json
:
"workspaces": [
"app/*",
"lib/*",
"tool/*"
]
Now some of you might wonder why we didn’t simply use a packages folder like in other monorepos. Mainly because we wanted to create a separation between our applications and our component library. Besides that, we also knew we needed to create some tooling of our own. So, we came up with the folders you see above and here is an explanation for each one of them:
typography
, media
and primitive
packages;Regardless of where we place them, all our packages always have a src folder and optionally have a bin folder. The src folder of our app and lib packages might contain some of the following folders:
redux
or useReducer
;redux
store or useReducer
;react-router
components or history
functions;redux
state or our API payloads.This folder structure allows us to write really modular code since it creates a clear separation of concerns between different concepts defined by our dependencies. This helps with the lookup of variables, functions, or components in the repository, independently of us knowing if they exist or not. Furthermore, it also helps keeping the contents of these folders to a minimum, which in turn makes them easier to process.
One challenge with this new folder structure is to make sure we stick to it. It is tempting to start creating different folders in different packages and organizing files differently between them. While that might not always be a bad idea we would end up with a mess if we don’t do it consistently. To help with that, we created a file system linter which I’ll describe in more detail in the next section.
#programming #typescript #react #tdd #javascript