The Ugly Side of React Hooks

Written by niryo | Published 2020/10/04
Tech Story Tags: react | react-hooks | javascript | reactjs | react-native | react-hook | rethinking-programming | software-development

TLDR The Ugly Side of React Hooks is a point of view about Hooks. Hooks are a new concept, and a pretty weird one, but Funclasses are a concept, not a syntax. Hook function is not a regular function, because it has state, and it has a weird looking looking looking like a hook. Hooking functions is the biggest problem of React’s approach to state-of-the-box. Hooked functions are hard to reuse and don’t offer a way to “attach a reusable behavior to a reusable state’, leaving the state out of the box.via the TL;DR App

In this post, I will share my own point of view about React Hooks, and as the title of this post implies, I am not a big fan.
Let’s break down the motivation for ditching classes in favor of hooks, as described in the official React’s docs.

Motivation #1: Classes are confusing

we’ve found that classes can be a large barrier to learning React. You have to understand how "this" works in JavaScript, which is very different from how it works in most languages. You have to remember to bind the event handlers. Without unstable syntax proposals, the code is very verbose […] The distinction between function and class components in React and when to use each one leads to disagreements even between experienced React developers.
Ok, I can agree that
this
could be a bit confusing when you are just starting your way in Javascript, but arrow functions solve the confusion, and calling a stage 3 feature that is already being supported out of the box by Typescript, an “unstable syntax proposal”, is just pure demagoguery. React team is referring to the class field syntax, a syntax that is already being vastly used and will probably soon be officially supported:
class Foo extends React.Component {
  onPress = () => {
    console.log(this.props.someProp);
  }

  render() {
    return <Button onPress={this.onPress} />
  }
}
As you can see, by using a class field arrow function, you don’t need to bind anything in the constructor, and 
this
 will always point to the correct context.
And if classes are confusing, what can we say about the new hooked functions? A hooked function is not a regular function, because it has state, it has a weird looking
this
(aka
useRef
), and it can have multiple instances. But it is definitely not a class, it is something in between, and from now on I will refer to it as a Funclass. So, are those Funclasses going to be easier for human and machines? I am not sure about machines, but I really don’t think that Funclasses are conceptually easier to understand than classes. Classes are a well known and thought out concept, and every developer is familiar with the concept of
this
, even if in javascript it’s a bit different. Funclasses on the other hand, are a new concept, and a pretty weird one. They feel much more magical, and they rely too much on conventions instead of a strict syntax. You have to follow some strict and weird rules, you need to be careful of where you put your code, and there are many pitfalls. Telling me to avoid putting a hook inside an
if
statement, because the internal mechanism of hooks is based on call order, is just insane! I would expect something like this from a half baked POC library, not from a well known library like React. Be also prepared for some awful naming like useRef (a fancy name for
this
), useEffect ,useMemo, useImperativeHandle (say whatt??) and more.
The syntax of classes was specifically invented in order to deal with the concept of multiple instances and the concept of an instance scope (the exact purpose of 
this
 ). Funclasses are just a weird way of achieving the same goal, using the wrong puzzle pieces. Many people are confusing Funclasses with functional programming, but Funclasses are actually just classes in disguise. A class is a concept, not a syntax.
Oh, and about the last note:
The distinction between function and class components in React and when to use each one leads to disagreements even between experienced React developers
Until now, the distinction was pretty clear- if you needed a state or lifecycle methods, you used a class, otherwise it doesn’t really matter if you used a function or class. Personally, I liked the idea that when I stumbled upon a function component, I could immediately know that this is a “dumb component” without a state. Sadly, with the introduction of Funclasses, this is not the situation anymore.

Motivation #2: It’s hard to reuse stateful logic between components

React doesn’t offer a way to “attach” reusable behavior to a component (for example, connecting it to a store)…React needs a better primitive for sharing stateful logic.
Isn’t it ironic? The biggest problem of React, in my opinion at least, is that it doesn’t provide an out-of-the-box state management solution, leaving us with a long lasted debate on how we should fill this gap, and opening a door for some very bad design patterns like Redux. So after years of frustration, React team has finally came to the conclusion that it is hard to share stateful logic between components…who could have guessed. Anyways, are hooks going to make the situation any better? The answer is not really. Hooks can’t work with classes, so if your codebase is already written with classes, you still need another way for sharing stateful logic. Also, hooks only solve the problem of sharing per-instance logic, but if you want to share state between multiple instances, you still need to use stores and 3rd party state management solutions, and as I said, if you already use them, you don’t really need hooks. So instead of just fighting the symptoms, maybe it’s time for React to take an action and implement a proper state management tool for managing both global state (stores) and local state (per instance), and thus kill this loophole once and for all.

Motivation #3: Complex components become hard to understand

We’ve often had to maintain components that started out simple but grew into an unmanageable mess of stateful logic and side effects. Each lifecycle method often contains a mix of unrelated logic. […] Mutually related code that changes together gets split apart, but completely unrelated code ends up combined in a single method. […] Hooks let you split one component into smaller functions based on what pieces are related (such as setting up a subscription or fetching data), rather than forcing a split based on lifecycle methods.
If you are already using stores, this argument is almost not relevant. Let’s see why:
class Foo extends React.Component {
    componentDidMount() {
        doA(); 
        doB(); 
        doC();
    }
}
As you can see in this example, we are possibly mixing unrelated logic in
componentDidMount
, but Is it bloating our component? Not exactly. The whole implementation sits outside of the class, and the state sits in the store. without stores, all the stateful logic must be implemented inside the class, and the class would have been bloated indeed. But again, it looks like React is solving a problem that mostly exists in a world without state-management tools. In reality, most of the big apps are already using a state management tool, and this problem is already mitigated. Also, in most cases we could probably break this class to smaller components and put each 
doSomething()
 in the 
componentDidMount
 of the sub-components.
With Funclasses, we could write something like this:
function Foo() {
   useA(); 
   useB(); 
   useC();
}
It looks a bit cleaner, but is it? We still need to write 3 different 
useEffect
hooks somewhere, so at the end we are going to write more code, and look what we did here- with the class component, you can tell at first glance what the component is doing on mount. In the Funclass example, you need to follow the hooks, and try to search for a 
useEffect
 with an empty dependencies array, in order to understand what the component is doing on mount. The declarative nature of the life-cycle methods is mostly a good thing, and I found it much harder to investigate the flow of Funclasses. I have seen many cases were Funclasses made it easier for developers to write bad code, we’ll see an example later on.
But first, I must admit that there is something nice about useEffect, take a look at the following example:
useEffect(() => {
    subscribeToA();
    return () => {
      unsubscribeFromA();
    };
 }, []);
The 
useEffect
 hook lets us pair together subscribe and unsubscribe logic. This is actually a pretty neat pattern. Same goes for pairing together
componentDidMount
 and 
componentDidUpdate
. In my experience, those cases are not so common, but they are still valid use-cases, and useEffect is really helpful here. The question is- why do we have to use Funclasses in order to get useEffect? why can’t we have something similar with classes? The answer is we can:
class Foo extends React.Component {
   someEffect = effect((value1, value2) => {
     subscribeToA(value1, value2);
     return () => {
        unsubscribeFromA();
     };
   })
   render(){ 
    this.someEffect(this.props.value1, this.state.value2);
    return <Text>Hello world</Text>   
   }
}
The 
effect
 function will memoize the given function, and will call it again only if one of its params has changed. By triggering the effect from within our render function, we make sure that it’s being called on every render/update, but the given function will run again only if one of its params has been changed, so we achieve similar results of 
useEffect
 in terms of combining 
componentDidMount
 and 
componentDidUpdate
, but sadly, we still need to manually do the last cleanup in
componentWillUnmount
. Also, it is a bit ugly to call the effect function from within the render. In order to get the exact same results like useEffect, React will need to add support for it.
The bottom line is that 
useEffect
 should not be considered as valid motivation for moving into Funclasses. It is a valid motive on its own, and could be implemented for Classes too.
You can check out the implementation of the effect function here, and if you want to see it in action, check out this working example.

Motivation #4: Performance

We found that class components can encourage unintentional patterns that make these optimizations fall back to a slower path. Classes present issues for today’s tools, too. For example, classes don’t minify very well, and they make hot reloading flaky and unreliable
The React team is saying that classes are harder to optimize and minimize, and that Funclasses should somehow improve things. Well, I have only one thing to say about this- show me the numbers.
I couldn’t find any paper what so ever, or any benchmark demo app that I could clone and run, comparing the performance of Funclasses VS classes. The fact that we haven’t seen such a demo is not surprising- Funclasses need to implement this (or 
useRef
 if you prefer) somehow, so I pretty much expect that the same problems that makes classes hard to optimize, will effect Funclasses too.
Anyways, all the debate about performance is really worth nothing without showing the numbers, so we can’t really use it as an argument.

Motivation #5: Funclasses are less verbose

You can find many examples for code reduction by converting a Class to a Funclass, but most if not all of the examples take advantage of the
useEffect
hook in order to combine
componentDidMount
and
componentWillUnmount
, thus achieving great impact. But as I said earlier,
useEffect
should not be considered as a Funclass’ advantage, and if you ignore the code reduction achieved by it, you are left with a much minor impact. And if you’ll try to optimize your Funclasses using
useMemo
,
useCallback
and so on, you could even end up with a more verbose code than an equivalent class. When comparing small and trivial components, Funclasses win without a doubt, because classes have some inherent boilerplate that you need to pay no matter how small your class is. But when comparing big components, you can barely see the differences, and sometimes as I said, classes can even be cleaner..
Lastly, I have to say few words about 
useContext
: useContext is actually a huge improvement over the original context API that we currently have for classes. But again- why can’t we have this nice and clean API for classes too? why can’t we do something like this:
//inside "./someContext" :
export const someContext = React.Context({helloText: 'bla'});
//inside "Foo":
import {someContext} from './someContext';
class Foo extends React.component {
   render() {
      return (
        <View>
          <Text>{someContext.helloText} 
          </Text>
      </View>)
   }
}
When helloText is changed in the context, the component should be re-rendered in order to reflect the changes. That’s it. no need for ugly HOCs.
So why did the React team choose to improve only the useContext API and not the regular context API? I don’t know. But it doesn’t mean that Funclasses are inherently cleaner. All it means is that React should do a better job by implementing the same API improvements for classes too.
So after raising some questions about the motivations, let’s take a look at some other stuff that I don’t like about Funclasses.

The hidden side effect


One of the things that bothers me the most in the implementation of useEffect for Funclasses, is the lack of clarity about what are the side effects of a given component. With classes, if you wanted to find out what a components is doing on mount, you could easily check out the code in
componentDidMount
 or check the constructor. If you see a repeating call, you should probably check out 
componentDidUpdate
. With the new
useEffect
hook, side effects could be hidden deeply nested within the code.
Let’s say we detect some unwanted calls to the server. We look at the code of the suspected component and see this:
const renderContacts = (props) => {
  const [contacts, loadMoreContacts] = useContacts(props.contactsIds);
  return (
    <SmartContactList contacts={contacts}/>
  )
}
Nothing special here. Should we investigate SmartContactList or maybe we should dive into useContacts? Let’s dive into useContacts:
export const useContacts = (contactsIds) => {
  const {loadedContacts, loadingStatus}  = useContactsLoader();
  const {isRefreshing, handleSwipe} = useSwipeToReresh(loadingStatus);
  // ... many other useX() functions
  useEffect(() => {
    //** lots of code, all related to some animations that are relevant for loading contacts*//
  
  }, [loadingStatus]);
  
  //..rest of code
}
Ok, it’s starting to get tricky. where is the hidden side effect? If we’ll dive into useSwipeToRefresh we will see:

export const useSwipeToRefresh = (loadingStatus) => {
  // ..lot's of code
  // ...
  
  useEffect(() => {
    if(loadingStatus === 'refresing') {
       refreshContacts(); // bingo! our hidden sideEffect!
    }  
  }); //<== we forgot the dependencies array!
}
We found our hidden effect. 
refreshContacts
 will accidentally call fetch contacts on every component render. In a big codebase, and some badly structured components, nested 
useEffect
 could cause nasty trouble.
I am not saying that you can’t write bad code with classes too, but Funclasses are much more error prone, and without the strictly defined structure of the life cycle methods, it’s much easier to do bad things.

Bloated API

By adding hooks API alongside classes, React’s API is practically doubled. Everyone needs to learn two completely different methodologies now. And I must say that the new API is much more obscure than the old one. Simple things like getting the previous props and state, are becoming good interview material. Can you write a hook for getting the prev props, without the help of google?
A big library like React must be very careful with adding such huge changes in the API, and the motivation here was not even close to being justified.

Lack of declarativity

In my opinion, Funclasses tend to get much messier than classes. For example, It’s harder to find the entry point of the component- with classes you just search for the render function, but with Funclasses it can be hard to spot the main return statement. Also, It’s harder to follow the different
useEffect
statements and understand the flow of the component, as opposed to the regular life-cycle methods that give you some good hints for where you need to look for your code. If I am searching for some kind of initialization logic, I will jump (cmd + shift + o in VSCode) to
componentDidMount
. If I am looking for some kind of updating mechanism, I will probably jump to componentDidUpdate and so on. With Funclasses I find it much harder to orient inside big components.

Coupling everything to React

People start to use specific React libraries for doing simple things that are mostly made of pure logic, and could easily be un-coupled from React. Take a look at this Tracking location hook, imported from a library called react-use:
import {useLocation} from 'react-use';
const Demo = () => {
  const state = useLocation();

  return (
    <div>
      {JSON.stringify(state)}
    </div>
  );
};
But isn’t it better to just use a pure vanilla library? something like this:
import {tracker} from 'someVanillaJsTracker';

const Demo = () => {
  const [location, setLocation] = useState({});
useEffect() {
     tracker.onChange(setLocation);
  }, []);
  return (
    <div>
      {JSON.stringify(state)}
    </div>
  );
};
Is it more verbose? Yep. The first solution is definitely shorter. But the second solution is keeping the JS world un-coupled from React, and adding few more lines of code is a small price to pay for such an important thing. Custom hooks have opened a door for endless possibilities of coupling pure logic to React’s state, and those libraries are spreading like wildfire.

It just feels wrong

You know that feeling that something isn’t right? This is how I feel about Funclasses. Sometimes I can put my finger on the exact problem, but sometimes it’s just a general feeling that we are on the wrong track. When you discover a good concept, you can see how things are working together nicely. But when you are struggling with the wrong concept, it turns out that you need to add more and more specific stuff and rules in order to make thing work. With hooks, there are more and more weird things that pop out, more “useful” hooks that help you do some trivial stuff, and more things to learn. If we need so many utils for our day to day work, just for hiding away some weird complications, this is a huge sign that we are on the wrong track.
Few years ago, when I switched from Angular 1.5 to React, I was amazed by how simple the API of React was, and how thin the docs are. Angular used to have huge docs. It would have taken you days to cover everything- the digest mechanism, the different compilation phases, transclude, binding, templates and more. This alone was a huge indication for me that something is just wrong. React on the other hand, immediately felt right. It was clean and concise, you could go over the whole docs in a matter of hours and you were good to go. While trying hooks for the first time, and for the second time, and for all the following times, I’ve found myself obligated to go back to docs again and again.

An Important note

I found out that many people think that I just love classes and that it's yet another fight between OOP vs Functional programming. Well, it’s far from being true.
Classes have their disadvantages and I don't think that they are the perfect solution, but Funclasses are just worst. As I have already stated at the beginning of the post- class is a concept, not a syntax. Remember that awful prototype syntax that was achieving the same goal of classes, but in the most awkward way? So that’s what I feel about Funclasses. You don’t have to love classes in order to hate the old prototype syntax, and you don’t have to love classes in order to hate Funclasses:)
It’s not a fight between OOP and Functional programming, because Funclasses aren’t related to functional programming by any means, and strictly speaking, writing an app with React, no matter if you use Classes or not, is not exactly OOP.

Conclusion

I hate being the party pooper, but I really think that Funclasses could be the 2nd worst thing that happened to the React community (first place is still taken by Redux). It added another useless debate to an already fragile eco-system, it’s not clear now if Funclasses are the recommended way to go or is it just another feature and a matter of personal taste.
I Hope that the React community will wake up and ask for parity between the features of Funclasses and classes. We can have a better Context API in classes, and we can have something like
useEffect
for classes, and even
useState
. React should give us the choice to continue using classes if we want to, and not forcefully killing it by adding more features for Funclasses only, leaving classes behind.
BTW, back in the end of 2017, I have published a post with the title “The ugly side of Redux”, and today even Dan Abramov, the creator of Redux, already admits that Redux was a huge mistake:
Is it all just the history repeating itself? time will tell.
Anyways, me and my teammates decided to stick with classes for now, and a Mobx-based solution as a state management tool. I think that there is a big difference in the popularity of Hooks among solo developers and those who work in a team- the bad nature of Hooks is way more visible in a big codebase, where you need to handle other’s people code. Personally I really wish React could just ctrl + z hooks all together.
I am currently working on a new feature request for React that will suggest a simple, clean, built-in state-management solution for React, and will solve the problem of sharing stateful logic once and for all, hopefully with a less awkward way than Funclasses. You can check out the RFC here, please add your comments and help me make it better, and if you like it, give it a thumb up so we could bring it into focus:)

Written by niryo | Software developer @Wix.com
Published by HackerNoon on 2020/10/04