Home > front end >  React: Hook not triggering when parent component changes boolean value passed to child
React: Hook not triggering when parent component changes boolean value passed to child

Time:12-05

so I have a parent that is responsible for rendering a list of items. When the item is clicked it will send an event to the parent, that will record on a Map the selection state. If selected it passes the selected state to the child so that it can change style. Here's the code for the parent:

export default function CreateYourMixProductsList(props: ProductsInterface) {
    const selectedProducts: Map<number, Product> = new Map<number, Product>();

function onProductClick(product: Product): void {
    selectedProducts.set(product.id, product);
}

return (
    <>
        <List sx={{width: '100%', bgcolor: 'background.paper'}}>
            {props?.products?.map(product => (
                <ProductListItem key={product.id} product={product}
                                 selected={selectedProducts.has(product.id)}
                                 onClick={(product) => onProductClick(product)} />
            ))}
        </List>
    </>
);

and the child

export default function ProductListItem(props: ProductProps) {
    const [selected, setSelected] = React.useState(false);

    function onClick(product: Product) {
       props.onClick(product);
    }

    useEffect(() => {
        setSelected(!selected);
    }, [props.selected]);

    return (
        <>
            <ListItemButton alignItems="flex-start" onClick={event => {onClick(props.product)}} selected={props.selected ? selected : false}>
//omitted code to keep it short

The useEffect is triggered only on rendering, whilst to my understanding, it should be triggered every time the props passed down is an immutable variable. What am I missing here?

CodePudding user response:

here's a bit of documentation on useEffect https://reactjs.org/docs/hooks-effect.html

By default, useEffect will trigger after each render. Adding an array will skip this process and instead only trigger the useEffect on the changing of the specified prop(s). If we take a look at your code example:

useEffect(() => {
        setSelected(!selected);
    }, [props.selected]);

this block is triggering the useEffect each time props.selected is updated, hence why it only triggers on render, when you give the value of false to selected. What you can do instead is call setSelected(!selected) as part of the onClick event. Once this is occurs, you can update the useEffect to handle any logic that should occur when selected changes, or remove the useEffect entirely.

CodePudding user response:

Lets unwrap this:

Why is the useEffect not running?

Hooks re-run every time a variable in their dependency array changes. Your hook is not running again because the value of props.selected does not change. You can easily verify this by simply logging the value in the component.

Why is props.selected not changing?

Your click handler correctly sets the value on your Map. However, React does not recognize that a a new value was set inside the map. The component never re-renders and selectedProducts.has() is not called again. So the value of props.selected is indeed still the same.

How can you make React recognize your change?

First of all, you should avoid declaring state like this in your component. Each time this component renders it will re-declare all variables defined inside it (selectedProducts will always be set to a new empty map). Use reacts hook api instead.

To make the variable stick - and reactive - you can simply use it with useState() as you did in your child-component. E.g.:

...
const [selectedProducts, setSelectedProducts] = useState<Map<number, Product>>(new Map<number, Product>());

function onProductClick(product: Product): void {
    selectedProducts.set(product.id, product);
    // Note how we are re-setting the variable to make React aware of the change!
    setSelectedProducts(new Map(selectedProducts));
}
...

It is important to note that Reacts useState hook compares values with tripple-equal (===) to determine whether it should trigger a re-render. Due to the nature of JavaScript (where variables only hold references to objects), setting a new value on a Map will not change its reference. To trigger a re-render you have to replace the Map.

Creating new objects is computationally cheap so this shouldn't cause any issues most of the times.

  • Related