DEV Community

Cover image for Best Practices for React Developers in 2021
Morgan for MESCIUS inc.

Posted on

Best Practices for React Developers in 2021

It may be hard to believe, but this year React turned eight years old. In the technology landscape, especially on client-side web development, this is quite remarkable. How can a simple library for building UIs be that old and still be this relevant?

The reason is, React not only revolutionized the building of UIs, but it also made functional paradigms for building UIs popular. And even then, React did not stop there. They continued to push innovative concepts forward without breaking the existing codes. As a result, React is stabler, leaner, and faster than ever.

But, the downside of React's ever-evolving nature is that best practices change over time. To harvest some of the newest performance benefits, one needs to carefully study the new additions. And figuring that out is not always easy, sometimes it's not straightforward at all.

In this article, we will take a look at the best practices that apply to React in 2021.

Conventions

To structure your work with React, it makes sense to follow a few conventions. Some conventions are even required for the tooling to work smoothly. For example, if you name your components using camelCase, then the following would not work:

const myComponent = () => <div>Hello World!</div>;

ReactDOM.render(<myComponent />, document.querySelector('#app'));
Enter fullscreen mode Exit fullscreen mode

This is because the standard JSX transformer from Babel (or TypeScript) uses the naming convention to decide whether to pass a string or an identifier to React.

As a result, the transpiled code would look as follows:

const myComponent = () => React.createElement("div", null, "Hello World!");

ReactDOM.render(React.createElement("myComponent", null), document.querySelector('#app'));
Enter fullscreen mode Exit fullscreen mode

This is not what we want. Instead, we can use PascalCase. In this case, the JSX transformer will detect the usage of a custom component and the required reference.

const MyComponent = () => <div>Hello World!</div>;

ReactDOM.render(<MyComponent />, document.querySelector('#app'));
Enter fullscreen mode Exit fullscreen mode

In this case, everything is fine:

ReactDOM.render(React.createElement(MyComponent, null), document.querySelector('#app'));
Enter fullscreen mode Exit fullscreen mode

While other conventions are less strict, they should be still followed. For instance, it makes sense to use quoted string attributes instead of JSX expressions:

// avoid
<input type={'text'} />

// better
<input type="text" />
Enter fullscreen mode Exit fullscreen mode

Likewise, it makes sense to keep the attribute quote style consistent. Most guides will propagate using single-quoted strings in JS expressions, and double-quoted strings for these React props. In the end, it doesn’t matter as long as its usage within the codebase is consistent.

Speaking of conventions and props, these should also follow the standard JS naming convention of using camelCase.

// avoid
const MyComponent = ({ is_valid, Value }) => {
  // ...
  return null;
};

// better
const MyComponent = ({ isValid, value }) => {
  // ...
  return null;
}; 
Enter fullscreen mode Exit fullscreen mode

Additionally, be sure not to misuse the names of the built-in HTML component props (for example, style or className). If using these props, forward them to the respective in-built component. Also, keep them at the original type (for example, for style a CSS style object and for className a string).

// avoid
const MyComponent = ({ style, cssStyle }) => {
  if (style === 'dark') {
    // ...
  }

  // ...
  return <div style={cssStyle}>...</div>;
};

// better
const MyComponent = ({ kind, style }) => {
  if (kind === 'dark') {
    // ...
  }

  // ...
  return <div style={style}>...</div>;
};
Enter fullscreen mode Exit fullscreen mode

This makes the intention of the props much clearer and establishes a consistency level that is critical for efficient usage of larger component collections.

Component Separation

One of React's biggest advantages is its ability to easily test and reason about components. However, this is only possible if a component is small and dedicated enough to support that.

Back when React first started gaining popularity, they introduced the concept of a controller and a view component to efficiently structure larger components. Even though today we have dedicated state containers and hooks, it still makes sense to structure and categorize components in some way.

Let's consider the simple example of loading some data:

const MyComponent = () => {
  const [data, setData] = React.useState();

  React.useEffect(() => {
    let active = true;

    fetch('...')
      .then(res => res.json())
      .then(data => active && setData(data))
      .catch(err => active && setData(err));


    return () => {
      active = false;
    };
  }, []);

  return (
    data === undefined ?
      <div>Loading ...</div> :
      data instanceof Error ?
        <div>Error!</div> :
        <div>Loaded! Do something with data...</div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Of course, a componentless action would be better suited here. But the point is that the written component has to both gather the data and display it.

A cleaner model would imply a separation that could look like this:

const MyComponent = ({ error, loading, data }) => {
  return (
    loading ?
      <div>Loading ...</div> :
      error ?
        <div>Error!</div> :
        <div>Loaded! Do something with data...</div>
  );
};

const MyLoader = () => {
  const [data, setData] = React.useState();

  React.useEffect(() => {
    let active = true;

    fetch('...')
      .then(res => res.json())
      .then(data => active && setData(data))
      .catch(err => active && setData(err));

    return () => {
      active = false;
    };
  }, []);

  const isError = data instanceof Error;

  return (
    <MyComponent
      error={isError ? data : undefined}
      loading={data === undefined}
      data={!isError ? data : undefined} />
  );
};
Enter fullscreen mode Exit fullscreen mode

To further improve it, the most ideal separation is extraction into a custom hook:

function useRemoteData() {
  const [data, setData] = React.useState();

  React.useEffect(() => {
    let active = true;

    fetch('...')
      .then(res => res.json())
      .then(data => active && setData(data))
      .catch(err => active && setData(err));

    return () => {
      active = false;
    };
  }, []);

  const isError = data instanceof Error;

  return [data === undefined, !isError ? data : undefined, isError ? data : undefined];
}

const MyComponent = () => {
  const [loading, data, error] = useRemoteData();

  return (
    loading ?
      <div>Loading ...</div> :
      error ?
        <div>Error!</div> :
        <div>Loaded! Do something with data...</div>
  );
}; 
Enter fullscreen mode Exit fullscreen mode

Hooks

React hooks are among the most debated technology features in the frontend space. When they were first introduced, they were considered elegant and innovative. On the flip side, there have been a growing number of critics over the years.

Pros and cons aside, in general, using hooks can be a best practice depending on the scenario.

Keep in mind that some hooks are there to help you with performance optimizations:

  • useMemo helps avoid doing expensive calculations on every re-render.
  • useCallback produces stable handlers, similarly to useMemo, but more conveniently geared towards callbacks.

As an example, let’s look at the following code without useMemo:

const MyComponent = ({ items, region }) => {
  const taxedItems = items.map(item => ({
      ...item,
      tax: getTax(item, region),
  }));

  return (
      <>
        {taxedItems.map(item => <li key={item.id}>
          Tax: {item.tax}
        </li>)}
      </>
  );
};
Enter fullscreen mode Exit fullscreen mode

Considering there might be a lot of items in that array, and that the getTax operation is quite expensive (no pun intended), you’d have quite a bad re-rendering time, assuming minimal items and region change.

Therefore, the code would benefit a lot from useMemo:

const MyComponent = ({ items, region }) => {
  const taxedItems = React.useMemo(() => items.map(item => ({
      ...item,
      tax: getTax(item, region),
  })), [items, region]);

  return (
      <>
        {taxedItems.map(item => <li key={item.id}>
          Tax: {item.tax}
        </li>)}
      </>
  );
}; 
Enter fullscreen mode Exit fullscreen mode

The beauty of useMemo is that it's almost invisible. As you can see, all we need to do is to wrap the computation in a function. That's it. No other changes required.

A more subtle issue is the lack of useCallback. Let's have a look at some very generic code:

const MyComponent = () => {
  const save = () => {
    // some computation
  };
  return <OtherComponent onSave={save} />;
}; 
Enter fullscreen mode Exit fullscreen mode

Now, we don't know anything about OtherComponent, but there are certain possible changes originating here, for example:

  • It’s a pure component and will prevent re-rendering, as long as all props remain untouched.
  • It uses the callback on either some memoization or effect hooks.
  • It passes the callback to some component that uses one of these properties.

Either way, passing values as props that essentially have not changed should also result in values that have not changed. The fact that we have a function declared inside our rendering function will be problematic.

An easy way out is to write the same thing using useCallback:

const MyComponent = () => {
  const save = React.useCallback(() => {
    // some computation
  }, []);
  return <OtherComponent onSave={save} />;
};
Enter fullscreen mode Exit fullscreen mode

Now, the recomputed callback is taken only if one of the dependencies given in the array changed. Otherwise, the previous callback (for instance, a stable reference) is returned.

Like before, there are almost no code changes required for this optimization. As a result, you should always wrap callbacks using useCallback.

Components

Speaking of pure components, while class components had the PureComponent abstraction, a functional pure component can be introduced to React explicitly using memo.

// no memoed component
const MyComponent = ({ isValid }) => (
  <div style=\{{ color: isValid ? 'green' : 'red' }}>
    status
  </div>
);

// memoed component
const MyComponent = React.memo(({ isValid }) => (
  <div style=\{{ color: isValid ? 'green' : 'red' }}>
    status
  </div>
));
Enter fullscreen mode Exit fullscreen mode

The React documentation is quite detailed about memo. It says: “If your component renders the same result given the same props, you can wrap it in a call to React.memo for a performance boost in some cases by memoizing the result. This means that React will skip rendering the component, and reuse the last rendered result.”

Keep in mind that — like any other comparison done by React — the props are only shallowly compared. Therefore, this optimization is only applied if we are careful what to pass in. For instance, if we use useMemo and other techniques for complex props such as arrays, objects, and functions.

You may have noticed that we exclusively used functional components. As a matter of fact, since the introduction of hooks, you can practically work without class components.

There are only two possible reasons to still use class components:

  1. You want to have access to the more sophisticated life cycle events. For example, shouldComponentUpdate.
  2. You want to introduce error boundaries.

However, even in these cases, you might just need to write one React class component to fulfill your needs. Look at this boundary:

export class Boundary extends React.Component {
  state = {
    error: undefined,
  };

  componentDidCatch(error) {
    this.setState({
      error,
    });
  }

  render() {
    const { error } = this.state;
    const { children, ShowError } = this.props;

    if (error) {
      return <ShowError error={error} />;
    }

    return children;
  }
}
Enter fullscreen mode Exit fullscreen mode

Not only will the component catch any errors which may appear in its children, but it will also display a fallback component passed in as ShowError receiving a single prop: the error.

Operators

Some operators can be used to simplify the tree construction in React. For instance, the ternary operator allows us to write code that looks like this:

<div>
  {currentUser ? <strong>{currentUser}</strong> : <span>Not logged in</span>}
</div> 
Enter fullscreen mode Exit fullscreen mode

Boolean operators such as && and || may also be useful, but there are a few traps to watch out for. As an example, look at this code snippet:

<div>
  {numUsers && <i>There are {numUsers} users logged in.</i>}
</div>
Enter fullscreen mode Exit fullscreen mode

Assuming that numUsers is always a number between 0 and the total number of users, we'd end up with the expected output if numUsers is positive.

<div>
  <i>There are 5 users logged in.</i>
</div>
Enter fullscreen mode Exit fullscreen mode

However, for the edge case of zero users, we'd get this:

<div>
  0
</div>
Enter fullscreen mode Exit fullscreen mode

Which may not be what we wanted, so a boolean conversion or more explicit comparison could help here. In general, the following is more readable:

<div>
  {numUsers > 0 && <i>There are {numUsers} users logged in.</i>}
</div> 
Enter fullscreen mode Exit fullscreen mode

Now, in the zero users edge case scenario we get:

<div>
</div>
Enter fullscreen mode Exit fullscreen mode

Using the ternary operator as an exclusive boolean operator avoids the issue completely. But what about a state where we don't want to render anything? We could either use false or an empty fragment:

<div>
  {numUsers ? <i>There are {numUsers} users logged in.</i> : <></>}
</div> 
Enter fullscreen mode Exit fullscreen mode

The empty fragment has the advantage of giving us the ability to just add content later. However, for users less familiar with React, it could look a bit strange.

Conclusion

In this article, we went over some of the best practices that make your React codebase easier to work with. By switching over from class components to functional components, you can dive more into hooks. This will provide the ability to automatically introduce a great separation of concerns, where the behavioral aspects are all done in functions and rendering is defined within components.

By following a set of useful conventions, together with some techniques such as the use of the right operators, hooks, and separation of concerns, you should end up with a clean codebase that can be maintained and extended quite easily.

Top comments (6)

Collapse
 
techjobinsight profile image
Tech Job Insight

Rather than kind === "dark" I would replace it with variant === "dark" imo

Collapse
 
tautvydaspetrauskas profile image
Tautvydas

Also you could use !!numUsers && so it will be falsy for 0.

Collapse
 
olsard profile image
olsard

Great! Thanks for sharing.

Collapse
 
eyalshalev profile image
Eyal Shalev

In your component separation example, I would have used an AbortController instead of the active flag.
That way you can abort the fetch if the component is unmounted.

Collapse
 
potcode profile image
Potpot

About the state management of data fetching, you can just opt-in the react-query

Collapse
 
ayabouchiha profile image
Aya Bouchiha

Great article !