Prelude
Though not to discount them entirely, the purpose of this blogpost is to highlight the downsides of React Hooks. Hooks are glorified for their simplicity and ease of reusability, but they don’t come without a price. I haven’t added any code examples, but my intention is to pinpoint potential problems that the reader, as either a newbie or an experienced developer, can identify in their own code. We often take our codes for granted and are not critical enough about it. Being sceptical about one’s own code is one of the greatest virtues a developer can possess.
What hooks do well?
Hooks make it easy to extract logic and they make it easy to reuse it. While using class components, it is possible to share code between components which makes it much more troublesome. Unfortunately, they don’t allow us to mix old classes of components with themselves. They also make creating small components really easy since you usually don’t need big class component boilerplates.
When are hooks debatable?
Hooks can be used in a better way than the “default” one shown in React docs. Hooks are an interesting tool, the downsides of which can be overcome by properly designing the architecture of frontend solutions. However, the default approach often leads to the problems described below.
Tools should also be measured by how easy it is to create a bad code using them.
My personal opinion is that hooks can lead to ending up with a messy code much easier than class components which strictly define component’s callbacks and state.
So why are they bad?
- Hooks indirectly make functions that should be pure by passing the state in a “magical way” compared to the old school HOC/class component approaches. React’s initial idea was to have a one-way data flow, but this idea is basically destroyed with hooks.
- Hooks require passing dependencies. If they are not passed, it can lead to unexpected and hard-to-debug behavior. Despite raw values, any callbacks should be added as dependencies as well. If one doesn’t want a hook to be called, such callbacks may have to be wrapped in a `useMemo`/`useCallbacks` hook quickly propagating into unmaintainable code filled with hooks.
- `useEffect` hook is the worst of all (since it can and is used for just about anything). It is usually used to modify DOM, modify state, call callbacks and so on. If there are several of these hooks with a few dependencies each handling the logic, it’s impossible to decipher what’s actually happening.
- Hooks rely on dependencies, but at the same time, objects or arrays cannot be used as dependencies very easily because they are not comparable in a simple way (the default mechanism will compare references only). Some hacks or tricks have to be utilized to overcome this issue.
- Hooks rely on an execution order so it’s impossible to call a hook conditionally – it would change the hook order. That reduces the ability to actually structure the hook/component’s code.
- Hooks are usually used for application logic, yet they cannot be called directly. Instead, they rely solely on property change but without the ability to actually see what was changed. These types of change-tracking hooks can be built, but they only magnify the problems in the long run. Logic that is only built upon property change seems to be attractive (like using RxJS for example), but in the long term it makes code very hard to reason with if everything is “too reactive”.
- Let’s imagine that hooks are connected with each other – The `useEffect` hook reacts on search field change, changes the debounced-state of the filter (hook) and that triggers the search fetch hook. The downloaded data triggers the change state on two state hooks (data and loading state), which then trigger other hooks to operate, and everything can trigger rerenders at any point. This is not such an uncommon situation, yet it’s unnecessarily hard to go through using only dependencies as reference for code logic.
- All hook problems can be solved using hooks – so hooks bravely solve problems that they are causing.
Should I drop hooks?
No. They are a decent tool that really gets the job done. Without them, we would still be using class components. But let’s be conscious of their drawbacks and look for solutions – they are few at least. We need to focus on architecture, be skeptical about existing codes, and find a better way to handle those issues.