Skip to content

Instantly share code, notes, and snippets.

@jlongster
Last active May 7, 2021 17:58
Show Gist options
  • Save jlongster/febd2a397aff9501abec0c2d66075ec8 to your computer and use it in GitHub Desktop.
Save jlongster/febd2a397aff9501abec0c2d66075ec8 to your computer and use it in GitHub Desktop.

3 Gripes With React

I started using React 3.5 years ago, and I still love it. It was such a well-designed solution that not much has changed since then, only superficial stuff like naming. What I learned then is still wholly applicable today because it's such a good idea (although now you can choose from many other libraries). On top of that, we now benefit from an entirely new architecture (fiber) without changing much.

However, nothing is perfect. There are a few things I keep hitting as I work in large, complex codebases. React probably has other weak spots, but these are what I keep hitting.

Opaque references

React allows you to pass a ref property to a component to get the instance when it's created. When it's a native DOM element, like an input, you get the raw DOM element. When it's a React component, you get the React instance.

The problem is that refs are special properties and do not actually exist in the props of a component. Let's say you are wrapping an input to make a custom Input component. If you pass down the props like <input {...this.props} />, the ref is not passed down, which means when I use <Input ref={...} /> I will get a React instance, not the DOM element.

This is probably the correct default, as developers most likely don't mean to pass down ref and consumers of a component would accidentally get the wrong thing (causing very confusing bugs). However, I find myself needing to expose an innerRef property quite often that I manually pass down, allowing the consumer to bypass my wrapper and get the internal instance itself.

This is needed for Input because I want the user to use it like a native DOM element, but ref is broken for that case as they won't get an input element. Users need to use <Input innerRef={...} />.

The biggest frustration with this is when using somebody else's component that doesn't expose something like innerRef, but I need it. This is common when I need to do something special the div that it creates, but I don't have access to it.

I don't know what the solution is, but we should thinking about a way for React to help facilite this.

PureComponent, but only for props

Pure components are a tradeoff: they allow you avoid unnecessary rerenders, but at the cost of a shallow check of props and state. For fast components this shallow check may actually hurt performance.

I almost always find myself using pure components to guard against incoming prop changes only. If the state changes it should always rerender. I wonder if there's value in a PureComponent that only checks props (something like return shallowEquals(this.props, nextProps) || this.state !== nextState).

This would cut the time for shouldComponentUpdate in half on average and make it less of an issue (you can use it more freely without worrying about the overhead).

This could be something done entirely in user-land, nothing should change within React itself.

Complicated lifecycle hook componentWillReceiveProps

Probaby the most time-consuing grunt work is managing state changes in componentWillReceiveProps. There are probably better ways to manage this state, but it's easy to fall into this:

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { messages: getNewMessages(props.messages) }
  }

  componentWillReceiveProps(nextProps) {
    // Make sure to process the messages from nextProps and
    // update the local state
  }
}

Maybe I need to step back and find a better pattern for this. But even if I did, my teammates will most likely write code like this. It's the most straight-forward at first, but it's hard to maintain. In real apps, the code for deriving the state is a lot more complicated, and what do to next in componentWillReceiveProps may even depend on the state of the component itself.

For example, I have an app that renders a list of messages and automatically display the oldest unread message. When all messages are read, a "all caught up!" message is shown. When new messages come in, I automatically show the oldest new message, but only if the user is on the "all caught up!" screen. Otherwise don't change anything.

This app-specific logic is inherently complicated; but I find myself mixing in this logic inside of componentWillReceiveProps with other code concerned with specific state changes and things to avoid performance pitfalls.

Maybe we need clearer guidelines for how to structure state transitions, because in those components it doesn't feel as simple as "f(state) -> UI`. We might already have those guidelines but I haven't read them!

This is just a braindump of things I've been thinking of improving. It may not be coherent (particularly the last one), but let me know if you have any ideas. It's safe to say that if these are my only gripes with React, it's a pretty damn good library.

@jlongster
Copy link
Author

As for the above replies, it's all a spectrum for where you put your logic. Putting it outside of the component makes the consumer do more, which is appropriate for some APIs, but not others. You can also sideload data and transform it through some kind of stream (as @faceyspacey is saying) and that is appropriate sometimes too. Sometimes it's best for the logic to be inside the component itself, and all other things are much uglier and lead to more boilerplate. I use all of these ways to manage state.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment