Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to force React (functional component using hooks) to create a new component from scratch on props change?

TLDR

Is there a way to force React to recreate a component (functional component using hooks) from scratch instead of re-rendering it on a prop change?

EDIT

The key attribute DOES work. The problem I was having was due to the state update sequence that I was doing. The first update was trying immediately to render a component that needed the last update to be already done.


I'm working on a component called <AddProductWidget>, that will get the following props about a product and need to display its details. It belongs to a page with a search bar that you fill the productID and it gets the product details from a cloud function.

It's rendered like this:

  <AddProductWidget
    userImages={props.productDetails.userImages}    // Array of strings (urls)
    title={props.productDetails.title}
    stars={props.productDetails.stars}
  />

AddProductWidget.js

It renders a <UserImages> component that lets the user clicks on some images to select before saving to the database. So it needed a state to 'remember' which images have been clicked by the user.

function AddProductWidget(props) {

  const [userImagesSelected, setUserImagesSelected] = useState(
    () => {
      if (props.userImages && props.userImages > 0) {
        return new Array(props.userImages.length).fill(false);
      }
      return null;
    }
  );

  // IT renders the <UserImages> component
  // The code here is simplified

  return(
      <UserImages
        userImages={props.userImages}
        userImagesSelected={userImagesSelected}
        setUserImagesSelected={setUserImagesSelected}
      />
  );

}

The initial state of that array userImagesSelected should be the same length of the userImages array and be filled with false values, that will become true once the user start clicking on the images. If the product doesn't have user images, props.userImages will be null and userImagesSelected should also be null.

Everything is working as expected when I load the first product. Whether it has images or not, everything is being displayed correctly.

The problem:

The problem is when I load a product with zero images, and subsequentely I load another product with 5 images, for example. Basically the props for AddProductWidget will change (since it received a new product), but because React seems to be trying to use the same component instance, I get an error, because my UserImages component will try to render the selected images information, but the state of userImagesSelected will still be null, because the last products had no images.

When I was working with classes, I could force React to recreate a component instance using the key attribute, but that doesn't seem to work with hooks, since the following code didn't solve my problem:

  <AddProductWidget
    key={productID}
    userImages={props.productDetails.userImages}    // Array of strings (urls)
    title={props.productDetails.title}
    stars={props.productDetails.stars}
  />

Question

Is there a way to force React to recreate a component (functional component using hooks) from scratch instead of re-rendering it on a prop change?

Extra

A found a way around it, but it seems a bit hacky, even though it's on React docs. It basically uses a state to detect when props change and calls setState() during render. I would like it better to recreate my component from scratch.

Hooks FAQ: How do I implement getDerivedStateFromProps

like image 769
cbdeveloper Avatar asked Sep 02 '25 02:09

cbdeveloper


1 Answers

Not sure how you decided that a different key won't re-mount the component but it should do that (are you sure the key has changed?)

Here is a running example showing this

function Test() {
  React.useEffect(() => {
    console.log('Test was mounted');
    return () => {
      console.log('Test was un-mounted');
    };
  }, []);

  return "hi there, I'm Test";
}

function App() {
  const [val, setVal] = React.useState("Change me...");
  return (
    <div className="App">
      <input value={val} onChange={e => setVal(e.target.value)} />
      <br />
      <Test key={val} />
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id="root" />
like image 146
Sagiv b.g Avatar answered Sep 05 '25 14:09

Sagiv b.g