【问题标题】:React + Material-UI: list should have a unique "key" propReact + Material-UI:列表应该有一个唯一的“key”道具
【发布时间】:2019-12-03 16:51:48
【问题描述】:

我在渲染过程中收到以下错误/警告:

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)

怎么办?我需要为我的ListItemmaterial-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


    【解决方案1】:

    您应该在 .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 这样的唯一属性。

    【讨论】:

    • 我添加了你所有的建议,没有效果,对不起。
    • @János 您能否提供一个工作示例来重现该问题?
    • 我解决了整个代码的问题 :) 它正在工作,但发出警告
    【解决方案2】:

    您的问题在以下循环中

     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

    【讨论】:

    • 我添加了你所有的建议,没有效果,对不起。
    【解决方案3】:

    您是对的,您需要为每个ListItem 提供一个唯一的密钥,例如id。您可以使用来自Array.map() 的索引,但一般不推荐。

    正如官方 React documentation 所述,

    Keys 帮助 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>
          )
    });
    

    【讨论】:

    • 添加了key={page.id},但仍然收到该警告。你还有什么想法吗?
    • 因为缺少内部列表的key。看看其他两个答案
    • 我添加了你所有的建议,没有效果,对不起。
    • 是的,如果组件内有一个列表,这个子列表必须有一个“key”,检查组件内“map”的其他使用情况,每个都添加“key”,它会解决问题。
    【解决方案4】:

    将 ListItem 组件包含在 中,并将 key 属性应用到 组件,如下所示:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2023-02-01
      • 2022-01-14
      • 2021-12-25
      • 2020-09-06
      • 2017-07-28
      • 2022-08-08
      • 1970-01-01
      • 2021-02-12
      相关资源
      最近更新 更多