Back

6 React Best Practices For 2021

6 React Best Practices For 2021

Writing clean and readable code is essential to improve your code’s quality. On top of that, clean code is easier to test. There’s no reason not to spend five extra minutes refactoring your code to make it more readable.

This article looks at six React best practices for 2021 to improve your code. We’ll cover the following items:

  1. Make use of event.target.name for event handlers
  2. How to avoid manually binding event handlers to this?
  3. Make use of React hooks to update your state
  4. Cache expensive operations with useMemo
  5. Decouple functions into functional functions to improve code quality
  6. How do you create custom hooks in React?

#1: Use event handler name

When you have a form with a single input field, you’ll write one onFirstInputChange function to capture the contents of your input field.

However, do you write ten event handlers when you have a form with ten input fields? The answer is no.

We can set the name property on an input field and access it from the event handler. This value allows us to use a single input handler for onChange events.

Here’s your current situation when using a not-optimized form with two input fields. We have to define an onChange event handler for each individual form input element. This pattern creates a lot of duplicate code, which is hard to maintain.

export default class App extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            item1: "",
            item2: "",
            items: "",
            errorMsg: ""
        };
        this.onFirstInputChange = this.onFirstInputChange.bind(this);
        this.onSecondInputChange = this.onSecondInputChange.bind(this);
    }
    onFirstInputChange(event) {
        const value = event.target.value;
        this.setState({
            item1: value
        });
    }
    onSecondInputChange(event) {
        const value = event.target.value;
        this.setState({
            item2: value
        });
    }
    render() {
        return (
            <div>
                <div className="input-section">
                    {this.state.errorMsg && (
                        <p className="error-msg">{this.state.errorMsg}</p>
                    )}
                    <input
                        type="text"
                        name="item1"
                        placeholder="Enter text"
                        value={this.state.item1}
                        onChange={this.onFirstInputChange}
                    />
                    <input
                        type="text"
                        name="item2"
                        placeholder="Enter more text"
                        value={this.state.item2}
                        onChange={this.onSecondInputChange}
                    />
                </div>
            </div>
      );
    }
}

As you can see, each event handler function updates the state. When dealing with multiple form elements, your code becomes very messy as you have to write a new function for every event.

Let’s tackle this situation differently using the name field. We can access this value via the event.target.name property. Now, we can create a single function that can handle both events at once. Therefore, we can remove both functions onFirstInputChange and onSecondInputChange.

onInputChange = (event) => {
  const name = event.target.name;
  const value = event.target.value;
  this.setState({
    [name]: value
  });
};

Easy, right? Of course, you often need additional validation for the data you save to your state. You can make use of a switch statement to add custom validation rules for each submitted value.

#2: Avoid manually binding this

You will most likely know that React doesn’t retain the this binding when attaching an event handler to an onClick or onChange event. Therefore, we need to bind this manually. Why do we bind *this*? We want to bind the this of the event handler to the component’ instance, so we don’t lose its context when we pass it as a callback.

Here’s a classic “binding” example that happens in the constructor.

class Button extends Component {
  constructor(props) {
    super(props);
    this.state = { clicked: false };
    this.handleClick = this.handleClick.bind(this);
  }
  
  handleClick() {
    this.props.setState({ clicked: true });
  }
  
  render() {
    return <button onClick={this.handleClick}>Click me!</button>;
  );
}

Yet, binding is not necessary anymore since the create-react-app CLI command makes use of @babel/babel-plugin-transform-class-properties plugin version >= 7 and babel/plugin-proposal-class-properties plugin version < 7.

Note: You have to change the event handler syntax to arrow function syntax.

Below you find an example of the arrow function syntax. Here, we don’t need to write additional code in the constructor to bind this.

class Button extends Component {
  constructor(props) {
    super(props);
    this.state = { clicked: false };
  }
  
  handleClick = () => this.setState({clicked: true });
  render() {
    return <button onClick={this.handleClick}>Click me!</button>;
  );
}

That’s as simple it can get! You don’t need to worry about binding functions in your constructor.

#3: Use React hooks to update your state

Since React version 16.8.0, it’s now possible to use state and lifecycle methods inside functional components using React Hooks. In other words, we can write better readable code that’s also much easier to maintain.

For this, we’ll be using the useState hook. For those that are not aware of what a hook is and why you would use it. Here’s a short definition from the React documentation.

What is a Hook? A Hook is a special function that lets you “hook into” React features. For example, useState is a Hook that lets you add React state to function components.

When would I use a Hook? If you write a function component and realize you need to add some state to it, previously you had to convert it to a class. Now you can use a Hook inside the existing function component.

First, let’s take a look at how we update the state using the setState hook.

this.setState({
    errorMsg: "",
    items: [item1, item2]
});

Now, let’s make use of the useState hook. We need to import this hook from the react library. We can now declare new state variables and pass them an initial value. We’ll use destructuring to get a variable for retrieving the value and one for setting the value (this one is a function). Let’s take a look at how we can do this for the above example.

import React, { useState } from "react";

const App = () => {
  const [items, setIems] = useState([]);
  const [errorMsg, setErrorMsg] = useState("");
};

export default App;

Now, you can directly access both constants items and errorMsg in your component.

Further, we can update the state inside a function like this:

import React, { useState } from "react";

const App = () => {
  const [items, setIems] = useState([]);
  const [errorMsg, setErrorMsg] = useState("");

  return (
    <form>
        <button onClick={() => setItems(["item A", "item B"])}>
          Set items
        </button>
    </form>
  );
};

export default App;

That’s how you can use state hooks.

#4: Cache expensive operations with useMemo

Memoization is an optimization technique to store the result of expensive operations. In other words, a sorting operation is often an expensive operation that requires a lot of processing. We don’t want to execute this expensive function for every page render.

Therefore, we can use the useMemo hook to remember the output when you pass the same parameters to the memoized function. The useMemo hook accepts a function and the input parameters to remember. React refers to this as a dependencies array. Every value referenced inside the function should also appear in your dependencies array.

Here’s a simple, abstract example. We pass two parameters a and b to an expensive function. As the function uses both parameters, we’ve to add them to the dependencies array for our useMemo hook.

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

#5: Decouple functions into pure functions to improve code quality

As a general React best practice, you should decouple functions that don’t rely on your component. In other words, a function that doesn’t rely on any state or React hooks.

Therefore, a sorting function is an excellent example of a function that you can extract as a pure function.

You might wonder why you should embrace functional programming?

Functional programming is the process of building software by composing pure functions, avoiding shared state, mutable data, and side-effects. Pure function are better readable and easier to test. Therefore, they improve code quality. - freeCodeCamp

Now, let’s apply this concept to React components. Here’s a function that can sit both inside a React component or outside. Both are comparison functions, which you can pass to an expensive sorting function, that accept two input parameters. As there’s no interaction with the state, we can extract both functions to pure functions. This allows us to place the pure functions in a separate file and import them in multiple locations if needed.

function ascSort (a, b) {
  return a < b ? -1 : (b > a ? 1 : 0);
}

function descSort (a, b) {
  return b < a ? -1 : (a > b ? 1 : 0);
}

#6: Create custom React hooks

We’ve learned how to use the useState and useMemo React hooks. Yet, React allows you to define your own React hooks to extract logic and make components more readable.

We can specify custom React hooks starting with the use keyword, like all other React hooks. It’s beneficial when you want to share logic between different functions. Instead of copying the function, we can define the logic as a React hook and reuse it in other functions.

Here’s an example of a React component that updates the state when the screen size gets reduced to below 600 pixels. If this happens, the isScreenSmall variable is set to true. Otherwise, the variable is set to false. We use the resize event from the window object to detect screen size changes.

const LayoutComponent = () => {
  const [onSmallScreen, setOnSmallScreen] = useState(false);

  useEffect(() => {
    checkScreenSize();
    window.addEventListener("resize", checkScreenSize);
  }, []);

  let checkScreenSize = () => {
    setOnSmallScreen(window.innerWidth < 768);
  };

  return (
    <div className={`${onSmallScreen ? "small" : "large"}`}>
      <h1>Hello World!</h1>
    </div>
  );
};

Now, we want to use the same logic in another component that relies on this isScreenSmall variable. Instead of duplicating the code, let’s transform this example into a custom React hook.

import { useState, useEffect } from "react";

const useSize = () => {
  const [isScreenSmall, setIsScreenSmall] = useState(false);

  let checkScreenSize = () => {
    setIsScreenSmall(window.innerWidth < 600);
  };
  useEffect(() => {
    checkScreenSize();
    window.addEventListener("resize", checkScreenSize);

    return () => window.removeEventListener("resize", checkScreenSize);
  }, []);

  return isScreenSmall;
};

export default useSize;

We can create a custom React hook by wrapping the logic in a use function. In this example, we named the custom React hook useSize. Now we can import the useSize hook anywhere we require it.

React custom hooks are nothing new. You wrap the logic with a function and provide it a name that starts with use. It acts like a normal function. However, by following the starts with “use” convention, you´re telling anyone who´s importing it, that it is a hook. Furthermore, because it is a hook, you should structure it in a way that follows the rules of hooks.

Here’s how our component looks like now. The code becomes much cleaner!

import React from 'react'
import useSize from './useSize.js'

const LayoutComponent = () => {
  const onSmallScreen = useSize();

  return (
    <div className={`${onSmallScreen ? "small" : "large"}`}>
      <h1>Hello World!</h1>
    </div>
  );
}

BONUS TIP: Open Source Session Replay

OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.

replayer.png

Start enjoying your debugging experience - start using OpenReplay for free.

That’s it! This article has taught you six techniques to improve code readability and code quality.