Reducing Bugs in React Codebase by understanding anti-patterns by@darshitac

Reducing Bugs in React Codebase by understanding anti-patterns

June 15th 2022 1,760 reads
Read on Terminal Reader
Open TLDR
react to story with heart
react to story with light
react to story with boat
react to story with money
In this article, we discussed that using props or context as initial state and 'Destroy and Recreate' are anti-patterns while using internal state in JSX and props as a dependency in useMemo are good patterns. We also learned that we should be cautious when we are using hooks without a dependency array and nesting for arranging React components.
image
Darshita Chaturvedi HackerNoon profile picture

Darshita Chaturvedi

Co-Founder & CEO, Atri Labs — Creating a new full-stack web development framework. Prev: MIT, IIT, BlackRock


Recently, while working with our large React app codebase at Atri Labs, we came to a screeching halt against three categories of bugs — they were not compile-time or run-time errors but unexpected code behaviors.


  • A component does not update upon a user event.
  • A component updates partially upon a user event.
  • A component renders unexpectedly.


Our first instinct was to “battle evil wherever we could find it”.


Source: imgflip.com

Source: imgflip.com


However, even after a litany of print statements, the bugs remained hard to trace. It is then that we realized that certain parts of our code could be considered anti-patterns. So we spent a lot of time understanding and characterizing them as such to ensure that we avoid these mistakes in the future. This article is an attempt to explain those discoveries.

Patterns and Anti-Patterns in React

In this article, a React code qualifies as a good pattern if:

  • The component is reusable.

  • The code is easier to review and debug.


Note that the code is still considered a pattern if we wrote more lines of code or we (expectedly) introduced a few extra renders in order to achieve the above objectives.


Source: giphy.com

Source: giphy.com


Why do even experienced developers fall into the trap of anti-patterns?

  1. The React code looks surprisingly similar when it follows a pattern vs. the time when it follows an anti-pattern.
  2. The pattern seems so obvious that it gets ignored.

How to identify anti-patterns?

Hint #1: Hook without a dependency array


In React, different pieces of code are linked to each other by dependencies. These pieces of interdependent code together keep the application state in its desired form. Therefore, if we write a piece of code in React that does not have a dependency, there is a high chance that it will result in bugs.


Hence, be cautious when you are using hooks such as useState, useRef etc., because they do not take dependencies arrays.


Source: imgflip.com

Source: imgflip.com


Hint #2: Nesting over the composition


There are two mechanisms using which React components are arranged:

  1. Composition: All children have the same data
  2. Nesting: Each child can have different data


Image by author

Image by author


Let us imagine a scenario where we observe that there is a bug in “Child 3”.


If we had arranged the components using composition, we would not have to look into the code of “Child 1” and “Child 2” because all are independent. Hence, the time complexity of debugging would be O (1).


However, if we had arranged the components using nesting, we would have to check all the children before “Child 3” to figure out the source of the bug. In this case, the time complexity of debugging would be O (n) where n is the number of children above “Child 3”.


Hence, we can conclude that nesting often makes the debugging process harder than composition does.

Example App

Now let us consider an app to demonstrate different patterns and anti-patterns.


The desired behavior of the app


When an article is clicked in the left navigation menu, it opens on the right. This is followed by two actions:


  1. Computation: The total character count of the article is calculated as (num_chars(title) + num_chars(text) and displayed.
  2. Network request: Based on the total character count of the article, an emoji is fetched via a network request and displayed. As the character count increases, the emoji changes from sad to happy.



Building the app


We will get to the correct way of building this app in four steps:


  • Incorrect: The app does not work as expected — neither computation nor network request is triggered when a new article is selected.
  • Partially correct: The app works as expected but a flickering effect is observed when a new article is selected.
  • Correct but suboptimal: The app works as expected without DOM flickers but makes unnecessary network requests.
  • Correct and optimal: The app works as expected without DOM flickers and unnecessary network requests.


Below is a link to the sandbox of this app. Review each approach by clicking on the corresponding option in the top navigation bar. Check how the app performs when an article is clicked in the left navigation menu.



Code structure


You can open the above sandbox by clicking on the button “Open Sandbox” at the bottom right corner.


The src/pages directory has pages mapped to each step. The file for each page in src/pages contains a ArticleContent component. The code under discussion is inside this ArticleContent component. To follow along, you may check the corresponding file in the sandbox or refer the code snippet attached.




Let us now review the anti-patterns and patterns followed in the above four approaches.


Anti-Pattern #1: Props or context as initial state


In the incorrect approach, props or context has been used as an initial value for useState or useRef. In line 21 of Incorrect.tsx, we can see that the total character count has been calculated and stored as a state.


import { useCallback, useEffect, useState } from "react";
import { useGetArticles } from "../hooks/useGetArticles";
import { useGetEmoji } from "../hooks/useGetEmoji";
import { Articles } from "../types";
import { Navigation } from "../components/Navigation";

const styles: { [key: string]: React.CSSProperties } = {
  container: {
    background: "#FEE2E2",
    height: "100%",
    display: "grid",
    gridTemplateColumns: "10rem auto"
  },
  content: {}
};

const ArticleContent: React.FC<{
  article: Articles["articles"]["0"];
}> = (props) => {
  // Step 1. calculate length as we need it to get corresponding emotion
  const [length] = useState<number>(
    props.article.text.length + props.article.title.length
  );

  // Step 2. fetch emotion map from backend
  const emotions = useGetEmoji();

  // Step 3. set emotion once we get emotion map from backend
  const [emotion, setEmotion] = useState<string>("");
  useEffect(() => {
    if (emotions) {
      setEmotion(emotions["stickers"][length]);
    }
  }, [emotions, length]);

  return (
    <div>
      <div>
        <h2>{props.article.title}</h2>
        <div>{props.article.text}</div>
      </div>
      <h3
        dangerouslySetInnerHTML={{
          __html: `Total Length ${length} ${emotion}`
        }}
      />
    </div>
  );
};

const Incorrect: React.FC = () => {
  const articles = useGetArticles();
  const [currentArticle, setCurrentArticle] = useState<
    Articles["articles"]["0"] | null
  >();
  const onClickHandler = useCallback((article) => {
    setCurrentArticle(article);
  }, []);
  return (
    <div style={styles.container}>
      <Navigation articles={articles} onClickHandler={onClickHandler} />
      <div style={styles.content}>
        {currentArticle ? <ArticleContent article={currentArticle} /> : null}
      </div>
    </div>
  );
};

export default Incorrect;


This anti-pattern is the reason why neither computation nor network request is triggered when a new article is selected.


Anti-Pattern #2: Destroy and Recreate


Let us make amends to our incorrect approach by using ‘Destroy and Recreate’ anti-pattern.


Destroying a functional component refers to destroying all the hooks and the states created during the first function call. Recreating refers to calling the function again as if it had been never called before.


Note that a parent component can use the key prop to destroy the component and recreate it every time the key changes. Yes, you read it right - you can use keys outside loops.


Specifically, we implement ‘Destroy and Recreate’ anti-pattern by using the key prop while rendering the child component ArticleContent of the parent component PartiallyCorrect in the PartiallyCorrect.tsx file (line 65).


import { useCallback, useEffect, useState } from "react";
import { Navigation } from "../components/Navigation";
import { useGetArticles } from "../hooks/useGetArticles";
import { useGetEmoji } from "../hooks/useGetEmoji";
import { Articles } from "../types";

const styles: { [key: string]: React.CSSProperties } = {
  container: {
    background: "#FEF2F2",
    height: "100%",
    display: "grid",
    gridTemplateColumns: "10rem auto"
  },
  content: {}
};

const ArticleContent: React.FC<{
  article: Articles["articles"]["0"];
}> = (props) => {
  // Step 1. calculate length as we need it to get corresponding emotion
  const [length] = useState<number>(
    props.article.text.length + props.article.title.length
  );

  // Step 2. fetch emotion map from backend
  const emotions = useGetEmoji();

  // Step 3. set emotion once we get emotion map from backend
  const [emotion, setEmotion] = useState<string>("");
  useEffect(() => {
    if (emotions) {
      setEmotion(emotions["stickers"][length]);
    }
  }, [emotions, length]);

  return (
    <div>
      <div>
        <h2>{props.article.title}</h2>
        <div>{props.article.text}</div>
      </div>
      <h3
        dangerouslySetInnerHTML={{
          __html: `Total Length ${length} ${emotion}`
        }}
      />
    </div>
  );
};

const PartiallyCorrect: React.FC = () => {
  const articles = useGetArticles();
  const [currentArticle, setCurrentArticle] = useState<
    Articles["articles"]["0"] | null
  >();
  const onClickHandler = useCallback((article) => {
    setCurrentArticle(article);
  }, []);
  return (
    <div style={styles.container}>
      <Navigation articles={articles} onClickHandler={onClickHandler} />
      <div style={styles.content}>
        {/** Step 4. Using key to force destroy and recreate */}
        {currentArticle ? (
          <ArticleContent article={currentArticle} key={currentArticle.id} />
        ) : null}
      </div>
    </div>
  );
};

export default PartiallyCorrect;


The app works as expected but a flickering effect is observed when a new article is selected. Hence, this anti-pattern results in a partially correct output.


Pattern #1: Internal State in JSX


Instead of using ‘Destroy and Recreate’ anti-pattern, in this ‘correct but suboptimal’ approach, we will use ‘re-rendering’.


Re-rendering refers to calling the react functional component again with the hooks intact across function calls. Note that in ‘Destroy and Recreate’, all the hooks are destroyed first and then recreated from scratch.


To implement ‘re-rendering’, useEffect and useState will be used in tandem. The initial value for the useState can be set to null or undefined and an actual value will be computed and assigned to it once useEffect has run. In this pattern, we are circumventing the lack of dependency array in useState by using useEffect.


Specifically, notice how we have moved the total character count computation into JSX (line 44) in the Suboptimal.tsx and we are using props (line 33) as a dependency in the useEffect (line 25). Using this pattern, the flickering effect has been avoided but a network request is made to fetch emojis whenever props change. So even if there is no change in character count, an unnecessary request is made to fetch the same emoji.


import { useCallback, useEffect, useState } from "react";
import { Navigation } from "../components/Navigation";
import { useGetArticles } from "../hooks/useGetArticles";
import { useGetEmoji } from "../hooks/useGetEmoji";
import { Articles } from "../types";

const styles: { [key: string]: React.CSSProperties } = {
  container: {
    background: "#FEFCE8",
    height: "100%",
    display: "grid",
    gridTemplateColumns: "10rem auto"
  },
  content: {}
};

const ArticleContent: React.FC<{
  article: Articles["articles"]["0"];
}> = (props) => {
  // Step 2. fetch emotion map from backend
  const emotions = useGetEmoji();

  // Step 3, set emotion once we get emotion map from backend
  const [emotion, setEmotion] = useState<string>("");
  useEffect(() => {
    if (emotions) {
      setEmotion(
        emotions["stickers"][
          props.article.text.length + props.article.title.length
        ]
      );
    }
  }, [emotions, props]);

  return (
    <div>
      <div>
        <h2>{props.article.title}</h2>
        <div>{props.article.text}</div>
      </div>
      <h3
        dangerouslySetInnerHTML={{
          __html: `Total Length ${
            props.article.text.length + props.article.title.length
          } ${emotion}`
        }}
      />
    </div>
  );
};

const Suboptimal: React.FC = () => {
  const articles = useGetArticles();
  const [currentArticle, setCurrentArticle] = useState<
    Articles["articles"]["0"] | null
  >();
  const onClickHandler = useCallback((article) => {
    setCurrentArticle(article);
  }, []);
  return (
    <div style={styles.container}>
      <Navigation articles={articles} onClickHandler={onClickHandler} />
      <div style={styles.content}>
        {currentArticle ? <ArticleContent article={currentArticle} /> : null}
      </div>
    </div>
  );
};

export default Suboptimal;



Pattern #2: Props as a dependency in useMemo


Let us do it correctly as well as optimally this time. It all started with Anti-Pattern #1: props or context as initial state.


We can fix this by using props as a dependency in useMemo. By moving the total character count computation to useMemo hook in Optimal.tsx (line 22), we are able to prevent network request to fetch emoji unless the total character count has changed.


import { useCallback, useEffect, useMemo, useState } from "react";
import { Navigation } from "../components/Navigation";
import { useGetArticles } from "../hooks/useGetArticles";
import { useGetEmoji } from "../hooks/useGetEmoji";
import { Articles } from "../types";

const styles: { [key: string]: React.CSSProperties } = {
  container: {
    background: "#F0FDF4",
    height: "100%",
    display: "grid",
    gridTemplateColumns: "10rem auto"
  },
  content: {}
};

const ArticleContent: React.FC<{
  article: Articles["articles"]["0"];
}> = (props) => {
  // Step 1. calculate length as we need it to get corresponding emotion
  const length = useMemo<number>(
    () => props.article.text.length + props.article.title.length,
    [props]
  );

  // Step 2. fetch emotion map from backend
  const emotions = useGetEmoji();

  // Step 3. set emotion once we get emotion map from backend
  const [emotion, setEmotion] = useState<string>("");
  useEffect(() => {
    if (emotions) {
      setEmotion(emotions["stickers"][length]);
    }
  }, [emotions, length]);

  return (
    <div>
      <div>
        <h2>{props.article.title}</h2>
        <div>{props.article.text}</div>
      </div>
      <h3
        dangerouslySetInnerHTML={{
          __html: `Total Length ${length} ${emotion}`
        }}
      />
    </div>
  );
};

const Optimal: React.FC = () => {
  const articles = useGetArticles();
  const [currentArticle, setCurrentArticle] = useState<
    Articles["articles"]["0"] | null
  >();
  const onClickHandler = useCallback((article) => {
    setCurrentArticle(article);
  }, []);
  return (
    <div style={styles.container}>
      <Navigation articles={articles} onClickHandler={onClickHandler} />
      <div style={styles.content}>
        {currentArticle ? <ArticleContent article={currentArticle} /> : null}
      </div>
    </div>
  );
};

export default Optimal;


Closing Thoughts:

In this article, we discussed that using props or context as initial state and ‘Destroy and Recreate’ are anti-patterns while using internal state in JSX and props as a dependency in useMemo are good patterns. We also learned that we should be cautious when we are using hooks without a dependency array and nesting for arranging React components.


Written in collaboration with Shyam Swaroop, Co-Founder & CTO at Atri Labs


Also published here.

react to story with heart
react to story with light
react to story with boat
react to story with money
Darshita Chaturvedi HackerNoon profile picture
by Darshita Chaturvedi @darshitac.Co-Founder & CEO, Atri Labs — Creating a new full-stack web development framework. Prev: MIT, IIT, BlackRock
Read my stories
L O A D I N G
. . . comments & more!