Home > Enterprise >  Why won't React component re-render after setting new state?
Why won't React component re-render after setting new state?

Time:07-20

I have a component that doesn't appear to re-render even though the parent state has in fact updated.

Here is the (contrived) code:

const Index = () => {
    render (
        <Foo initialFoo={{ isTrue: true, count: 0 }} />
    )
}

const Foo = ({ initialFoo }) => {
    const [foo, setFoo] = useState({ ...initialFoo })

    const reset = () => {
        setFoo({ ...initialFoo })
    }

    return (
        <>
        {
            foo.isTrue ?
            <Bar initialBar={foo} reset={reset} /> :
            <span>{foo.count}</span>
        }
        </>
    )
}

const Bar = ({ initialBar, reset }) => {
    const [bar, setBar] = useState({ ...initialBar })

    return (
        <div
            onm ouseOver={() => setBar({ ...bar, count: bar.count   1 })}
            onClick={reset}
        >
            <span>{bar.count}</span>
        </div>
    )
}

Hovering over the component increments count by 1 and displays it accordingly, but when I click on it, nothing changes when I expect the initial state to be displayed.

If I add logs in the Foo component, I can see that foo is updating back to the initial state after clicking on the component, but Bar isn't re-rendering even though it depends on foo. Shouldn't a change in state in the parent component trigger the children to re-render?

Note: I'm not looking for suggestions on how to write the code better/differently to achieve the desired result, rather just trying to understand the reason why the component is not displaying the original state after clicking on it in this current setup

CodePudding user response:

When you declare this state:

const [bar, setBar] = useState({ ...initialBar })

You are telling React to start tracking a new state variable which is initialised with the current value of initialBar. When the value of initialBar changes in future renders, it will have no effect on the state derived from it - the initialisation only happens on component mount.

To keep bar synchronised with initialBar, you would need to add a useEffect and manually set bar yourself:

const [bar, setBar] = useState({ ...initialBar })

useEffect(() => {
  setBar(initialBar)
}, [initialBar])

This effect will be run only when the reference value of initialBar changes, which is currently only when the reset function is called. You would also effectively be setting bar twice on component mount.

CodePudding user response:

Everything works as expected, reset drops foo to initail state but in you Bar component initialBar is used only to populate the initial state and it will not update after that.

import React, { useEffect, useState } from "react";
import "./styles.css";

export default function App() {
  return <Index />;
}

const Index = () => {
  return <Foo initialFoo={{ isTrue: true, count: 0 }} />;
};

const Foo = ({ initialFoo }) => {
  const [foo, setFoo] = useState({ ...initialFoo });

  const reset = () => {
    console.log(foo, initialFoo);
    setFoo({ ...initialFoo });
  };

  return (
    <>
      {foo.isTrue ? (
        <Bar initialBar={foo} reset={reset} />
      ) : (
        <span>{foo.count}</span>
      )}
    </>
  );
};

const Bar = ({ initialBar, reset }) => {
  const [bar, setBar] = useState({ ...initialBar });

  // Uncomment to update bar on initialBar changed
  /*
  useEffect(() => {
    setBar({ ...initialBar });
  }, [initialBar]);
  */

  return (
    <button
      onm ouseOver={() => setBar({ ...bar, count: bar.count   1 })}
      onClick={reset}
    >
      <span>{bar.count}</span>
    </button>
  );
};

Edit compassionate-bohr-8ehbkd

  • Related