React class components pitfalls

React class components pitfalls

Up until a few months ago class components were cool, but the arrival of React Hooks put an end to that. Why are class components not a great thing anymore? In this post we are going to enumerate the main pitfalls when working with class components.

The "this" nightmare

In a class component the value of this inside a function depends upon how that function is invoked (when the event handler occurs and the handler is invoked, the this value fallsback to default binding and is set to to undefined, as class declarations and prototype methods run in strict mode).

More on this: https://medium.freecodecamp.org/this-is-why-we-need-to-bind-event-handlers-in-class-components-in-react-f7ea1a6f93eb

This means that event handlers that are triggered outside the class component it self will loose the this reference to the class were they belong.

If we try to run this sample, when clicking on the the Change Text button we will get an error:

Cannot read property 'setState' of undefined

export class MyHelloComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { myText: "hello" };
  }

  onChangeText() {        
    this.setState({ myText: "world" });
  }

  render() {
    return (
      <>
        <h3>{this.state.myText}</h3>
        <button onClick={this.onChangeText}>Change text</button>
      </>
    );
  }
}

For every single event handler function, we have to remember to bind this to the class component (you can use explicit hard binding or experimental class method fat arrow).

export class MyHelloComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { myText: "hello" };
+    this.onChangeText = this.onChangeText.bind(this);
  }

  onChangeText() {        
    this.setState({ myText: "world" });
  }

  render() {
    return (
      <>
        <h3>{this.state.myText}</h3>
        <button onClick={this.onChangeText}>Change text</button>
      </>
    );
  }
}

Live demo: https://codesandbox.io/s/71opm61rzq

This is a source of bugs, how many times have you forgetten to properly bind your event handler and bummer getting runtime error?

Monolyth state plus hard to extract funcionallity

When defining state in a class component we have a single place to store everything, and on the other hand a single way to update the state.

What's the issue here? Is hard to separate concerns, it's hard to extract functionallity that could be reused.

In this case we have two unrelated topics living in the same class based state:

export class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { color: "teal", name: "John", lastname: "Doe" };
  }

  getFullname() {
    return `${this.state.name} ${this.state.lastname}`;
  }

  render() {
      return (
          <div style={{ background: this.state.color }}>
              <h3>{this.getFullname()}</h3>
          </div>
      )
  }
}

Live demo: https://codesandbox.io/s/yp9v4yyr8x

Let's check how could we handle this using hooks, we could get two separate state instances for each concern:

import React from "react";

export const MyComponent = () => {
  const [color, setColor] = React.useState("teal");
  const [clientInfo, setClientInfo] = React.useState({name: 'John', lastname: 'Doe'});

  const getFullname = () => {
      return `${clientInfo.name} ${clientInfo.lastname}`;
  }

  return (
    <div style={{ background: color }}>
      <h3>{getFullname()}</h3>
    </div>
  );
};

Live demo: https://codesandbox.io/s/ppom543mj7

We could even go one step further and encapsulate the user info handling into a custom hook:

const useClientInfo = (name, lastname) =>  {
  const [clientInfo, setClientInfo] = React.useState({
    name,
    lastname,
  });

  const getFullname = () => {
    return `${clientInfo.name} ${clientInfo.lastname}`;
  };

  return {clientInfo, setClientInfo, getFullname}
}

export const MyComponent = () => {
  const [color, setColor] = React.useState("teal");
  const {getFullname} = useClientInfo('John', 'Doe');

  return (
    <div style={{ background: color }}>
      <h3>{getFullname()}</h3>
    </div>
  );
};

Live demo: https://codesandbox.io/s/ly7wlq9mo9

Managing related concerns in separate event handlers

When we use React class components, we got several events in the lifecycle (componentDidMount, ComponentDidUpdate, ComponentWillUnMount...),these events may handle related functionality, but we are forced to split that functionallity into separate methods. For instance:

Let's define a parent component:

export class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { visible: false };
  }

  render() {
    return (
      <>
        {this.state.visible && <MyChildComponent />}
        <button onClick={() => this.setState({ visible: !this.state.visible })}>
          Toggle Child component visibility
        </button>
      </>
    );
  }
}
  • Let's define a ChildComponent that will just display userInfo
export class MyChildComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { name: "John", lastname: "Doe" };
  }

  componentDidMount() {
    console.log("Hey Im mounting");
    console.log(`${this.state.name} ${this.state.lastname}`);
  }

  componentDidUpdate() {
    console.log("Just updating...");
    console.log(`${this.state.name} ${this.state.lastname}`);
  }

  componentWillUnmount() {
    console.log("bye bye, unmounting...");
  }

  render() {
    return (
      <div>
        <h3>
          {this.state.name} {this.state.lastname}
        </h3>
        <input
          value={this.state.name}
          onChange={e => this.setState({ name: e.target.value })}
        />
        <input
          value={this.state.lastname}
          onChange={e => this.setState({ lastname: e.target.value })}
        />
      </div>
    );
  }
}

Live Demo: https://codesandbox.io/s/oqyo3159jq

By using hooks we can group all this functionality in a single function:

const MyChildComponent = () => {
  const [userInfo, setUserInfo] = React.useState({name: 'John', lastname: 'Doe'})

  React.useEffect(() => {
    console.log('called when the component is mounted and right after it gets updated');

    return () => console.log('Clean up from the previous render before running effect next time ... ');
  })

  return (
    <div>
      <h3>
        {userInfo.name} {userInfo.lastname}
      </h3>
      <input
        value={userInfo.name}
        onChange={e => setUserInfo({ ...userInfo, name: e.target.value })}
      />
      <input
        value={userInfo.lastname}
        onChange={e => setUserInfo({ ...userInfo, lastname: e.target.value })}
      />
    </div>
  );
}

In this example the code defined under the useEffect is executed just when the component is mounted,and right after every render. On the other hand the clean up function is executed right after the component is mounted and right after each time the effect is executed.

Live Demo: https://codesandbox.io/s/5zllr3k09p

Uh? fine, but... what if I just want to execute a piece of code only when the component is just mounted and clean up when the component is unmounted We can play with React.UseEffect second parameter.

const MyChildComponent = () => {
  const [userInfo, setUserInfo] = React.useState({name: 'John', lastname: 'Doe'})

  React.useEffect(() => {
-    console.log('called just when the component is mounted and when after it gets updated');
+    console.log('called just when the component is mounted');

-    return () => console.log('Clean up from the previous render before running effect next time ... ');
+    return () => console.log('Clean up executed just when the component gets unmounted ... ');
-  })
+  }, [])

  return (
    <div>
      <h3>
        {userInfo.name} {userInfo.lastname}
      </h3>
      <input
        value={userInfo.name}
        onChange={e => setUserInfo({ ...userInfo, name: e.target.value })}
      />
      <input
        value={userInfo.lastname}
        onChange={e => setUserInfo({ ...userInfo, lastname: e.target.value })}
      />
    </div>
  );
}

Live Demo: https://codesandbox.io/s/q91qjql8r4

High Order components usage noise

High Order Components are a great way to teach new tricks to components via composition, but:

  • You need to refactor your target component signature in order to use them.
  • Chaining several high order components is a pain in the neck.

Let's take a look at a minimal example: the following component will just greet the current user logged in:

import React from "react";

export const MyComponent = (props) => {
  return (
    <>
      <h3>Hello: </h3>
    </>
  )
}

Now if we want to inject the user logged in we can create a HoC:

const withUserInfo = (ComponentToWrap) => (props) =>
  <>
    <ComponentToWrap {...props} user="John" />
  </>

Let's make usage of this HoC in MyComponent

- export const MyComponent = (props) => {
+ const MyComponentInner = (props) => {  
  return (
    <>
-      <h3>Hello: </h3>
+      <h3>Hello: {props.user}</h3>
    </>
  )
}

+ export const MyComponent = withUserInfo(MyComponentInner)

Live demo: https://codesandbox.io/s/l7qznjjyw9

That's pretty powerful, but wouldn't it be easier just to say... I want to use this functionality? Let's find out how to implement this behavior using hooks:

import React from "react";

const useUserInfo = () => {
  const [userInfo, setUserInfo] = React.useState('John');

  return {userInfo, setUserInfo}
}

export const MyComponent = (props) => {
  const {userInfo} = useUserInfo();

  return (
    <>
      <h3>Hello: {userInfo}</h3>
    </>
  )
}

Live code demo: https://codesandbox.io/s/pwj6z446xq

Advantages:

  • No need to rewrite our component signature (remove export, create a wrapper component...).
  • No mess wrapping nested HoC.
  • Clear contract, no need to guess what properties are being passed to my component by the HoC.

Swap from functional to class components and the other way around

When you start implementing a component, you probably want to keep it simple, and start implementing a functional component, let's get something easy, display a Hello message receiving from the parent component a userName property:

import React from "react";

export const MyComponent = () => {
  return (
    <>
      <MyChildComponent userName="John"/>
    </>
  )
}

export const MyChildComponent = (props) => {
  return (
    <>
      <h3>Hello: {props.userName}</h3>      
    </>
  )
}

Live Demo: https://codesandbox.io/s/985wyr1olw

Now let's say that instead of getting this userName property from a parent component, we want to store it locally in our component state. A classic approach would involve refactoring the component to a class component base and totally revamping it (be careful not to mess it up with this.state, adding a constructor, a render method...), something like this:

export const MyComponent = () => {
  return (
    <>
-       <MyChildComponent userName="John"/>
+       <MyChildComponent/>
    </>
  )
}

- export const MyChildComponent = (props) => {
+ export class MyChildComponent extends React.Component {  
+   constructor(props) {
+     super(props);
+     this.state = {userName: 'John'}
+   } 
+ 
+   render() {
      return (
        <>
-          <h3>Hello: {props.userName}</h3>      
+          <h3>Hello: {this.state.userName}</h3>      
        </>
      )
+   }
+ }

Live Demo: https://codesandbox.io/s/40l5k4q1o9

How could we make this refactor using hooks?

import React from "react";

export const MyComponent = () => {
  return (
    <>
-      <MyChildComponent userName="John"/>
+      <MyChildComponent/>
    </>
  )
}

export const MyChildComponent = (props) => {
+ const [userName]  = React.UseState('John');
  return (
    <>
-      <h3>Hello: {props.userName}</h3>      
+      <h3>Hello: {userName}</h3>      
    </>
  )
}

Live Demo: https://codesandbox.io/s/jlkoxm6plw

Mixed approach functional vs classes

Last but not least, in a larger project having to deal with a mix of function and class based components, it brings along several maintainability issues:

  • The code base is not consistent.
  • Is harder for new developers to ramp up into the project, they have to learn both approaches, know when to apply one way or the other, or when and how to refactor from class to function (plus the other way around).
  • Refactoring from function to class component or vice versa is an error-prone process (e.g.: forget to remove the this statement, messing up refactoring render function, class vs function sintax...).

Wrapping up

When new stuff gets into town, before jumping on the bandwagon, it's good practice to learn why we need to use all this cool aid and what problems it solves. I hope that this post helps you understand why hooks have generated so much hype in the community and have become a de facto standard. In next posts of these series we will start digging into hooks details, showing both their happy path plus edge cases based on real project usage, so stay tuned!

Hope you enjoyed this article, thanks for reading.

About basefactor

We are a team of Javascript experts. If you need coaching or consultancy services, don't hesitate to contact us.

Doers/

Location/

C/ Pintor Martínez Cubells 5 Málaga (Spain)

General enquiries/

info@lemoncode.net

+34 693 84 24 54

Copyright 2018 Basefactor. All Rights Reserved.