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?
>Solution :
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.