Hello everyone, I'm Juan, and today I want to share with you some tips and tricks that I use to write more reusable code. For the past 2 weeks, I've been writing about how to develop projects faster and how it's irrelevant to spend too much time looking for the perfect code at the outset of the project. Although, you are not always going to be working on your own projects, and sometimes you won't be in a rush. Or maybe, just maybe, you are one of those accountable developers who, after spending time creating something amazing in just 48 hours and applying the strategies that I shared, now you are at the point where you have to fix the mess that you've created, developing as fast as you could.
The example project that I'm going to use is React, but you can apply most of these things practically anywhere you want. I'm going to be showing folder structures, mentioning the architecture that I'm following, and also providing code snippets with explanations and examples of multiple cases.
What is going on with the industry?
I've been lucky, and thanks to that I've worked at companies of different sizes and styles, some bigger, some smaller, but in all of them, I saw a pattern that repeats everywhere: the code is broken. No structure whatsoever—logic, styles, types, everything in the same place. That might not be a problem when you just have a bunch of files, but when your codebase is gigantic, it is really easy to make it impossible to grow or sustain over time.
Right now, I'm working for a huge company with over 29,000 employees. You wouldn't expect a company like this to make the mistakes that I was describing, but it does. After talking to people who theoretically know how to lead a project and help sustain a readable and scalable codebase, I came to the conclusion that most of us lack the knowledge or interest, or both, when it comes to structuring the code. Hopefully, this post will help someone out there write better code and know how to sustain a project over time.
The architecture that I use - let's scream
Hopefully, you've heard about this architecture before. It's called Screaming Architecture, and the name is no joke—it literally screams at you. When you enter the project, it perfectly summarizes what's going on without needing you to run the project. Let me show you:
It's worth mentioning that I made some modifications to the Screaming Architecture to better suit my needs, and that is something you can do too. As always, there is no perfect solution, just solutions that may adapt better to your specific case. Feel free to keep the things that work best for you and discard the rest. But when you do it, be honest with yourself and ask the following question: "Am I really doing this because it is better or just because I don't want to learn something new?"
Explaining the basic structure:
In the components folder, you'll create all the components that you need to reuse across your application. These are not specific components, and if you are going to put it there, it is because you are already using that component and you want to reuse it somewhere else.
In the interfaces folder, you'll define all your types. We are doing it this way because most of the time, you are sharing types across your entire application, so it doesn't make much sense to me to have the types in a deeper folder.
Finally, in the features folder, you'll have most of the code, following a recursive structure:
As you can probably see here, we are creating a Logic file because we don't want the logic of our application leaking into our visual components. Let me show you an example in React to see how this works. The component that I'm going to show you is Window. It renders on the screen the Window's width of our user. You'll notice that we have almost no logic here, just a <p>
tag that is returning the width size of the screen.
import Logic from "./Logic";
const Window = () => {
const { size } = Logic();
return <p>{size}</p>;
};
export default Window;
And here's the logic:
import { useState, useEffect } from "react";
const Logic = () => {
const [size, setWindowSize] = useState(0);
useEffect(() => {
// get the windows width
setWindowSize(window.screen.width);
}, []);
return { size };
};
export default Logic;
We have successfully isolated our logic and our view. There will be people reading this and thinking that this is similar in some ways to the Model View Controller pattern, and they are correct. This is really helpful and most of the time will avoid having a file that is overwhelmingly huge to read. But this is not a silver bullet, and there will be cases where you'll have to take other approaches. So let's see other concepts and examples.
Abstraction and Encapsulation
When I started programming, I remember that one of the things that I hated the most was OOP (Object-Oriented Programming). For some reason, I couldn't get it. Most of the time, I found myself asking why I would ever write something like that. At some point in my life, I realized that OOP was amazing and really helpful. Concepts like SOLID or Abstraction were pillars of any clean code. So for that reason, before moving to some code snippets, I consider it appropriate to make a stop on some of those concepts.
Abstraction
It is by far one of the most important things on the priority list when it comes to writing the best code possible, and also most of the time, it's one of the most forgotten. I've seen people who write code that astonishingly solves the problem they are presented with, but when you start to take a deeper look and ask yourself (or them) how this is going to be reusable, you find no answer. Most of the time, that is due to a lack of understanding of the Abstraction principle.
Abstraction can be understood as the capacity to ignore the obvious and put the necessary attention on the more general aspects of the things that we are confronting (the big picture). If you are trying to describe a glass of water, you may say that: "it is a piece of transparent material, containing water," and indeed, that would be correct... on the small picture, but useless on the big picture. Instead, we can say: "it is a piece of material, with some geometry, restrained to a structure able to contain liquids, with a color and transparency determined." The second description is able to generalize a lot better than the first one, giving us the opportunity to use the same description for many things. This is Abstraction, the skill to see the "big picture." Work on it, write code thinking about it, and you from the future and the people who will have to work with you will be grateful.
Do you see any Abstraction in the previous example? It has, but it might be hard to see in its current state. Let me make a little change and see if you get it:
import { useState, useEffect } from "react";
const useWindowSize = () => {
const [size, setWindowSize] = useState(0);
useEffect(() => {
// get the windows width
setWindowSize(window.screen.width);
}, []);
return { size };
};
export default useWindowSize;
Now instead of "Logic," we have useWindowSize. Separating the logic from the view gives us the opportunity to see that we have a piece of code that is reusable. Now that we have this custom hook that we can reuse and generalize a lot better, what do you think if we move it to the utils/ folder, so we can reuse it later?
Encapsulation
Would you give anyone entire access to your phone? Most likely you said no; otherwise, you are crazy since with that, any person would be able to enter your bank account and personal contacts list. We don't want that to happen, so we'll let some people access some part of our phone. We won't expose our entire phone to everybody. This, my dear friend, in an intricate and funny way, is Encapsulation. You could summarize it as: "Some can see and touch it; some just can see it."
Great, this is helpful in many ways for your code. It becomes handy, especially when you are working with too many people or when you have code that is going to be used by someone else, like in the case of my React Library.
Now we understand what Encapsulation is. Do you see it applied in any way in our useWindowSize? Yes! You got it. We are keeping setWindowSize private, just making it accessible to the code inside of useWindowSize and no one else. That's great because in that way, no one will be able to alter the window size besides the logic of our code, making it more secure and predictable.
Finally - Code Snippets
Sorry for the stop on OOP concepts, but it's necessary. It's amazing how those two concepts can change your life. With the new knowledge and understanding that we now have, we'll be able to better appreciate the code that we are going to see.
I haven't seen it anywhere else
The code that I'm going to show you is something that I've been using maybe too much lately and for some reason, no one is talking about it. I'll give you context. Have you ever found yourself using a perfect and reusable component, but each time you have to reimplement some specific logic because you need to access that from a higher component? No? Maybe just me... Anyway, let's see the solution.
type fields = "text" | "image" | "number"
interface ModalProps {
show: boolean;
fields: fields[]
}
const Modal = ({ show, fields }: ModalProps) => (
return show ? <div className="modal">
{fields.map((field) => {
// Modal code...
}}
</div> : ''
)
const ImageGallery = () => {
const [show, setShow] = useState(false)
const images = [...]
return (
<>
<Modal show={show} fields={["image", "text"]}/>
{images.map((image, i) => {
<img src={image} onClick={() => setShow(true)}/>
})}
</>
)
}
Perfect, we have an ImageGallery that is using our "beautiful" Modal. In this case, imagine that the modal is dynamically creating a form to upload an image with a description. Now let's say that I want to add another page similar to ImageGallery, maybe a Comments section. I'll have to re-write the show logic. Let's try to avoid it:
type fields = "text" | "image" | "number"
interface ModalProps {
fields: fields[]
}
const ModalHandler = () => (
const [show, setShow] = useState(false)
const Modal = ({ fields }: ModalProps) => (
show ? <div className="modal">
{fields.map((field) => {
// Modal code...
}}
</div> : '')
return { Modal, show, setShow }
)
const ImageGallery = () => {
const { show, setShow, Modal } = ModalHandler()
const images = [...]
return (
<>
<Modal fields={["image", "text"]}/>
{images.map((image) => {
<img src={image} onClick={() => setShow(true)}/>
})}
</>
)
}
const Comments = () => {
const { show, setShow, Modal } = ModalHandler()
const comments = [...]
return (
<>
<Modal fields={["text"]}/>
{comments.map((comment) => {
<p onClick={() => setShow(true)}>
{comment}
</p>
})}
</>
)
}
We did it! In this case, it is an overkill scenario because it is just a show and setShow that may be arguably re-implemented in every case, but the important thing is the concept. With this approach, we can create more reusable components, and at least for me, it is more readable than the composition pattern. Speaking of which, why don't we take a look:
Composition Pattern
Let's create a grid to preview images:
const ImageGrid = ({ images }) => {
const handleAnimations = useCallback(() => {
...
}, [])
useEffect(() => {
handleAnimations()
}, [])
const complexGridLogic = () => {
...
}
return (
<div className="magic-grid">
{complexGridLogic()}
{images.map((image, i) => {
<div className="animated-container" id={`animate-${i}`}>
<img src={image}/>
</div>
})}
</div>
)
}
Great, we have our ImageGrid (again, it is just a concept). It will mount images and then animate them. Also, it has a "complexGridLogic". Now let's say that I want a CommentsGrid with extremely similar behavior. What a problem. Let's see how the Composition Pattern can save us:
const AnimatedContainer = ({ children, id }) => {
const handleAnimations = useCallback(() => {
...
}, [])
useEffect(() => {
handleAnimations()
...
}, [])
return <div className="animated-container" id={id}>{children}</div>
}
const MagicGrid = ({ children }) => {
const complexGridLogic = () => {
...
}
return (
<div className="magic-grid">
{complexGridLogic()}
{children}
</div>
)
}
const ImageGallery = ({ images }) => {
return (
<MagicGrid>
{images.map((image) => (
<AnimatedContainer id={image}>
<img src={image}/>
</AnimatedContainer>
))}
</MagicGrid>
)
}
const CommentSection = ({ comments }) => {
return (
<MagicGrid>
{comments.map((comment) => (
<AnimatedContainer id={comment}>
<p>{comment}</p>
</AnimatedContainer>
))}
</MagicGrid>
)
}
I see it and I love it. What a wonderful world it would be if everyone cared as much as you and I do about our code. Now we have 2 reusable components and 2 implementations that are perfectly reusable. This also gave us something called:
Separation of concerns
At the beginning of this post, I showed you the structure that I like to use for my projects. I think it is understandable and cleaner than many things out there, and a big part of that is thanks to the Separation of Concerns, a principle that I always try to follow. You can summarize it as: don't take responsibility for someone else's actions. It's that easy. The button shouldn't have the logic of the input, obviously, and the Grid shouldn't be in charge of rendering your images, but instead rendering any children.
It's that easy, really, and for some reason, sometimes I find myself arguing about it with some random co-worker (this happened to me last week).
Wrapping up
I'm finishing this post, and I feel like there's something off with it, but I'm not sure what it is. I have the feeling that I've tried to cover so many aspects that I couldn't go much deeper on any of them. So if you are interested in me talking more in-depth about some of these aspects, let me know.
Before you go
If you enjoyed it, consider following me, liking, and if you really enjoyed or found it useful, please help me to buy a new book, with just $20 would be enough for me to get The Art and Business of Online Writing
[--------------------------------------------------------------------------] 0% of $20
Top comments (2)
Interesting topic! Everything is explained articulately and clearly. For your project, consider checking out this free npm package: select-paginated.
Wow Shaogat, that library seems really interesting, I'll take it in consideration for future project