Where React Fails

Some people like dogs, some people like cats. Some people like React, some people like Angular.

When asked why people prefer Angular, they say that it's better structured and it's nice to have a full package. When asking the same about React, people say it's more open, more flexible and simpler.

At first thought I would say they are right. But then I kept looking at that final word: simpler. Is React in fact simpler? I'm going to dive in, but let's get this out of the way first:

Apples and Oranges

Yes, I am going to compare React to Angular here and there. Yes, yes, you cannot directly compare React and Angular. They don't have the same scope, and so on. All of that is true and has been well established. I'm not trying to fully compare the two here. All I want to do is highlight some painful areas and explain why some people don't think React is simple at all.

Why React is Not Simple

Event Handling

I'm going to start with a classic:

One of the first frustrations people seem to have with React is how the this works with event handling. Consider the following piece of code taken from the official documentation:

class Toggle extends React.Component {
  constructor(props) {
    super(props);
    this.state = {isToggleOn: true};

    // This binding is necessary to make `this` work in the callback
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.setState(state => ({
      isToggleOn: !state.isToggleOn
    }));
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        {this.state.isToggleOn ? 'ON' : 'OFF'}
      </button>
    );
  }
}

ReactDOM.render(
  <Toggle />,
  document.getElementById('root')
);
}

You have to use something like Function.prototype.bind or arrow functions to make sure it works correctly. The reason is clear: Synthetic Events. But is annoying and easy to forget. Couldn't React just add a Function.prototype.apply() somewhere in the code base?

That's just JavaScript, dude.

You could make a case that something similar happens with native event handling. And it is clearly pointed out in the documentation. But I've never had to worry about that in Angular. Granted, using Function Components in combination with Hooks solves most of these problems. So, we might let this one slide.

But there is more, let's take a look at this:

function onClick(event) {
  console.log(event); // => nullified object.
  console.log(event.type); // => "click"
  const eventType = event.type; // => "click"

  setTimeout(function() {
    console.log(event.type); // => null
    console.log(eventType); // => "click"
  }, 0);

  // Won't work. this.state.clickEvent will only contain null values.
  this.setState({clickEvent: event});

  // You can still export event properties.
  this.setState({eventType: event.type});
}

Since the event object is pooled, you cannot access it in an asynchronous way. It just returns null. How weird is that? Especially in a such an asynchronous environment. It's a sacrifice React decided to make to improve performance.

Hidden State Rules

Both React and Angular need to be informed when state has been modified to potentially re-render components. Angular uses a system behind the scene that deeply compares objects whenever something asynchronous happens. In React, you have to explicitly call setState() or use the State Hook. I've got no problem with that. Coming from knockout, it's clear that this is a very efficient mechanism.

However, I don't really like this:

Because this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state.

This is wrong:

this.setState({
  counter: this.state.counter + this.props.increment,
});

And this is right:

this.setState((state, props) => ({
  counter: state.counter + props.increment
}));

And you just need to know that. You can read why here. Only at this point in time, I realized why the React team created the overload that takes a function in the first place. But once again, this is solved with Hooks, right?

Hooks Solve Everything?

Don't get me wrong I like Hooks. But when you get started with it you quickly bump into this: Rules of hooks. It tells us to call hooks only at the top level, because hooks need to be called in the exact same order all the time.

The first thing that came to mind was: "Isn't that extremely fragile?". I mean, I would never design a system that way. You could simply give a unique name to every hook, so you know which one you're calling? It would result into slightly longer code, but is that not worth it?

Ok, it is properly documented. And there even is an ESLint plugin. But still, ...

While Function Components with Hooks solve a lot of problems, it also introduces new ones. The most notorious one is known as the closure hell. Consider the following piece of code:

const MyComponent = (props) => {
  const handleClick = () => {
    //do stuff
  });

  return(
    <ChildComponent onClick={handleClick} />
  )
}

Since the component is the render function. Everything inside the component is re-created on re-render. In this example, that means two things:

  1. The handleClick function is created on every render
  2. The ChildComponent will receive a new value for props.onClick leading to a re-render of that child component.

You can see how this would result in even more re-rendering and more unnecessary creation of temporary variables and functions.

You can easily solve it by using the Callback hook:

const MyComponent = (props) => {
  const handleClick = useCallback(()=>{
    //do stuff
  }, []);

  return(
    <ChildComponent onClick={handleClick} />
  )
}

This will make sure the handleClick function is memoized, so the ChildComponent doesn't receive new props every time. But it doesn't solve the fact that the function is re-created on every render.

But we did that before with inline functions in JSX?

I know! I was not a big fan of already and now we do it even more. React will keep your garbage collector busy, I can tell you that!

So, how about this?

const handleClick = () => {
  //do stuff
});

const MyComponent = (props) => {
  return(
    <ChildComponent onClick={handleClick} />
  )
}

By defining the callback function outside the Component, I solved both problems. However, usability is very limited, because I don't have access to the state of the component since that relies on closures as well. And even if I could access it, how would I know that I have the latest version of the state? Closures can easily lead to working with "stale" versions of the captured variable. So, we are kind of stuck to re-creating everything on each render.

Ow, yeah, and when you use state, don't forget to inform useCallback about it.

const [count, setCount] = useState(1);

const handleClick = useCallback(
  () => {
    setCount(count + 1)
  },
  [count]);

With every change of 'count', you will get a new handleClick function, with a freshly captured value for 'count'. Even if your child component is not clicked, and hence, does not use this function, it will still be re-rendered.

Here is another one about hooks:

You can write this:

useEffect(() => {
    fetch('http://localhost:3001/cars')
    .then(resp => resp.json())
    .then(result => setCars(result))
}, []);

But you can't write this:

useEffect(async () => {
    const resp = await fetch('http://localhost:3001/cars');
    const result = await resp.json();
    setCars(result);
}, []);

Wait isn't that the same code?

Apparently not:

Warning: useEffect function must return a cleanup function or nothing. Promises and useEffect(async () => ...) are not supported, but you can call an async function inside an effect.

I get it, the first doesn't return anything, the second one does. But should you not expect that people want to use async and await in an effect hook? Can React not simply check the type and distinguish between a cleanup function and an AsyncFunction object?

Once again, there is a nice warning, and it even tells you how to solve it. But yet another thing to remember.

CSS is Not Componentized

This is probably my biggest concern. While React claims to be a library where you separate concerns, you can't even create CSS that only applies to one component. It will bleed out to any child components and there is nothing you can do about it. Yes, you can create unique classes, and the direct child (>) selector does help. But surely, nobody is happy with this situation.

Refs and Children

First of all, refs are problematic because they cannot reference a function component. While there are ways around this, you now have to know how your child components are implemented. Doesn't that violate the whole encapsulation principle?

Second, how complicated are refs and children? In Angular, the closest thing is a ViewChild and a ContentChild. You get a reference, and then you have access to anything that is public, change detection takes care of re-rendering and you're good to go. In React, children are read-only. Which results in having to use render props or tricks like React.cloneElement. render props is a clever pattern, but you shouldn't need a pattern like that.

A Few Other Things

I like the Redux pattern and definitely see the upside. However, it may be off putting for newcomers and smaller projects. But hey, Angular needs Redux too right? Well, it does make sense at one point. But I feel like React needs it a lot quicker than Angular. In Angular you have services and dependency injection. Plus, the change detection mechanism doesn't force you to keep state in a component.

I also see a lot of patterns in React (custom hooks, context API, HOCs, ...) that are put in place to share state and behavior across components when props won't cut it. Angular also has those areas where Inputs and Outputs won't cut it. But everything is immediately solved by injecting a service, instead of relying on various specific patterns and techniques.

Conclusion

When I put everything together it seems that React fails in meet two areas:

1. It Fails at Encapsulation

The weird rules, the bleeding CSS, the excessive need of patterns, ... You need to know the framework in and out to build a proper application. While writing your code, you always need to think about how the React library will respond to your it. I constantly feel I have to write my code a certain way because the React team decided to implement a feature in a certain way. Libraries are supposed to hide implementation details, not constantly push them to the surface. It's strange that the documentation constantly brings up the importance of encapsulation while the library itself keeps violating the principle.

2. It Provides Half Solutions

With every new feature, React provides something that almost solves your problem. While, at the same time, overlapping with some of the old solutions. This makes React a confusing place to start.

Also, the official documentation is not that great. For example, I don't like how Hooks is just separated from the rest of the documentation. It's like: "Surprise, here is the thing you should be using now" while the rest of documentation doesn't reflect that at all.

/reactfails/rage-react.png

Agree? Disagree? What is your biggest frustration with React? Let me know in the comments below.