反应:警告列表中的每个孩子都应该有一个唯一的键[重复]

Posted

技术标签:

【中文标题】反应:警告列表中的每个孩子都应该有一个唯一的键[重复]【英文标题】:React: Warning each child in a list should have a unique key [duplicate] 【发布时间】:2021-01-15 18:25:29 【问题描述】:

我一直在调试这个并开始掉头发。到目前为止,我还没有找到解决方案。这是Teaser 组件。我最初是为Home 组件编写测试,但由于样式组件,它有一些错误,所以我的技术主管告诉我为这个新的Teaser 组件(这是Home 组件中的新组件)编写测试),因为它可能会产生一些影响。运行 Teaser.test.tsx 时出现此错误(与键有关):

 FAIL  src/features/home/Teaser.test.tsx (6.781s)
  Teaser component
    × renders Teaser component when user has tonieboxes (185ms)

  ● Teaser component › renders Teaser component when user has tonieboxes

    expect(jest.fn()).not.toBeCalled()

    Expected number of calls: 0
    Received number of calls: 1

    1: "Warning: Each child in a list should have a unique \"key\" prop, "·
    Check the render method of `Teaser`.", "", "
        in Fragment (created by Teaser)
        in Teaser (at Teaser.test.tsx:16)
        in I18nextProvider (at test-utils/index.jsx:38)
        in AuthProvider (at test-utils/index.jsx:37)
        in ConfigProvider (at test-utils/index.jsx:36)
        in ThemeProvider (at test-utils/index.jsx:34)
        in Router (created by MemoryRouter)
        in MemoryRouter (at test-utils/index.jsx:33)
        in Providers"

      41 | // eslint-disable-next-line jest/no-duplicate-hooks
      42 | afterEach(() => 
    > 43 |   expect(console.error).not.toBeCalled()
         |                             ^
      44 |   expect(console.warn).not.toBeCalled()
      45 | 
      46 |   // Reset any request handlers that we may add during the tests,

      at Object.<anonymous> (src/setupTests.js:43:29)

我的预告测试:


import React from 'react'
import  render, screen  from '../../utils/test-utils'
import  Teaser, Tonieboxes  from './Teaser'

const tonieboxes: Tonieboxes[] = [
  
    id: 'toniebox-id-1',
    name: 'toniebox-name-1',
    imageUrl: 'toniebox-image-1',
  ,
]

describe('Teaser component', () => 
  const welcomeMessage = 'welcome-message'
  test('renders Teaser component when user has tonieboxes', () => 
    render(<Teaser tonieboxes=tonieboxes />)
    expect(screen.getByTestId(welcomeMessage)).toBeInTheDocument()
  )
)


我的 Teaser 组件:


import React,  useState, useEffect  from 'react'
import  useTranslation  from 'react-i18next'
import styled from 'styled-components'
import 
  variables,
  Text,
  Bello,
  media,
  Headline,
  Modal,
 from '@boxine/tonies-ui'
import  Link  from 'react-router-dom'
import  HorizontalScrollList  from '../../components/HorizontalScrollList/index'
import BenjaminBlümchen from '../../assets/01_Teaser_Charakter Benjamin.png'
import BibiAndTinaImg from '../../assets/05_Teaser_Charaktere Bibi&Tina.png'

/* German Images */
import newEpisodesImgDE from '../../assets/03_Teaser_Welcome Audiothek DE.png'
import newTonieBoxTurqouiseImgDE from '../../assets/02_2_Teaser_Toniebox Turquoise DE.png'
import creativeToniesImgDE from '../../assets/04_Teaser_Kreativ-Tonies DE.png'
import registerTonieboxImgDE from '../../assets/02_1_Teaser_Toniebox registrieren DE.png'
/* English Images */
import newEpisodesImg from '../../assets/03_Teaser_Welcome Audiothek EN.png'
import newTonieBoxTurqouiseImg from '../../assets/02_2_Teaser_Toniebox Turquoise EN.png'
import creativeToniesImg from '../../assets/04_Teaser_Kreativ-Tonies EN.png'
import AddTonieboxModalContent from '../tonieboxes-page/components/AddTonieboxModalContent'
import registerTonieboxImg from '../../assets/02_1_Teaser_Toniebox registrieren EN.png'

export interface Tonieboxes 
  id: string
  name: string
  imageUrl: string


interface TeaserProps 
  tonieboxes: Tonieboxes[]


interface TunesTeaser 
  alt: string
  src: string
  link: string
  noTonieboxes?: boolean


const tunesTeasersDE: TunesTeaser[] = [
  
    alt: 'BenjaminBlümchen',
    src: BenjaminBlümchen,
    link: '/audio-library?filter=beee313f-55b2-40c1-8032-c41057f92e21',
  ,
  
    alt: 'Tonieboxen',
    src: newTonieBoxTurqouiseImgDE,
    link: '/tonieboxes',
  ,
  
    alt: '400 Neue Folgen',
    src: newEpisodesImgDE,
    link: '/audio-library',
  ,
  
    alt: 'Kreativ Tonies',
    src: creativeToniesImgDE,
    link: '/creative-tonies',
  ,
  
    alt: 'Bibi und Tina',
    src: BibiAndTinaImg,
    link: '/audio-library?filter=dacc4edb-ad1d-4ecd-b98c-b4b31983b5f8',
  ,
]

const tunesTeasersNoTonieboxesDE: TunesTeaser[] = [
  
    alt: 'BenjaminBlümchen',
    src: BenjaminBlümchen,
    link: '/audio-library?filter=beee313f-55b2-40c1-8032-c41057f92e21',
  ,
  
    alt: 'Registriere Deine Toniebox',
    src: registerTonieboxImgDE,
    link: '',
    noTonieboxes: true,
  ,
  
    alt: '400 Neue Folgen',
    src: newEpisodesImgDE,
    link: '/audio-library',
  ,
  
    alt: 'Kreativ Tonies',
    src: creativeToniesImgDE,
    link: '/creative-tonies',
  ,
  
    alt: 'Bibi und Tina',
    src: BibiAndTinaImg,
    link: '/audio-library?filter=dacc4edb-ad1d-4ecd-b98c-b4b31983b5f8',
  ,
]

const tunesTeasersEng: TunesTeaser[] = [
  
    alt: 'Benjamin Bluemchen',
    src: BenjaminBlümchen,
    link: '/audio-library?filter=beee313f-55b2-40c1-8032-c41057f92e21',
  ,
  
    alt: 'Tonieboxes',
    src: newTonieBoxTurqouiseImg,
    link: '/tonieboxes',
  ,
  
    alt: '400 New Episodes',
    src: newEpisodesImg,
    link: '/audio-library',
  ,
  
    alt: 'Creative Tonies',
    src: creativeToniesImg,
    link: '/creative-tonies',
  ,
  
    alt: 'Bibi and Tina',
    src: BibiAndTinaImg,
    link: '/audio-library?filter=dacc4edb-ad1d-4ecd-b98c-b4b31983b5f8',
  ,
]

const tunesTeasersNoTonieboxesEng: TunesTeaser[] = [
  
    alt: 'Benjamin Bluemchen',
    src: BenjaminBlümchen,
    link: '/audio-library?filter=beee313f-55b2-40c1-8032-c41057f92e21',
  ,
  
    alt: 'Register Your Toniebox',
    src: registerTonieboxImg,
    link: '',
    noTonieboxes: true,
  ,
  
    alt: '400 New Episodes',
    src: newEpisodesImg,
    link: '/audio-library',
  ,
  
    alt: 'Creative Tonies',
    src: creativeToniesImg,
    link: '/creative-tonies',
  ,
  
    alt: 'Bibi and Tina',
    src: BibiAndTinaImg,
    link: '/audio-library?filter=dacc4edb-ad1d-4ecd-b98c-b4b31983b5f8',
  ,
]

const Wrapper = styled.div`
  margin: 1rem 0 0;
`

const StyledLink = styled(Link)`
  display: block;
`

const List = styled.li`
  display: block;
  cursor: pointer;
`

const StyledHeadline = styled(Headline)`
  text-align: center;
`

const StyledText = styled(Text)`
  text-align: center;
  $media.tablet`
    font-size: 1rem;
  `
  $media.laptop`
    font-size: 1.25rem;
  `
`

const TextWrapper = styled.div`
  position: relative;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-bottom: 1rem;
  width: 18rem;
    $media.mobileL`
    width: 21rem;
    `
    $media.tablet`
    width: 26rem;
    `
    $media.laptop`
    width: 27rem;
    `
`
const StyledHorizontalScrollList = styled(HorizontalScrollList)`
  ul 
    padding: 0 1rem 0.5rem 0;
  
`

const ScrollListWrapper = styled.div`
  margin-left: 1rem;
  $media.laptopL`
    margin-left: 0;
  `
`

export const TeaserCard = styled.img`
  width: 100%;
  height: 100%;

  border-radius: 1rem;
  box-shadow: 0.25rem 0.25rem 0 0 $props => props.theme.DirtyWhiteDarker;
  $media.tablet`
    box-shadow: 0.375rem 0.375rem 0 0 $props => props.theme.DirtyWhiteDarker;
  `
  $media.laptop`
    box-shadow: 0.5rem 0.5rem 0 0 $props => props.theme.DirtyWhiteDarker;
  `
`

export function Teaser( tonieboxes : TeaserProps) 
  const [columns, setColumns] = useState(3)
  const [toggleTonieboxModal, setToggleTonieboxModal] = useState(false)
  const [allBoxes, setAllBoxes] = useState<Tonieboxes[]>(tonieboxes)
  const [tunesTeasers, setTunesTeasers] = useState<TunesTeaser[]>([])

  const  i18n  = useTranslation()
  const  t  = useTranslation(['home'])

  function toggleModal() 
    setToggleTonieboxModal(!toggleTonieboxModal)
  

  function tonieboxAdded(toniebox) 
    setAllBoxes([...allBoxes, toniebox])
  

  useEffect(() => 
    function update() 
      const matchTablet = window.matchMedia(
        `(min-width: $variables.screenTabletpx)`
      ).matches
      const matchScreenMobileLarge = window.matchMedia(
        `(min-width: $variables.screenMobileLpx)`
      ).matches

      setColumns(matchTablet ? 2.75 : matchScreenMobileLarge ? 2 : 1.35)
    

    update()

    function checkAndSetTunesTeasers() 
      if (i18n.language === 'de') 
        if (tonieboxes.length === 0) 
          setTunesTeasers(tunesTeasersNoTonieboxesDE)
         else 
          setTunesTeasers(tunesTeasersDE)
        
       else 
        if (tonieboxes.length === 0) 
          setTunesTeasers(tunesTeasersNoTonieboxesEng)
         else 
          setTunesTeasers(tunesTeasersEng)
        
      
    

    checkAndSetTunesTeasers()

    window.addEventListener('resize', update)
    return () => window.removeEventListener('resize', update)
  , [i18n.language, tonieboxes.length])

  return (
    <>
      <Wrapper>
        <TextWrapper>
          <StyledHeadline
            styleTag=columns === 1.25 ? 'h3' : 'h2'
            dataTestId="welcome-message"
          >
            Werde ein <Bello>Ipsum</Bello> der Tonies
          </StyledHeadline>
          <StyledText>
            Bist du bereit für Hörabenteuer? Entdecke jetzt die ganze Vielfalt
            der Tonies.
          </StyledText>
        </TextWrapper>
        <ScrollListWrapper>
          <StyledHorizontalScrollList columns=columns>
            tunesTeasers.map(teaser => 
              return (
                <>
                  teaser.noTonieboxes ? (
                    <List key=teaser.alt onClick=toggleModal>
                      <TeaserCard src=teaser.src alt=teaser.alt />
                    </List>
                  ) : (
                    <StyledLink key=teaser.alt to=teaser.link>
                      <TeaserCard src=teaser.src alt=teaser.alt />
                    </StyledLink>
                  )
                </>
              )
            )
          </StyledHorizontalScrollList>
        </ScrollListWrapper>
      </Wrapper>
      <Modal
        headline=t('add-toniebox-modal:AddTonieboxModalTitle')
        isOpen=toggleTonieboxModal
        onClose=toggleModal
      >
        <AddTonieboxModalContent
          onClose=toggleModal
          onSuccess=tonieboxAdded
        />
      </Modal>
    </>
  )


【问题讨论】:

这个错误是不言自明的。大约有一百万条关于此的帖子。错误出现在您的.map 电话中。最外面的元素(在本例中为片段)需要一个键。就像这样:tunesTeasers.map(teaser =&gt; return ( &lt;React.Fragment key=teaser.src&gt;... - 我假设 src 属性在不同的预告片之间是独一无二的。如果没有,请使用它们的属性之一。如果他们没有,就做一个 您的 TeaserCard 组件是什么样的?看起来您对于 ListStyledLink 组件的每组数据都有一个唯一键。 【参考方案1】:

这绝对是一个数组关键问题,但似乎您在每个数据集(数组)中都有独特的alt 属性。

<StyledHorizontalScrollList columns=columns>
  tunesTeasers.map(teaser => teaser.noTonieboxes ? (
    <List key=teaser.alt onClick=toggleModal>
      <TeaserCard alt=teaser.alt src=teaser.src />
    </List>
  ) : (
    <StyledLink key=teaser.alt to=teaser.link>
      <TeaserCard alt=teaser.alt src=teaser.src />
    </StyledLink>
  )
</StyledHorizontalScrollList>

【讨论】:

【参考方案2】:

可能与tunesTeasers.map 相关。反应键需要位于映射的 outer-most 元素上,在这种情况下为 Fragment。预告链接在集合中似乎是唯一的,但如果不是,您可能需要为元素提供唯一的 id 属性以用作反应键。

tunesTeasers.map(teaser => 
  return (
    <Fragment key=teaser.link>
      teaser.noTonieboxes ? (
        <List key=teaser.alt onClick=toggleModal>
          <TeaserCard src=teaser.src alt=teaser.alt />
        </List>
      ) : (
        <StyledLink key=teaser.alt to=teaser.link>
          <TeaserCard src=teaser.src alt=teaser.alt />
        </StyledLink>
      )
    </Fragment>
  )
)

【讨论】:

是的!非常感谢!!

以上是关于反应:警告列表中的每个孩子都应该有一个唯一的键[重复]的主要内容,如果未能解决你的问题,请参考以下文章

警告:列表中的每个孩子都应该有一个唯一的“关键”道具。反应.js

警告:列表中的每个孩子都应该有一个唯一的“关键”道具。 - 反应JS

反应列表中的每个孩子都应该有一个唯一的“关键”道具。即使钥匙存在

反应警告:数组或迭代器中的每个孩子都应该有一个唯一的“key”道具。检查`App`的渲染方法

警告 - 列表中的每个孩子都应该有一个唯一的“关键”道具

如何修复警告:列表中的每个孩子都应该有一个唯一的“关键”道具