Melvin
Melvin

Reputation: 339

React fetching data re-renders the DOM

I have an array of objects with dates and ids in them, which I render from MongoDB. When rendered, the individual dates are selectable and I toggle a CSS-style to the selected item(s). When I get the array of objects from the constant "allDates" at the top of the code, everything works as expected. Inside the Element "Dates" I have my code to fetch the same data from MongoDB commented out. If I remove the comment and use it, the selection (toggling of styles) also works, but the DOM appears to be re-rendering on every click. How do I get rid of the re-rendering of all elements? Any help is much appreciated.

import React, { useEffect, useState } from "react";

const allDates = [
  {date: "2021-01-21",id: "d1"},
  {date: "2021-02-21",id: "d2"},
  {date: "2021-03-21",id: "d3"},
  {date: "2021-04-21",id: "d4"},
  {date: "2021-05-21",id: "d5"},
  {date: "2021-06-21",id: "d6"},
  {date: "2021-07-21",id: "d7"},
  {date: "2021-08-21",id: "d8"},
  {date: "2021-09-21",id: "d9"},
];

const Test = () => {
  const [selectedItems, setSelectedItems] = useState([]);

  const selectionHandler = (itemIndex, id) => {
    if (selectedItems.includes(itemIndex)) {
      setSelectedItems((prevSelectedItems) =>
        prevSelectedItems.filter((item) => item !== itemIndex)
      );
    } else {
      setSelectedItems((prevSelectedItems) => [
        ...prevSelectedItems,
        itemIndex,
      ]);
    }
  };

  const Dates = () => {
    const actionDates = allDates;

    //const [actionDates, setActionDates] = useState([]);

    // useEffect(() => {
    //   const sendRequest = async () => {
    //     try {
    //       const dates = await fetch(
    //         `${process.env.REACT_APP_BACKEND_URL}/de/daten`
    //       );

    //       const responseData = await dates.json();

    //       if (!dates.ok) {
    //         throw new Error(responseData.message);
    //       }

    //       setActionDates(responseData);
    //     } catch (err) {
    //       throw new Error(err.message);
    //     }
    //   };
    //   sendRequest();
    // }, []);
    const action = actionDates.map((v, index) => {
      return (
        <div
          key={v.id}
          style={{
            borderBottom: selectedItems.includes(index) && "2px solid blue",
          }}
          onClick={() => selectionHandler(index, v.id)}
        >
          <p>
            <span className="fr_fontBold">{v.date} </span>
          </p>
        </div>
      );
    });
    return action;
  };

  return <Dates />;
};

export default Test;

Upvotes: 1

Views: 83

Answers (1)

Drew Reese
Drew Reese

Reputation: 202605

Issue

It is "rerendering" all elements because you are defining a new Dates React component inside the body of another component. Each time the outer Test component is rerendered (i.e. state and/or props update) it creates a new Dates component and renders it. Dates isn't being rerendered, it's actually being unmounted/mounted as a new component each time.

Solution

You would normally factor Dates into a standalone component but there are dependencies on the state and callbacks from the outer Test component scope. Based on usage my guess is that you accidentally merged the data fetching logic side-effect with the result rendering logic. These should be split out and the result should be returned from the Test component.

const Test = () => {
  const [selectedItems, setSelectedItems] = useState([]);

  const selectionHandler = (itemIndex, id) => {
    if (selectedItems.includes(itemIndex)) {
      setSelectedItems((prevSelectedItems) =>
        prevSelectedItems.filter((item) => item !== itemIndex)
      );
    } else {
      setSelectedItems((prevSelectedItems) => [
        ...prevSelectedItems,
        itemIndex,
      ]);
    }
  };

  useEffect(() => {
    const sendRequest = async () => {
      try {
        const dates = await fetch(
          `${process.env.REACT_APP_BACKEND_URL}/de/daten`
        );

        const responseData = await dates.json();

        if (!dates.ok) {
          throw new Error(responseData.message);
        }

        setActionDates(responseData);
      } catch (err) {
        throw new Error(err.message);
      }
    };
    sendRequest();
  }, []);

  return actionDates.map((v, index) => {
    return (
      <div
        key={v.id}
        style={{
          borderBottom: selectedItems.includes(index) && "2px solid blue",
        }}
        onClick={() => selectionHandler(index, v.id)}
      >
        <p>
          <span className="fr_fontBold">{v.date} </span>
        </p>
      </div>
    );
  });
};

Minor Optimization Suggestion

Use a Map or object to hold the selected id so you get constant-time (O(1)) lookups, as opposed to linear-time (O(n)). Just lookup in the selectedIds state for the current toggled selected value (selectedItems[v.id]). Converting selectionHandler to a curried handler also removes the anonymous callback handler.

const Test = () => {
  const [selectedItems, setSelectedItems] = useState({});

  const selectionHandler = (id) => () => {
    setSelectedItems(ids => ({
      ...ids,
      [id]: !ids[id],
    }));
  };

  useEffect(() => {
    const sendRequest = async () => {
      try {
        const dates = await fetch(
          `${process.env.REACT_APP_BACKEND_URL}/de/daten`
        );

        const responseData = await dates.json();

        if (!dates.ok) {
          throw new Error(responseData.message);
        }

        setActionDates(responseData);
      } catch (err) {
        throw new Error(err.message);
      }
    };
    sendRequest();
  }, []);

  return actionDates.map((v, index) => {
    return (
      <div
        key={v.id}
        style={{
          borderBottom: selectedItems[v.id] && "2px solid blue",
        }}
        onClick={selectionHandler(v.id)}
      >
        <p>
          <span className="fr_fontBold">{v.date} </span>
        </p>
      </div>
    );
  });
};

Upvotes: 3

Related Questions