How Granular Should your components be?

We all love splitting up our code into small chunks, but where is the limit, is there even one?

Have you ever worked on an application that had a ridicolous amount of lines in every single component. Noticed how it was really hard to find the relevant part of the code that you wanted to fix or improve, noticed how a lot of times certain functionality would repeat itself in different components, and how it was really hard to maintain and test?

Or how about the other extreme, were you ever horrified to open up the components folder only to be greeted by a waterfall of files and folder. Multiple of those files only being used once or twice in the code base.

Both of those approaches are not ideal, and both of them are very common. So how do we find the right balance? How granual should our components be?

Let's start off by tackling the first extreme.

Big do-all components

The first extreme is the one that I have seen the most. The one where a component has a lot of lines of code, and a lot of different responsibilities. A confusing mess of code that is hard to maintain and test. But how does it come to this in the first place, what drives a developer to write such a components? Well there are a few reasons for this and understanding why it happens is a good way to prevent it from happening in the first place.

1. No planning phase

This is by far the most common cause of the issue, and it is also the easiest to prevent. The problem is that a lot of developers don't plan out their components before they start writing them. They just start writing code and then they realize that they need to add some more functionality, so they just add it to the component. And then they realize that they need to add some more functionality, and so on and so on. This can very easily be prevented by simply planning out your components and functionality before you write a single line of code. There are a few methods that you can use to do this. For instance what generally helped me get started was simply writing down a skeleton of a component with comments before I started writing the actual code. This way I could plan out the functionality and the structure of the component before I started writing it.

Here's how something like that would look like

// MainNavigation.tsx

/*
This component is responsible for rendering
the main navigation of the app
*/
/*
  Props:
    - links: an array of links that should be rendered in the navigation
    - activeLink: the link that is currently active
    - onLinkClick: a function that is called when a link is clicked
*/

// implement the component here
export const MainNavigation = () => {
  // destructure the props 

  // define the component's state

  // define onLinkClick handler

  // render the component
  return {
    /*
    We want to display a logo on the left side of the navigation
    NOTE: the likelyhood of the logo being re-used in 
    other parts of our application is quite low
    so we can just render it here, no extra component needed
    */

    /*
    render the links out here
    NOTE: we should probably implement a Link 
    component that we can use here
    it would take a single prop of type Link and render it out
    */

    /* 
    we do have user authentication so a user menu 
    for authenticated users is a must
    NOTE: the user menu is likely to be 
    re-used in other parts of the application
    so we should probably implement a 
    UserMenu component that we can use here

    Additionally the usermenu should turn into a 
    login and signup button when the user is not authenticated
    this additional complexity is a good argument for 
    implementing a UserMenu component
    */
  }
}

As you can see in the example above we've already planned out the structure of our MainNavigation.tsx component and we've also planned out the functionality that we want to implement. This way we can make sure that we don't add too much functionality to a single component. Additionally as I was skeletoning out the component I noticed good candidates for separate components. For instance the UserMenu and the Link components. This way we can make sure that we don't end up with a big do-all component.

Now we can start writing the actual code for our component, which can look something like this

interface ILink {
  text: string;
  href: string;
}
interface IProps {
  links: Link[];
  activeLink: Link;
  onLinkClick: (link: Link) => void;
}

export const MainNavigation = (props: React.PropsWithChildren<IProps>) => {
  const { links, activeLink, onLinkClick } = props;
  const [isMenuOpen, setIsMenuOpen] = useState(false);

  const handleLinkClick = (link: Link) => {
    setIsMenuOpen(false);
    onLinkClick(link);
  };

  return {
    <div className="logo">
      <img src="logo.png" alt="logo" />
    </div>

    {links.map((link) => (
      <Link
        key={link.href} // NOTE: use a stronger key here 
        link={link}
        isActive={link.href === activeLink.href}
        onClick={handleLinkClick}
      />
    ))}

    <UserMenu />
  }
}

And just like that we've written a clean and maintainable component. The component is easily readble, understandable and testible. We correctly discovered where further separation of concerns was needed.

2. Lack of experience

If you're new to react or web development in general the term separation of concerns might be foreign to you. It's nothing too complex it simply means that generally speaking each component should do one thing and do it well sorta like the original UNIX ethos. Meaning each component should be responsible for a single part of your application, e.g: you want a Navigation component to handle the navigation of your application, you want a LanguageSwitcher component to handle language switching, footer component to display the footer, UserMenu to render the user menu and so on. This way you can easily re-use your components in other parts of your application. Additionally it makes your components easier to test and maintain.

3. Team size / mentorship

If you're working in a small team or going solo on a project, it's very easy to fall into bad habbits like writing big do-all components. this is because working in a team or collaborating with other people naturally forces you to write cleaner and more maintainable code especially if you have any mentorship processes in place such as: pair programming, code reviews, etc.

But you may be wondering, "ok Dave but what if I'm working on a hobby project or my own solo project, I can't reap the benefits of working in a team".

Well you're partially right, there's one very easy solution to your problems it's called open source, if you simply open up your project to the world and let other people contribute to it, you'll naturally want to write cleaner and maintainable code since you will know that eventually other people will look at your code and we all want to put our best foot forward when we know other people are looking don't we. Additionally open sourcing your project will allow other people to contribute to it, which is a great way to learn and improve your skills.

If you're however already working in a team but don't have any mentorship processes set up I highly recommend you to look into them, they will help you and your team write cleaner and more maintainable code.

4. Pressure to deliver / lack of time

This one is fairly common, perhaps you work in a team where you're under a lot of pressure to deliver, in a constant push to get features out the door. This can lead to a lot of bad habbits such as writing big do-all components, not writing tests, not writing documentation, etc. This is a very common problem in the industry and it's not easy to solve. But I think the best way to solve it is to simply talk to your team lead or manager and let them know that you're struggling with the pressure to deliver and that you would like to focus on solving some of these issues that may seem trivial and unimportant but can very easily snowball into a big problem later on.

These four points are just some of the possible reasons why your codebase might be littered with huge components that do everything. I'm sure there are many more reasons out there, but I think these are the most common ones. Which brings us to the opposite extreme

A ton of small components

This is the opposite extreme of the previous problem, you can very easily take a good thing like separation of concerns and take it too far and ruin it by overdoing it. It always reminds me of one of my favorite quotes:

Premature optimization is the root of all evil

~ Donald Knuth

Simple, elegant and eloquent. It applies to our scenario perfectly. Too many times developers will rush and start making different components for every single thing without ever stopping to think if this component will ever really be used in more than once place. If the answer is no, then it simply does not need to be it's own component. Here's where I'll resort to using a quote yet again to drive my point home, in this example I'll be usig Dan Abramov's famous twitter thread:

It is a guideline though. It means literally “start by putting everything in one file; when it feels like it’s annoying, start splitting them up; what THAT gets annoying, maybe add some folders”.

~ Dan Abramov

Now this may go against my previous advice to carefully scaffold out your components and functionallity via comments, but I don't think that what Dan meant there was to write a huge component and then split it up into smaller components, I think he meant to start with a single file and then split it up into smaller components as you go along. This way you can make sure that you don't overdo it and end up with a ton of small components that are only used in one place.

Conclusion

In this article we've talked about two extremes of the same problem, big do-all components and a ton of small components. We've also talked about some of the possible reasons why you might end up with either of these two problems. I hope you've learned something new and I hope you'll be able to apply this knowledge to your own projects. If there's one take away from this article it's this: try and plan out your components and functionallity before you start writing code, but don't overdo it either. Try and find a good balance that works well for you and your team. Both big do-all components and component waterfalls are huge issues that can easily be fixed by simply taking a step back and thinking about your code.

Adopt a measure twice cut once approach to your code and you'll be fine.

Hope you enjoyed this article, if you did please consider sharing it with your friends and colleagues. If you have any questions or comments feel free to reach out to me on twitter: @dayvsterdev