I'm working on building a multistep thing, but the steps are dynamic and are built based on some conditions coming from params.
While building it I noticed that the child components are not refreshing once the state in the parent component is updated due to the way they are being declared/generated (inside a function returning an array of steps)
I've been trying to figure out if there is an easy way around this. If I wrap it with a context provider and access it within the steps it works fine, but it adds some complexity that I was trying to avoid, especially for unit testing later on, since there will be a lot of steps in the multistep approach. Also if I move the array creation inside the return it works, but then I lose access to the array variable, where I need the length to check the number of steps.
I've built an oversimplified example of the issue demoing the props not updating if using the array of components while if the step is declared directly it works fine: https://codesandbox.io/s/react-dynamic-child-issue-toukfg?file=/src/App.js:0-806
import React from "react"
const TargetGroupStep = ({ targetGroup, setTargetGroup }) => (
<div>
<div>props = {targetGroup}</div>
<button onClick={() => setTargetGroup("group1")}>Button 1</button>
<button onClick={() => setTargetGroup("group2")}>Button 2</button>
</div>
);
export default function App() {
const [targetGroup, setTargetGroup] = React.useState("");
const buildStepsFlow = () => [
<TargetGroupStep targetGroup={targetGroup} setTargetGroup={setTargetGroup}/>,
]
const [steps, setSteps] = React.useState(buildStepsFlow());
return (
<div className="App">
<div>Parent state: {targetGroup}</div>
{steps[0]}
<br/>
If declared:
<TargetGroupStep targetGroup={targetGroup} setTargetGroup={setTargetGroup}/>
</div>
);
}
Don't put components into state. Instead, have plain data in state, and transform it into components only when needed, during rendering - this ensures that all the rendered components have the most up-to-date props and state.
Since you don't call setSteps
, you can remove that state entirely - call buildStepsFlow
directly.
const TargetGroupStep = ({ targetGroup, setTargetGroup }) => (
<div>
<div>props = {targetGroup}</div>
<button onClick={() => setTargetGroup("group1")}>Button 1</button>
<button onClick={() => setTargetGroup("group2")}>Button 2</button>
</div>
);
function App() {
const [targetGroup, setTargetGroup] = React.useState("");
const buildStepsFlow = () => [<TargetGroupStep targetGroup={targetGroup} setTargetGroup={setTargetGroup}/>];
return (
<div className="App">
<div>Parent state: {targetGroup}</div>
{buildStepsFlow()[0]}
<br/>
If declared:
<TargetGroupStep targetGroup={targetGroup} setTargetGroup={setTargetGroup}/>
</div>
);
}
ReactDOM.createRoot(document.querySelector('.react')).render(<App />);
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<div class='react'></div>
If you do call setSteps
in your actual code, change around its state so that what it stores is plain arrays and objects, and map them them to components when returning at the end of App
.