Janice Zhong
Janice Zhong

Reputation: 878

React doesn't rerender on an array state update

I'm trying to make 3 button controls to open and close 3 corresponding menu lists. Material-UI is the UI framework I'm currently using. Here is the code:


const Filters = () => {

    const [anchors, setAnchors] = useState([null, null, null])

    const handleClick = (event, index) => {
        const arr = anchors
        arr[index] = event.target
        setAnchors(arr)    
    }

    const handleClose = (index) => {
        const arr = anchors
        arr[index] = null
        setAnchors(arr)  
    }

    return(
        <div id="top-filters">
            <div className="controls">
                <Button onClick={(event) => handleClick(event, 0)}>Sort By</Button>
                <Button onClick={(event) => handleClick(event, 1)}>Price</Button>
                <Button onClick={(event) => handleClick(event, 2)}>Delivery Fees</Button>
            </div>
        {
            console.log({anchors})
        }
            <Menu key={1}
                anchorEl={anchors[0]} 
                open={Boolean(anchors[0])} 
                onClose={() => handleClose(0)} 
                keepMounted
            >
                <MenuItem>
                    <ListItemText primary="By Rating" />
                </MenuItem>
            </Menu>

            <Menu key={2}
                anchorEl={anchors[1]} 
                open={Boolean(anchors[1])} 
                onClose={() => handleClose(1)} 
                keepMounted            
            >
                <MenuItem>
                    <Slider defaultValue={25} marks={marks} step={25} />
                </MenuItem>
            </Menu>

            <Menu key={3}
                anchorEl={anchors[2]} 
                open={Boolean(anchors[2])} 
                onClose={() => handleClose(2)} 
                keepMounted            
            >
                <MenuItem>
                    <ListItemText primary="By Delivery Fees" />
                </MenuItem>
            </Menu>

            <FormControl className="search-bar-filter">
                <Icon>search</Icon>
                <StyledInput classes={{ root: classes.root }} name="search" type="search" placeholder="Search" disableUnderline />
            </FormControl>
        </div>
    )
}

export default Filters

I checked the values update in console, they look fine, but I'm not sure why React won't rerender the page (when I click the buttons, nothing happens but anchors state is updated). Thanks for help.

Upvotes: 6

Views: 12975

Answers (2)

devserkan
devserkan

Reputation: 17608

What you are doing there is a mutation:

arr[index] = event.target

You should avoid mutation when updating your state. Because if you mutate your state React can not understand your state has changed. You can use methods like map, filter or any other method which do not mutate your state.

const handleClick = (event, index) => {
  setAnchors((prev) =>
    prev.map((el, i) => {
      if (i !== index) {
        return el;
      }

      return event.target;
    })
  );
};

or if you like a concise one:

const handleClick = (event, index) => {
  setAnchors((prev) => prev.map((el, i) => (i !== index ? el : event.target)));
};

The spread syntax can be used for non-nested arrays, but if you are working on nested arrays and changing the nested values just be careful since spread syntax creates shallow copies. This is why I like to to use methods like map most of the time.

If you don't want to map all over the array (some prefers this) Object.assign and spread syntax can be used together.

const handleClick = (event, index) => {
  setAnchors((prev) => Object.assign([], { ...prev, [index]: event.target }));
};

Update:

As I explained before, spread syntax only makes shallow copies.

Note: Spread syntax effectively goes one level deep while copying an array. Therefore, it may be unsuitable for copying multidimensional arrays, as the following example shows. (The same is true with Object.assign() and spread syntax.)

Source

This means nested values keep the same references as the original ones. So, if you change something for the new array (or object) it also changes the original one.

const arr = [
  { id: 0, name: "foo" },
  { id: 1, name: "bar" },
  { id: 2, name: "baz" },
];

const newArr = [...arr];

newArr[0].name = "something else";

console.log("newArr", newArr);
console.log("original arr", arr);

As you can see our original array also changed. Actually, this isn't a nested array but we are changing a nested property for the array element. Maybe this is a better example for nested arrays, but the example above is more realistic.

const arr = [["foo", "bar"], [1,2]];

const newArr = [...arr];

newArr[0][0] = "fizz";

console.log(newArr);
console.log(arr);

Upvotes: 13

wangdev87
wangdev87

Reputation: 8751

You are wrongly updating the state. const arr = anchors is not the correct way to clone the anchors. You need to use ... operator.

    const handleClick = (event, index) => {
        const arr = [... anchors]
        arr[index] = event.target
        setAnchors(arr)    
    }

    const handleClose = (index) => {
        const arr = [... anchors]
        arr[index] = null
        setAnchors(arr)  
    }

Upvotes: 7

Related Questions