React Hooks — Now What do I do?
Hooks, hooks, hooks. The blogosphere has produced a metric ton of articles on how to use these wonderful little tools. A majority of blogs are missing examples of the developmental pattern and mindset changes required to support an enterprise application. I see this as being analogous to a carpenter being shown a bunch of shiny new tools in ads that appear like they would make wood working a LOT easier/faster. However, they never really get to see how to truly use these tools. So, the tools are not being used to their fullest, or in some cases, incorrectly. This article is an endeavor to help shed some light on this topic, and close a bit of a knowledge gap.
The official React documentation is a good source for introducing hooks. Please, make sure you are comfortable with these tools before continuing.
As I read more about hooks and played with them, I quickly came to realize that the typical patterns we use needs to evolve. In some cases, a lot! For example, the ‘go to’ Container pattern. Oh, that turned into an unnecessary mess. The more complex the UI application, the more careful you must be in using these patterns so as to not produce the proverbial “ball of mud”.
Some guiding principles
I use three pillars to drive my software design beyond typical SOLID. I find it makes development A LOT easier. Disclaimer: Every seasoned developer comes up with their own principles for how they write software over time. The below information is NOT the only way, but it does help to get us on the same page when thinking about hooks.
- Extensible — The design follows the Open/Closed Principle already on a small scale. What about the overall strategy? The design must be easy to add more functionality to with little effort. Typically, this means more generic code; just be wary of over-engineering.
- Testable — It must be easy to write unit tests. Again, SOLID principles already support this in a primitive form. However, even if you use techniques like Dependency Injection (or some other version of IoC), you can still make your life very hard for testing (i.e. function side effects, not following immutable data principles, etc.).
- Maintainable — How much tribal knowledge is required to understand the design? Does your code have side effects? Is your code complexity high? Are you really following the Single Responsibility Principle? Seriously, check this twice. It could save you a lot of trouble down the road.
Now that we have a common basis of understanding for software design, let’s jump into the core principles of a hooks only world. Don’t be scared, I know they are aggressive. However, this will force you through the learning curve, AND you will find your designs to be FAR easier to read/test/maintain when you get into a rhythm. May I dare say that unit tests start to become trivial, perhaps? Wouldn’t it be nice if that was the norm? Even for the enterprise class problems.
- Avoid class based components. If you think you need one, you are not thinking in hooks. Mixing typical class based design patterns in React with functional components is a recipe for unit testing disaster. You have been warned!
- Drop redux, you actually don’t need it. If you are working on an existing code base that already uses it, then stop expanding its use if possible. Start migrating to hooks for new code. The relationships that redux provides between components and props can be more targeted, more meaningful due to simplicity, easier to test, and all with less boiler plate code. It takes more forethought in the design, but it pays off in the end.
- Keep data as local as possible to where you need it. Another way to say it, bring data from the source to exactly where you need it. Use few to no intermediaries to deliver the data. Unless you are dealing with HUGE amounts of data, this works really well.
- Use a data cache as your single source of truth. For example, the apollo client. Although you can use other tools as well. This is just convenient if you are already using GraphQL. Client side queries are easier than you might think. This abstraction provides a noticeable amount of freedom and flexibility in designs.
- Controversial statement here: Container, Singleton, and State Lifting patterns can arguably be considered Anti-Patterns in the world of React. These violate the three pillars above: Limited extensibility, and more challenging testability. With hooks, migrating to alternate patterns to solve the same problem makes life a LOT easier.
If you are already planning to argue on item 5. Good! This is the mental exercise you have to go through to write clean, strong code with hooks.
Complementing patterns for hooks
There are some non-traditional patterns to consider using that play nicely with hooks.
- Provider — Replaces the Singleton pattern. The backbone implementation here is the React Context API.
- Observer — Replaces a portion of prop and event handler passing to reduce complexity. There are many variants of the Observer pattern. The core idea should be embraced, not the textbook pattern. So, don’t copy from the following link if you want to learn more! Study the idea behind it. Reference for the Observer Pattern.
- Non-Rendering Containers — A different way to use the Container pattern and State Lifting. Short gist, these containers don’t actually render any HTML of their own. They simply martial child components to shape the component hierarchy. They become light-weight tools that help reduce cognitive load, and make relationships between components more obvious. This also makes them exceptionally easy to test.
Techniques to consider
- Presentation components are atomic, standalone, units. Rather than ONLY setup presentation components to receive props from a parent (i.e. the traditional Container pattern), they should also be able to read/write the data they require from/to the data cache. This becomes a trivial effort with hooks.
- You do not need to always pass props! Use the Context API. Write your own High Order Components or High Order Functions to help with common code decoration. A gotcha with the Context API: any fragment of an update in a given context will trigger a re-render for any component using that context. Even if it is not using that PART of the context. Design carefully.
There are many more items to think about nowadays when designing components. These are just a couple of big ones that lay the ground work for some nice extensions in advanced functionality. For example,
- Item 1 opens the door to easily migrate into Concurrent Rendering when that feature reaches GA in React.
- Item 1 opens the door to easier migration into working with your app in ‘offline’ mode.
- Item 2 decreases the cognitive load on developers, the tribal knowledge in understanding the workflows, and allows for easier composition of components wherever you need to use them.
- Item 2 makes unit testing easier to complete since the number of techniques employed is reduced. Boiler plate code is also reduced, and easier to work with.
Take aways and next steps…
Nothing stated in this post is a quick “light switch” fix for things. It requires evolving your thinking which takes time. It takes commitment. It takes trial and error. Not to mention internal debates within your own mind on what is right or wrong. Whatever derivative of the above items you wish to take on, stick to it.