Reputation: 1450
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.
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
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>
);
}
Upvotes: 1
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