Engineering

Hot Take: If you’re using Hooks instead of Containers, you’re doing it wrong

Stephanie Yang
Feb 20, 2024
Copy link

Introduction

As a developer, one thing that comes into play with any new project is figuring out which design patterns to follow and what is most appropriate for the given project. At Good Code, we often assist startups in building applications from the ground up. This means we can implement design patterns from the start, making it easy to scale for the future.

One pattern we often recommend is the Container-Presentation design pattern in React applications. Let’s dive into what exactly this pattern is and why we recommend it.

What is a Container vs a Presenter?

Container components are React components responsible for handling data and logic. They are typically used to fetch data from an external source, manage state, and pass data down to presentational or "dumb" components.

Presenter components are React components responsible for handling the display of the data being passed to them. They don’t worry about how the data gets to them, just how the data will display when it gets there.

What are the cons of this approach?

Let’s first talk about a couple reasons why you may not choose this approach:

  • More code. This one’s obvious, but having to create Containers for components definitely adds a non-insignificant amount of code even if it’s simple code. If your project isn’t too complex, there may not be a reason to introduce this pattern and add the extra weight.
  • The Containers themselves aren’t usable, but as we almost always work with GraphQL, this isn’t a big concern for us as we curate operations specific to each page. But if your project isn’t using GraphQL, this might be a larger concern for you.

What are the pros of this approach?

There are certainly other approaches that are popular such as Hooks or HOC (Higher Order Components), but after building several large scale applications, I’ve found that while the Container-Presentation pattern may not be a perfect solution, it does have many benefits:

  • It’s obvious. There’s a clear distinction between where the logic goes and where the UI elements go. This simple separation of concerns lets us easily bring on new team members to a project, or have any team member help out, code review, or fill in for another developer since they won’t have to learn the ins and outs of the project
  • Single API request. When implementing the hook approach, you usually end up generating your hooks automatically. This ends up making them more shareable, but when it comes to implementing a page, you often have to combine multiple hooks to render a single page. This results in multiple API calls or alternatively you could create your own hook but at that point, you’ve just made a Container, but in hook form without the benefits of the Container.
  • Conditional rendering. As we fetch, filter, mutate and refetch data, one major benefit I’ve noticed is that we can easily conditionally render loading states and keep track of previous and current data cleanly.
  • It keeps things DRY (Don’t Repeat Yourself). This will ultimately help keep visual components more modular and reusable.
  • On a similar note, UI testing also becomes easier! Presentation components don’t need to worry about logic, so similar to a pure function, the data that’s passed in should always return the same results in the UI. This allows for things like snapshot testing, or using tools like Storybook (which we love!).

Compared to other mentioned approaches, Hooks is another popular option. The downside I’ve seen with hooks is that it couples the components to the API and, in turn, creates a bit of a nightmare for testing. The HOC pattern can be difficult to follow and can lead to prop collision and prop drilling issues.

Comparing Approaches

Let’s compare the Hooks approach to the Container-Presentation approach through code.

Recently, I worked on a project where the Hooks pattern was preferred. Aside from the testing aspect being more complex, I also noticed this approach lost a bit of the GraphQL awesomeness as multiple query requests had to be made to combine data.

The queries were structured like this:

This generated custom hooks for each query that we could use:

At some point, we needed to combine this data and check for results that matched a filter (which, was yet another hook). And yes, this was all within a custom hook. So, for those who aren’t keeping track - we now have 3 hooks being used within a hook that is then being used as part of a component which uses… you guessed it, even more hooks.

The wrapper hook looked something like this (spoiler alert - this wrapper hook is basically a Container. Remember that single API request that I mentioned earlier?):

And the component looked something like this:

With this simple example, it might still be fairly easy to follow the code. Handling a filter and search term change here seems straight forward enough. But it’s concerned with both handling the logic and displaying the data. On top of that, the hooks within hooks adds extra layers of complexity, ultimately making the code harder to follow. For example, how does items in this case get updated? To figure that out, we have to go into the hooks file, taking us out of the context of this component. If the file structure is that the hooks stays within this component too, then it feels like we lose the reusability of it or, for other usages, we run into the same problem.

Now, let’s see how things play out if I convert the above to a Container-Presentation approach:

And the view component would look something like this:

Here, we end up with two files - a Container component which deals with all of the data fetching and logic, and a Presenter component which purely handles displaying the data and relies on callbacks to the Container to handle any sort of state changes. The Container stays within the same context of the component and I can easily follow the code. If we adhere to this pattern throughout the app, then I can know that this is true for any component, not just this one. There’s no hunting!

Of course, I’m not saying that hooks are bad and should never be used. Like anything else, it’s just another tool in our toolbox and we use it where appropriate. You’ll notice that we’re still using the useFilters hook - that’s because we want this piece to be reusable throughout the app. We’ve designed it to be generic enough to work on any page that needs to be searchable and/or filterable.

Ultimately, we find the second approach easier to follow and also easier to jump in and start making progress. While we can see that the Container component and GraphQL query here are not reusable, it feels like an acceptable trade off since we write GraphQL queries specific to each page. On the flip side, the hooks may be more reusable, but if another component wants to use it and wants another field included, all of the other queries using this hook would also receive the field. There’s no way to curate things specific to a page in this approach without rewriting the query anyways and abstracting that into another hook. Each of these abstractions just makes the entire project harder to follow.

Conclusion

In the end, the most important thing to remember is that everything changes. This is what works or us now, but we have to be flexible and adapt to new ideas and concepts so we continue to grow.

About Good Code

Ready to step into a world where cybersecurity meets a personalized, unparalleled user experience? Unlock the doors to a future where UI/UX isn't just a feature but a testament to seamless collaboration and innovation. Head over to www.goodcode.us – your next tech adventure begins now!

© All rights reserved, Good Code, LLC 2024.