When you have a bigger codebase that, again, scales over time, what I found useful is to not extract all your components in different files. This is one of the problems with applications and with codebases as they grow, right? You tend to build each component, you have each component in a new file. In this case, for example, the header of the dashboard component here, again, the code is not important, it's just more like a concept here, so there's a header exported from this file, but this file also contains, actually you can see it here, contains another component called search input group. This sounds like the kind of component you want to extract away, right? But it is very specific to that header. It is used only once there, so the decision is that whenever this happens, you keep the components next to the parent component. Even though this file gets long, and I can show another example here where we have the pricing plans on the checkout page, and this is like a complex UI with all sorts of things. Again, there are tons of components here defined as you can see in the same file. What's important is that the file exports a single component. Everything else is kind of like details of the main component and is not abstracted away, and is not taken someplace to be reused later. This, again, coming back to the lesson is collocation in the sense that when you write the code, you write it in a single file as long as you keep the concern is the same. You're just building this new component, even if it's broken down into multiple components or maybe it has some utility functions.
From the read perspective, it's also super beneficial because anyone comes in, and when they know they have to change something on the plans page, they don't have to search for the actual component or smaller component that actually represents or actually has the change that they have to introduce. You might be looking at this and thinking, okay, is this a case against reusability? You shouldn't reuse too much code in codebases? The answer is yes and no. As always, it depends because lesson number three is that reusability is a double-edged sword. You tend to think that everything, like, the better you reuse stuff, the better you abstract away utilities and components, the better it is for the codebase in the long run. But I would argue that there's a good part and a bad part to reusability and you have to balance them. On the good part, you have things like abstracting away black box, you have deduplication, obviously you don't repeat yourself and you have separation of concerns when you do it right. On the bad axis, you have zombie code, right? Assuming that some of those smaller components are written in separate files, for some reason, the main file changes, it no longer uses one of the small components, but you don't have a direct correlation that, oh, I have to remove this.
Of course, there are tools that help you with that, but fundamentally, when you're thinking about making the change, you don't have to think where else is this thing used or something like that? You have unnecessary abstraction, like I mentioned with the example earlier, like the API thing. If you have a single exception to a kind of design or architecture, then it's better to make it an exception than to change the abstraction to support that exception. Finally, change propagation, which means once you make a change in a file, how many files actually depend on that or how many places, you know, if you have a piece of code that is reusable and you have to change it, then are you sure that in all the places where that thing is used, you're not introducing some bug or some issue, plus the fact that you have to go through a couple of files or a set of files to make a single change to the code? So, you're kind of dangling this slider whenever you have to think about reusability, right? How much do you want to reuse? How much do you want to duplicate? I found it useful to have your own framework for this and think of it as, okay, should I reuse this thing? Just a couple of questions that I like to ask about this. Will these things change together? Meaning that if the parent component changes, does it mean that I have to go in the child component as well and make a change? Or if the component changes, does the utility that computes something also have to change? Because if they change together, they might better stick together than have some sort of reusability or abstraction there. So, if you do decide to abstract something away, how often do you change that reusable code? Thinking about change propagation, right? The moment some piece of code that is reusable needs to change, there's a higher chance that you introduce some sort of bug or regression because of the number of places where that thing is used. And finally, it's very important for the read phase, do you need to understand the code that is reused? So, you write the code, you extract something in the utility, does the name of the utility give you enough information so that when someone else comes in and reads the code, or you come in after a while and read the code, do you know what's there or do you have to actually open that to figure out what's happening?
Lesson number four, speaking of understanding that code, is to leave traces behind. This is very interesting. I really like to think more about this.
Comments