React + Material-UI:列表应具有唯一的“键”属性

问题描述 投票:2回答:3

我在渲染期间收到以下错误/警告:

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)

该怎么办?我是否需要向ListItem material-ui组件添加uniq键?

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>
    );
  }
}

reactjs material-ui
3个回答
2
投票

您应该在渲染器内部的.map内的组件中添加唯一的道具

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>
          );
        });

[请注意,使用i(索引)不是很好,您应该具有id之类的唯一属性。


1
投票

您的问题在以下循环中

 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>
     );
 })

每个列表项应具有唯一的键(在同级中),因此您需要为内部和外部循环提供键,像这样

        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>
            );
        });

在此示例中,我使用index作为键,但是您应该使用avoid that


1
投票

[是的,您需要为每个ListItem提供一个唯一的密钥,例如id。您可以使用Array.map()中的索引,但通常不建议使用。

如官方React documentation所述,

键可帮助React确定哪些项目已更改,添加或为删除。键应赋予数组内的元素以给出元素的身份稳定:

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>
      )
});

0
投票

[是的,您需要为每个ListItem提供一个唯一的密钥,例如id。您可以使用Array.map()中的索引,但通常不建议使用。

如官方React documentation所述,

键可帮助React确定哪些项目已更改,添加或为删除。键应赋予数组内的元素以给出元素的身份稳定:

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>
      )
});
© www.soinside.com 2019 - 2024. All rights reserved.