Simon Long
Simon Long

Reputation: 1450

Why doesn't this React component re-render when its prop changes?

I've created a cut-down version of a re-render issue which I'm having with an application I am working on.

In reality DisplayElement1 and DisplayElement2 are two complex components.

DisplayElement2 here is iterating through a simple array of numbers (supplied via its prop numbers) and displaying them.

Problem : When the array behind the numbers prop gets updated in the main component App (in this case by clicking on the Add Number to Array button I would expect DisplayElement2 to re-render with the updated array but it doesn't, why not ??

If I click Show Display 1 and then click back on Show Display 2 the updated array renders.

Edit simplified-dashboard-working

App.js

import React, { useState, useMemo } from "react";
import "./styles.css";
import DisplayComponent1 from "./DisplayComponent1";
import DisplayComponent2 from "./DisplayComponent2";
import { Button } from "@material-ui/core";

export default function App() {
  const [numbersToDisplay, setNumbersToDisplay] = useState([1, 2, 3]);

  ////////////////////////////////////////////////
  const component1 = useMemo(() => {
    return <DisplayComponent1 />;
  }, []);

  const component2 = useMemo(() => {
    return (
      <DisplayComponent2
        style={{ background: "red" }}
        numbers={numbersToDisplay}
      />
    );
  }, [numbersToDisplay]);
  ////////////////////////////////////////////////

  const [currentDisplayComponent, setCurrentDisplayComponent] = useState(
    component2
  );

  return (
    <div className="App">
      <Button
        variant="contained"
        color="secondary"
        onClick={() => setCurrentDisplayComponent(component1)}
      >
        Show Display 1
      </Button>
      <Button
        variant="contained"
        color="primary"
        onClick={() => setCurrentDisplayComponent(component2)}
      >
        Show Display 2
      </Button>

      <Button
        variant="contained"
        style={{ marginLeft: 50 }}
        onClick={() => {
          let tempArray = Array.from(numbersToDisplay);
          tempArray.push(4);
          setNumbersToDisplay(tempArray);
        }}
      >
        Add number to array
      </Button>

      {currentDisplayComponent}
    </div>
  );
}

DisplayElement1.js and DisplayElement2.js

import React from "react";
import {Paper} from "@material-ui/core";

export default function DisplayComponent1(props) {
  return (
    <Paper>
      <p>This is DisplayComponent1</p>
    </Paper>
  );
}
import React from "react";
import { Paper } from "@material-ui/core";

export default function DisplayComponent2(props) {
  return (
    <Paper>
      <p>This is DisplayComponent2</p>
      {props.numbers.map((currNumber, currIndex) => {
        return <div key={currIndex}>{currNumber}</div>;
      })}
    </Paper>
  );
}

Upvotes: 1

Views: 235

Answers (2)

Shubham Khatri
Shubham Khatri

Reputation: 282030

The reason your component doens't re-render with updated props is because you have a previous instance of your component stored in the currentDisplayComponent state which is what you use to render

  • A hacky workaround with your current code would be to make use of useEffect and update the component instance that is active

  • However the best solution in this scenarios is to take out the component instances outside of the state and render these based on a selected component string state.

To prevent unnecessary updates you can make use of React.memo

export default React.memo(function DisplayComponent2(props) {
  return (
    <Paper>
      <p>This is DisplayComponent2</p>
      {props.numbers.map((currNumber, index) => {
        return <div key={index}>{currNumber}</div>;
      })}
    </Paper>
  );
});

App.js

export default function App() {
  const [numbersToDisplay, setNumbersToDisplay] = useState([1, 2, 3]);

  const [currentDisplayComponent, setCurrentDisplayComponent] = useState(
    "component1"
  );

  const getCurrentComponent = currentDisplayComponent => {
    switch (currentDisplayComponent) {
      case "component1":
        return <DisplayComponent1 />;
      case "component2":
        return (
          <DisplayComponent2
            style={{ background: "red" }}
            numbers={numbersToDisplay}
          />
        );
      default:
        return null;
    }
  };
  return (
    <div className="App">
      <Button
        variant="contained"
        color="secondary"
        onClick={() => setCurrentDisplayComponent("component1")}
      >
        Show Display 1
      </Button>
      <Button
        variant="contained"
        color="primary"
        onClick={() => setCurrentDisplayComponent("component2")}
      >
        Show Display 2
      </Button>

      <Button
        variant="contained"
        style={{ marginLeft: 50 }}
        onClick={() => {
          let tempArray = Array.from(numbersToDisplay);
          tempArray.push(4);
          setNumbersToDisplay(tempArray);
        }}
      >
        Add number to array
      </Button>

      {getCurrentComponent(currentDisplayComponent)}
    </div>
  );
}

Working demo

Upvotes: 1

possum
possum

Reputation: 2927

Consider something like this for your App.js, where the display states are enumerated and we've removed the useMemo

import React, { useState } from "react";
import "./styles.css";
import DisplayComponent1 from "./DisplayComponent1";
import DisplayComponent2 from "./DisplayComponent2";
import { Button } from "@material-ui/core";

const DisplayStatEnum = {COMPONENT1: 0, COMPONENT2: 1}; 

export default function App() {
  const [numbersToDisplay, setNumbersToDisplay] = useState([1, 2, 3]);

  ////////////////////////////////////////////////
  const component1 = <DisplayComponent1 />;

  const component2 = <DisplayComponent2
        style={{ background: "red" }}
        numbers={numbersToDisplay}
      />;
  ////////////////////////////////////////////////

  const [currentDisplayComponent, setCurrentDisplayComponent] 
    = useState(DisplayStatEnum.COMPONENT2);

  const componentSelected = 
    currentDisplayComponent === DisplayStatEnum.COMPONENT1
      ? component1
      : component2;

  return (
    <div className="App">
      <div>
      <Button
        variant="contained"
        color="secondary"
        onClick={() => setCurrentDisplayComponent(DisplayStatEnum.COMPONENT1)}
      >
        Show Display 1
      </Button>
      <Button
        variant="contained"
        color="primary"
        onClick={() => setCurrentDisplayComponent(DisplayStatEnum.COMPONENT2)}
      >
        Show Display 2
      </Button>
      </div>

      <Button
        variant="contained"
        style={{ marginLeft: 50 }}
        onClick={() => {
          let tempArray = Array.from(numbersToDisplay);
          tempArray.push(4);
          setNumbersToDisplay(tempArray);
        }}
      >
        Add number to array
      </Button>

      {componentSelected}
    </div>
  );
}

Upvotes: 1

Related Questions