Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to isolate state among different instances of functional component that uses useState hook?

Example is simple functional component that use useState to keep click counter.

Stepping through Stepper MUI component, I want to create instance of Example component with different init value e.g. at step 0, init value 100, at step 1, init value 111, at step 2, init value 112.

While stepping through each case of step, despite passing different initial value, Example functional component keep state only to first init value i.e. 100.

File is /Components/Navigation/Stepper01a.js, Example component being referenced in StepContent, which is being referenced in HorizontalLinearStepper component. Overall code is example from Material UI Stepper component. Am just trying to test it to create different instance of other functional component (Example in this case) with different initial values at each step.

Example component:

function Example({ init }) {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = React.useState(init)

  return (
    <div>
      <p>init {init} </p>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  )
}

StepContent Component:

function StepContent({ step }) {
  console.log("step", step)

  switch (step) {
    case 0:
      return <Example init={100} />
    case 1:
      return <Example init={111} />
    case 2:
      return <Example init={112} />
    default:
      return "Unknown step"
  }
}

HorizontalLinearStepper Component:

export default function HorizontalLinearStepper() {
...
 <div>
     <StepContent step={activeStep} />
 </div>
...
}

See running example https://mj3x4pj49j.codesandbox.io/ code https://codesandbox.io/s/mj3x4pj49j

Upon clicking Next at step 0, count is expected to set with init value as 111 but it remains 100 and upon clicking Next at step 1, count is expected to set with init value 112 but again it remains 100. It seems that once state count is initialized to 100 at step 0, same state is being used in step 1 and 2 i.e. state is not isolated.

Does this problem occur due to violation of Rules of Hooks somehow?

like image 232
user1921636 Avatar asked Jun 13 '19 11:06

user1921636


2 Answers

Inside the component you are doing correct. Init value taken from props - it's common practice.

But see, useState() applies init value only on initial render(componentDidMount or constructor for class-based components). The way you use that component you did not re-create <Example but update that.

So in other words the same case you'd get having class-based component without componentDidUpdate: React updates existing <Example> instead of re-creating that and does not apply componentDidMount anymore(for your case it does not init useState with initial value).

I see different way to handle that.

  1. Force React to re-create element instead of updating by using key. Hacky but working way:
switch (step) {
  case 0:
    return <Example key="step-1" init={100} />
  case 1:
    return <Example key="step-2" init={111} />
  case 2:
    return <Example key="step-3" init={112} />
  default:
    return "Unknown step"
}
  1. You may use useEffect as hook-version of componentDidUpdate:
useEffect(() => {
  setCount(init);
}, [init]);

Here is a tricky moment since you are counting clicks. So making just setCount(init) might be not good move and will break your calculation. So actual code may be much more complicated. I'm not sure here since don't understand the logic behind counting.

  1. With lifting state up(push count to parent component) you will not need to update anything after init is updated.
like image 53
skyboyer Avatar answered Sep 30 '22 03:09

skyboyer


You need to give a hint to React that you need different instances of Example component during different steps. This hint can be done like so:

function StepContent({ step }) {
  console.log("step", step)

  switch (step) {
    case 0:
      return <Example init={100} key={0} />
    case 1:
      return <Example init={111} key={1} />
    case 2:
      return <Example init={112} key={2}/>
    default:
      return "Unknown step"
  }
}

The reason is that React is seeing an Example in virtual dom in all cases and according to its algorithm, it thinks it is the same component but with different prop so he just changes its state and does not re-init it.

like image 44
Oleksandr Nechai Avatar answered Sep 30 '22 01:09

Oleksandr Nechai