János
János

Reputation: 35090

React + Material-UI: list should have a unique "key" prop

I get following error / warning during rendering:

Warning: Each child in a list should have a unique "key" prop.

Check the render method of `App`. See .. for more information.
    in ListItemCustom (at App.js:137)
    in App (created by WithStyles(App))
    in WithStyles(App) (at src/index.js:7)

What to do? Do I need to add a uniq key to my ListItem material-ui component?

App.js:

import React, { Component } from "react";
import AppBar from "@material-ui/core/AppBar";
import Toolbar from "@material-ui/core/Toolbar";
import Typography from "@material-ui/core/Typography";
import Button from "@material-ui/core/Button";
import FacebookLogin from "react-facebook-login";
import Menu from "@material-ui/core/Menu";
import MenuItem from "@material-ui/core/MenuItem";
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem";
import ListItemText from "@material-ui/core/ListItemText";
import ArrowForwardIos from "@material-ui/icons/ArrowForwardIos";
import ArrowBackIos from "@material-ui/icons/ArrowBackIos";
import axios from "axios";
import ListItemCustom from "./components/ListItemCustom";
import ListSubheader from "@material-ui/core/ListSubheader";
import Switch from "@material-ui/core/Switch";
import TextField from "@material-ui/core/TextField";
import Box from "@material-ui/core/Box";
import IconButton from "@material-ui/core/IconButton";

// import this
import { withStyles } from "@material-ui/core/styles";

// make this
const styles = theme => ({
  root: {
    flexGrow: 1
  },
  menuButton: {
    marginRight: theme.spacing(2)
  },
  title: {
    flexGrow: 1
  },
  listSubHeaderRoot: {
    backgroundColor: "#E5E5E5",
    color: "#252525",
    lineHeight: "22px"
  }
});

class App extends Component {
  state = {
    accessToken: "",
    isLoggedIn: false,
    userID: "",
    name: "",
    email: "",
    picture: "",
    selectedEvent: undefined,
    buyOrRelease: "buy",
    pages: []
  };

  responseFacebook = response => {
    this.setState({
      accessToken: response.accessToken,
      isLoggedIn: true,
      userID: response.userID,
      name: response.name,
      email: response.email,
      picture: response.picture.data.url
    });
    let accessToken = response.accessToken;
    axios
      .get(
        "https://graph.facebook.com/v5.0/me/accounts?fields=id,name&access_token=" +
          response.accessToken
      )
      .then(async pagesResponse => {
        let promisesArray = pagesResponse.data.data.map(async page => {
          console.log("page " + page.id + " " + page.name);
          return axios
            .get(
              "https://graph.facebook.com/v5.0/" +
                page.id +
                "/events?fields=id,name&access_token=" +
                accessToken
            )
            .catch(e => e);
        });
        const responses = await Promise.all(promisesArray);
        var pages = [];
        responses.forEach((response, i) => {
          const page = pagesResponse.data.data[i];
          pages.push({
            id: page.id,
            name: page.name,
            events: response.data.data
          });
        });
        this.setState({
          pages: pages
        });
      });
  };

  handleClick = event =>
    this.setState({
      anchorEl: event.currentTarget
    });
  handleClose = () => {
    this.setState({ anchorEl: undefined });
  };
  handleCloseAndLogOut = () => {
    this.setState({ anchorEl: undefined });
    this.setState({ isLoggedIn: undefined });
    this.setState({ userID: undefined });
    this.setState({ name: undefined });
    this.setState({ email: undefined });
    this.setState({ picture: undefined });
  };

  switchToRelease = () => {
    this.setState({ buyOrRelease: "release" });
  };

  switchToBuy = () => {
    this.setState({ buyOrRelease: "buy" });
  };

  componentDidMount() {
    document.title = "Tiket.hu";
  }

  handleSort = event => {
    this.setState({ selectedEvent: event });
  };

  navigateBack = () => {
    this.setState({ selectedEvent: undefined });
  };

  render() {
    let fbOrMenuContent;
    let listContent;
    let buyOrReleaseMenuItem;
    if (this.state.isLoggedIn) {
      let eventsList;
      if (this.state.buyOrRelease === "buy") {
      } else {
        eventsList = this.state.pages.map(page => {
          let eventsList2 = page.events.map(event => (
            <ListItemCustom key={event.id} value={event} onHeaderClick={this.handleSort} />
          ));
          return (
            <div>
              <ListSubheader className={this.props.classes.listSubHeaderRoot} key={page.id}>{page.name}</ListSubheader>
              {eventsList2}
            </div>
          );
        });
      }
      listContent = (
        <div>
          <List component="nav" aria-label="main mailbox folders">
            {eventsList}
          </List>
        </div>
      );
      if (this.state.selectedEvent) {
        listContent = (
          <div>
            <List component="nav" aria-label="main mailbox folders">
              <ListItem button onClick={this.navigateBack}>
                <IconButton edge="start" aria-label="delete">
                  <ArrowBackIos />
                </IconButton>

                <Box textAlign="left" style={{ width: 150 }}>
                  Back
                </Box>
                <ListItemText
                  secondaryTypographyProps={{ align: "center" }}
                  primary={this.state.selectedEvent.name}
                />
              </ListItem>

              <ListItem button>
                <Box textAlign="left" style={{ width: 150 }}>
                  Select auditorium
                </Box>
                <ListItemText
                  secondaryTypographyProps={{ align: "right" }}
                  secondary="UP Újpesti Rendezvénytér"
                />
                <IconButton edge="end" aria-label="delete">
                  <ArrowForwardIos />
                </IconButton>
              </ListItem>

              <ListItem button>
                <Box textAlign="left" style={{ width: 150 }}>
                  Release purpose
                </Box>
                <ListItemText
                  secondaryTypographyProps={{ align: "right" }}
                  secondary="Normal selling"
                />
                <IconButton edge="end" aria-label="delete">
                  <ArrowForwardIos />
                </IconButton>
              </ListItem>

              <ListItem>
                <ListItemText primary="Start selling" />
                <Switch edge="end" />
              </ListItem>

              <ListItem>
                <ListItemText primary="Notify if different price would increase revenue" />
                <Switch edge="end" />
              </ListItem>

              <ListSubheader className={this.props.classes.listSubHeaderRoot}>
                Sector
              </ListSubheader>

              <ListItem button>
                <Box textAlign="left" style={{ width: 150 }}>
                  Select sector
                </Box>
                <ListItemText
                  secondaryTypographyProps={{ align: "right" }}
                  secondary="A"
                />
                <IconButton edge="end" aria-label="delete">
                  <ArrowForwardIos />
                </IconButton>
              </ListItem>

              <ListItem button>
                <Box textAlign="left" style={{ width: 500 }}>
                  Marketing resource configuration & result
                </Box>
                <ListItemText
                  secondaryTypographyProps={{ align: "right" }}
                  secondary=""
                />
                <IconButton edge="end" aria-label="delete">
                  <ArrowForwardIos />
                </IconButton>
              </ListItem>

              <ListItem>
                <ListItemText primary="Price in sector" />
                <TextField InputLabelProps={{ shrink: true }} />
              </ListItem>
            </List>
          </div>
        );
      }
      if (this.state.buyOrRelease === "buy") {
        buyOrReleaseMenuItem = (
          <Menu
            id="simple-menu"

            anchorEl={this.state.anchorEl}
            keepMounted
            open={Boolean(this.state.anchorEl)}
            onClose={this.handleClose}
          >
            <MenuItem onClick={this.handleCloseAndLogOut}>Log out</MenuItem>
            <MenuItem onClick={this.switchToRelease}>
              Switch Release mode
            </MenuItem>
            <MenuItem onClick={this.handleClose}>My tickets</MenuItem>
          </Menu>
        );
      } else {
        buyOrReleaseMenuItem = (
          <Menu
            id="simple-menu"
            anchorEl={this.state.anchorEl}
            keepMounted
            open={Boolean(this.state.anchorEl)}
            onClose={this.handleClose}
          >
            <MenuItem onClick={this.handleCloseAndLogOut}>Log out</MenuItem>
            <MenuItem onClick={this.switchToBuy}>Switch Buy mode</MenuItem>
          </Menu>
        );
      }
      fbOrMenuContent = (
        <div>
          <Button
            aria-controls="simple-menu"
            aria-haspopup="true"
            onClick={this.handleClick}
          >
            {this.state.name}
          </Button>
          {buyOrReleaseMenuItem}
        </div>
      );
    } else {
      let fbAppId;
      if (
        window.location.hostname === "localhost" ||
        window.location.hostname === "127.0.0.1"
      )
        fbAppId = "402670860613108";
      else fbAppId = "2526636684068727";
      fbOrMenuContent = (
        <FacebookLogin
          appId={fbAppId}
          autoLoad={true}
          fields="name,email,picture"
          scope="public_profile,pages_show_list"
          onClick={this.componentClicked}
          callback={this.responseFacebook}
        />
      );
    }
    return (
      <div className="App">
        <AppBar position="static">
          <Toolbar>
            <Typography variant="h6" className={this.props.classes.title}>
              Tiket.hu
            </Typography>
            <Button color="inherit">Search</Button>
            <Button color="inherit">Basket</Button>
            {fbOrMenuContent}
          </Toolbar>
        </AppBar>
        {listContent}
      </div>
    );
  }
}

export default withStyles(styles)(App);

ListItemCustom.js:

import React, { Component } from "react";
import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import ListItemText from "@material-ui/core/ListItemText";
import ArrowForwardIos from "@material-ui/icons/ArrowForwardIos";

export default class ListItemCustom extends Component {
  eventSelected = () => {
    this.props.onHeaderClick(this.props.value);
  };
  render() {
    return (
      <ListItem button key={this.props.value.id} onClick={this.eventSelected}>
        <ListItemText primary={this.props.value.name}/>
        <ListItemIcon>
          <ArrowForwardIos />
        </ListItemIcon>
      </ListItem>
    );
  }
}

Upvotes: 14

Views: 24232

Answers (6)

Prason Ghimire
Prason Ghimire

Reputation: 529

To get rid of this warning whatever component is giving you this warning you need to place the key props to its immediate child.

for eg:

<List ...>
    <ListItem key={applyUniqueValueHere}></ListItem>
</List>

OR

<Menu ...>
    <ToolTip key={applyUniqueValueHere}></ToolTip>
</Menu>

OR

<List ...>
    <>...</> 
    //This wont allow to provide key props so convert it to 
    React.Fragment
</List>

Like this

<List ...>
    <React.Fragment key={uniqueIdGoesHere}>...</React.Fragment> 
</List>

Upvotes: 0

Dimitris
Dimitris

Reputation: 11

Inside the map, wrap your element with a div like that:

<div key={i}> <ListItemCustom value={event} onHeaderClick={this.handleSort} /> </div>    

Note: It should work but you need to avoid using the index as a key

Upvotes: 1

nabeelfarid
nabeelfarid

Reputation: 4224

Enclose the ListItem component inside <React.Fragment> and apply key property to the <React.Fragment> component as follows:

<React.Fragment key={`some-unique-id`}>
   <ListItem >
     ...
   </ListItem>                     
</React.Fragment>

Upvotes: 4

wentjun
wentjun

Reputation: 42556

You are right, you will need to supply each ListItem with a unique key, such as an id. You may use the index from Array.map(), but it is generally not recommended.

As stated on the official React documentation,

Keys help React identify which items have changed, are added, or are removed. Keys should be given to the elements inside the array to give the elements a stable identity:

eventsList = this.state.pages.map((page) => {
      let eventsList2 = page.events.map((event) => (
        <ListItemCustom value={event} onHeaderClick={this.handleSort} />
      ));
      return (
        <div>
          <ListSubheader key={page.id}>{page.name}</ListSubheader>
          {eventsList2}
        </div>
      )
});

Upvotes: 1

Dupocas
Dupocas

Reputation: 21347

Your problem is in the following loop

 eventsList = this.state.pages.map(page => {
     let eventsList2 = page.events.map(event => (
         <ListItemCustom value={event} onHeaderClick={this.handleSort} />
     ));
     return (
          <div>
             <ListSubheader>{page.name}</ListSubheader>
             {eventsList2}
         </div>
     );
 })

Each list item should have an unique key (among siblings), so you need to provide keys for the inner and outer loop, like this

        eventsList = this.state.pages.map((page,index) => {
            let eventsList2 = page.events.map((event,i) => (
                <ListItemCustom key={i} value={event} onHeaderClick={this.handleSort} />
            ));
            return (
                <div key={index}>
                    <ListSubheader>{page.name}</ListSubheader>
                    {eventsList2}
                </div>
            );
        });

In this example I'm using the index as key, but you should avoid that

Upvotes: 1

Vencovsky
Vencovsky

Reputation: 31683

You should add an unique prop to your components inside .map that is inside your render

eventsList = this.state.pages.map(page => {     
    let eventsList2 = page.events.map((event, i) => (
            //       unique key prop
            <ListItemCustom key={i} value={event} onHeaderClick={this.handleSort} />
          ));
          return (
            <div key={page.name}> // unique key prop
              <ListSubheader>{page.name}</ListSubheader>
              {eventsList2}
            </div>
          );
        });

Please notice that using i (the index) isn't good, you should have an unique property like an id.

Upvotes: 11

Related Questions