0

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>
  );
}
Sidou Gmr
  • 138
  • 8
  • Redefining a component on every render isn't "thinking in react," there is a lot incorrect with how you have this. See [this answer](https://stackoverflow.com/a/72915266/864233) for some related ideas. – romellem Oct 28 '22 at 18:52

1 Answers1

3

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.

CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • I actually need to know the amount of steps in the array (it is used in other child components, like progress bar and etc), if I do it directly calling the build function I won't be able to access the value. I actually use setStep somewhere else, but on this oversimplified example it is not used – user3107720 Oct 28 '22 at 18:49
  • 2
    Are all the renderings for `TargetGroupStep` meant to be the same? If so, you could have the state be just a number: the number of steps. `const [steps, setSteps] = useState(1);` and then at the end, to render them: `Array.from({ length: steps }, buildStepsFlow)` where `buildStepsFlow` now returns a single element rather than an array. – CertainPerformance Oct 28 '22 at 19:03
  • I'm supposed to have multiple step components, I built using just target group to simplify. At the same time I understand having a simple array to defined steps, it's balão a bit cumbersome because I'll need to generate the components based on the array of strings anyway, so I was trying to shortcut it – user3107720 Oct 28 '22 at 19:13