反应:警告列表中的每个孩子都应该有一个唯一的键[重复]
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 => return ( <React.Fragment key=teaser.src>...
- 我假设 src
属性在不同的预告片之间是独一无二的。如果没有,请使用它们的属性之一。如果他们没有,就做一个
您的 TeaserCard 组件是什么样的?看起来您对于 List
和 StyledLink
组件的每组数据都有一个唯一键。
【参考方案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
反应列表中的每个孩子都应该有一个唯一的“关键”道具。即使钥匙存在