React 警告:列表中的每个 child 都应该有一个唯一的 "key" 道具,这不是由地图中缺少键引起的
React Warning: Each child in a list should have a unique "key" prop NOT caused by lack of key in map
我目前正在开发 React 18 应用程序。在其中一页上,我收到以下错误:
我知道这种错误通常与 map 函数中缺少唯一键有关,但在我的情况下不是这样,因为如果我使用 React DevTools 检查组件,所有键都是独特的,这导致我认为控制台中的警告具有误导性并且是由其他原因引起的,但我可能错了。
RockfonDropdownFilterResponsive组件代码:
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import React, { PropsWithChildren, useEffect, useRef, useState } from 'react';
import type { ReactElement } from 'react';
import AnimateHeight from 'react-animate-height';
import { useIsMounted } from '../../hooks/useIsMounted';
import { generateFilterRows, generateGroupFilters, generateGroupFiltersMobile } from '../shared/rockfon-filter-factory';
import type { FilterOptions, Group, RockfonProductFilterOrderSettings } from '../shared/types';
import { useOnClickOutside } from '../../hooks/useOnClickOutside';
interface BaseProductFilterProps {
filterTitle: string;
optionsTitle: string;
handleClose: () => void;
handleOpen: () => void;
noFiltersAvailableLabel: string;
selectedOptions: string[];
}
interface ProductFilterProps extends BaseProductFilterProps {
isOpen: boolean;
groups?: Group[];
options: FilterOptions[];
noFiltersAvailableLabel: string;
selectOption: (value: string) => void;
selectGroupOption: (value: string) => void;
selectedOptions: string[];
filterNarrowCondition?: (value: string, minValue?: number, maxValue?: number) => boolean;
applySelections: () => void;
isMobile: boolean;
shrinkingOptions?: (options: FilterOptions[], type: string) => FilterOptions[];
name?: string;
isShrinking?: number;
orderSettings: RockfonProductFilterOrderSettings;
}
interface ProductFilterPresentationProps extends BaseProductFilterProps {
height: number | string;
handleClose: () => void;
handleOpen: () => void;
filterRows: ReactElement[];
isOpen: boolean;
}
const RockfonDropdownFilterResponsive = (props: PropsWithChildren<ProductFilterPresentationProps>): ReactElement => (
<div className="filterDropdownContainer">
<div>
<div
role="listbox"
className={props.isOpen ? 'btn-dropdown is-open' : 'btn-dropdown'}
onClick={props.isOpen ? props.handleClose : props.handleOpen}
tabIndex={0}
>
<span className="filter-name" title={props.filterTitle}>{props.filterTitle}</span>
<svg height="20" width="20" aria-hidden="true">
<path d="M4.516 7.548c.436-.446 1.043-.481 1.576 0L10 11.295l3.908-3.747c.533-.481 1.141-.446 1.574 0 .436.445.408 1.197 0 1.615-.406.418-4.695 4.502-4.695 4.502a1.095 1.095 0 01-1.576 0S4.924 9.581 4.516 9.163s-.436-1.17 0-1.615z" />
</svg>
</div>
<AnimateHeight className="filter" contentClassName="filter-content" height={props.height} duration={300}>
{props.children && <>{props.children} <div className="divider" /></>}
<div className="filter__option-title">{props.optionsTitle}</div>
{props.filterRows.length > 0 && <div className="filter-scrollbar">{props.filterRows}</div>}
{props.filterRows.length === 0 && (
<div className="no-filters">{props.noFiltersAvailableLabel}</div>
)}
</AnimateHeight>
</div>
<div className="filter-item">
<div
role="listbox"
className={props.isOpen ? 'btn-dropdown is-open' : 'btn-dropdown'}
onClick={props.isOpen ? props.handleClose : props.handleOpen}
tabIndex={0}
>
<span className="filter-name" title={props.filterTitle}>
{props.filterTitle}{props.selectedOptions.length > 0 && <sup>({props.selectedOptions.length})</sup>}
</span>
<svg height="20" width="20" aria-hidden="true">
<path d="M4.516 7.548c.436-.446 1.043-.481 1.576 0L10 11.295l3.908-3.747c.533-.481 1.141-.446 1.574 0 .436.445.408 1.197 0 1.615-.406.418-4.695 4.502-4.695 4.502a1.095 1.095 0 01-1.576 0S4.924 9.581 4.516 9.163s-.436-1.17 0-1.615z" />
</svg>
</div>
<AnimateHeight height={props.height} duration={300}>
<div className="filter">
{props.children}
{props.children && <div className="divider" />}
{props.filterRows.length > 0 && (
<div className="filter-scrollbar">
{props.filterRows}
</div>
)}
{props.filterRows.length === 0 && (
<div className="no-filters">{props.noFiltersAvailableLabel}</div>
)}
</div>
</AnimateHeight>
</div>
</div>
)
export const RockfonDropdownFilter = (props: PropsWithChildren<ProductFilterProps>): ReactElement => {
const [height, setHeight] = useState<0 | 'auto'>(0);
const [displayedOptions, setDisplayedOptions] = useState<FilterOptions[]>([]);
const isMounted = useIsMounted();
const [openTabIndex, setOpenTabIndex] = useState<number>(0);
const ref = useRef();
useOnClickOutside(ref, () => {
if (!props.isMobile && props.isOpen) {
const filterContainer = document
.getElementById('filtersDesktopFrm');
const filterContainerDisplay = getComputedStyle(filterContainer).getPropertyValue('display');
if (filterContainerDisplay != 'none') {
props.handleClose();
}
}
});
useEffect(() => {
if (!isMounted) {
return;
}
if (props.isOpen) {
requestAnimationFrame(() => {
setHeight('auto');
});
}
else {
requestAnimationFrame(() => {
setHeight(0);
});
}
}, [props.isOpen]);
const handleFilterRowChange = (value: string) => {
if (isMounted) {
props.selectOption(value);
if (!props.isMobile) {
props.applySelections();
}
}
};
useEffect(() => {
if (props.isShrinking && props.isMobile) {
if (props.shrinkingOptions) {
setDisplayedOptions(props.shrinkingOptions(props.options, props.name));
}
}
}, [props.isShrinking])
useEffect(() => {
if (props.isMobile && !props.shrinkingOptions) {
setDisplayedOptions(props.options);
}
else {
if (props.shrinkingOptions) {
setDisplayedOptions(props.shrinkingOptions(props.options, props.name));
} else {
setDisplayedOptions(props.filterNarrowCondition ? props.options.filter(f => !props.filterNarrowCondition(f.value, f.minValue, f.maxValue)) : props.options);
}
}
}, [props.options, props.filterNarrowCondition]);
const hasGroups = props.groups && props.groups.length > 0;
const groupsOnOptions: string[] = props.options.map((f) => f.group).filter((g) => g) as string[];
const isAnyFilterWithGroup =
hasGroups && groupsOnOptions.some((f) => props.groups!.map((g) => g.value).includes(f));
let filterRows = [];
if (isAnyFilterWithGroup) {
const groupedFilters = props.isMobile
? generateGroupFiltersMobile(props.groups!, displayedOptions, props.selectedOptions, openTabIndex, setOpenTabIndex, handleFilterRowChange, props.orderSettings)
: generateGroupFilters(props.groups!, displayedOptions, props.selectedOptions, handleFilterRowChange, props.orderSettings)
filterRows.push(groupedFilters);
}
else {
filterRows = generateFilterRows(displayedOptions, props.selectedOptions, props.orderSettings, handleFilterRowChange);
}
const presentationProps: ProductFilterPresentationProps = {
selectedOptions: props.selectedOptions,
filterTitle: props.filterTitle,
filterRows,
height,
handleClose: props.handleClose,
handleOpen: props.handleOpen,
optionsTitle: props.optionsTitle,
isOpen: props.isOpen,
noFiltersAvailableLabel: props.noFiltersAvailableLabel
}
return <div ref={ref}>
<RockfonDropdownFilterResponsive {...presentationProps}>{props.children}</RockfonDropdownFilterResponsive>
</div>
}
有没有人知道如何摆脱这个警告,或者我应该如何找到更多关于这个问题的信息?
编辑:使用 generateGroupFilters 和 generateGroupFiltersMobile 函数更新问题。
export const generateGroupFilters = (
groups: Group[],
filterOptions: FilterOptions[],
checkedFilters: string[],
handleFilterRowChange: (value: string) => void,
orderSettings: RockfonProductFilterOrderSettings
): ReactElement => {
const groupRows = sortGroups(groups, orderSettings).map((gr) => {
const groupOptions = filterOptions.filter((f) => f.group === gr.value);
const groupFilterRows = generateFilterRows(groupOptions, checkedFilters, orderSettings, handleFilterRowChange);
return (
<React.Fragment key={`Group${gr.value}`}>
<GroupNameRow key={gr.value} {...gr} />
{groupFilterRows}
</React.Fragment>
);
});
const groupValues = groups.map((gr) => gr.value);
const filterOptionsWithoutGroup = filterOptions.filter((f) => !groupValues.includes(f.group || ''));
const filterRowsWithoutGroup = generateFilterRows(filterOptionsWithoutGroup, checkedFilters, orderSettings, handleFilterRowChange);
return (
<>
{groupRows}
{filterRowsWithoutGroup && filterRowsWithoutGroup.length > 0 && (
<>
<GroupNameRow key="no-group-indicator" {...{ label: 'Others', value: '' }} />
{filterRowsWithoutGroup}
</>
)}
</>
);
};
export const generateGroupFiltersMobile = (
groups: Group[],
filterOptions: FilterOptions[],
checkedFilters: string[],
openTabIndex: number,
setOpenTabIndex: (number) => void,
handleFilterRowChange: (value: string) => void,
orderSettings: RockfonProductFilterOrderSettings
): ReactElement => {
const getHeight = (index: number) => (index === openTabIndex ? 'auto' : 0);
const groupRows = sortGroups(groups, orderSettings).map((gr, index) => {
const groupOptions = filterOptions.filter((f) => f.group === gr.value);
const groupFilterRows = generateFilterRows(groupOptions, checkedFilters, orderSettings, handleFilterRowChange);
const tabIndex = index + 1;
return (
<React.Fragment key={`Group_${gr.value}`}>
<GroupNameRowMobile
key={gr.value}
{...gr}
count={groupOptions.filter((x) => checkedFilters.includes(x.value)).length}
className={tabIndex === openTabIndex ? '' : 'is-open'}
onClick={() => setOpenTabIndex(openTabIndex === tabIndex ? 0 : tabIndex)}
/>
<AnimateHeight height={getHeight(tabIndex)}>{groupFilterRows}</AnimateHeight>
</React.Fragment>
);
});
const groupValues = groups.map((gr) => gr.value);
const otherIndex = groupRows.length + 1;
const filterOptionsWithoutGroup = filterOptions.filter((f) => !groupValues.includes(f.group || ''));
const filterRowsWithoutGroup = generateFilterRows(filterOptionsWithoutGroup, checkedFilters, orderSettings, handleFilterRowChange);
return (
<div key="group_container">
{groupRows}
{filterRowsWithoutGroup && filterRowsWithoutGroup.length > 0 && (
<>
<GroupNameRowMobile
key="no-group-indicator"
{...{ label: 'Others', value: '' }}
onClick={() => setOpenTabIndex(otherIndex)}
/>
<AnimateHeight height={getHeight(otherIndex)}>{filterRowsWithoutGroup}</AnimateHeight>
</>
)}
</div>
);
};
您的 generateGroupFilters
returns 没有 key
的片段:
return (
<>
...
</>
);
您正在将这些片段推入 filterRows
并使用它。
即使数组中的片段也需要键,所以你会得到错误。
这是一个问题示例(很遗憾,Stack Snippets 仅支持 <React.Fragment>...<React.Fragment>
,不支持 <>...</>
,但没有任何区别):
const Example = () => {
const example = [
<React.Fragment>a</React.Fragment>,
<React.Fragment>b</React.Fragment>,
<React.Fragment>c</React.Fragment>,
];
return <div>{example}</div>;
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Example />);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
当您 运行 时,您会收到有关密钥的标准警告。 Here's 在 CodeSandbox 上使用 <>...</>
的实时副本,显示相同的错误。
向返回的片段添加在 filterRows
中唯一的键。
我目前正在开发 React 18 应用程序。在其中一页上,我收到以下错误:
我知道这种错误通常与 map 函数中缺少唯一键有关,但在我的情况下不是这样,因为如果我使用 React DevTools 检查组件,所有键都是独特的,这导致我认为控制台中的警告具有误导性并且是由其他原因引起的,但我可能错了。
RockfonDropdownFilterResponsive组件代码:
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import React, { PropsWithChildren, useEffect, useRef, useState } from 'react';
import type { ReactElement } from 'react';
import AnimateHeight from 'react-animate-height';
import { useIsMounted } from '../../hooks/useIsMounted';
import { generateFilterRows, generateGroupFilters, generateGroupFiltersMobile } from '../shared/rockfon-filter-factory';
import type { FilterOptions, Group, RockfonProductFilterOrderSettings } from '../shared/types';
import { useOnClickOutside } from '../../hooks/useOnClickOutside';
interface BaseProductFilterProps {
filterTitle: string;
optionsTitle: string;
handleClose: () => void;
handleOpen: () => void;
noFiltersAvailableLabel: string;
selectedOptions: string[];
}
interface ProductFilterProps extends BaseProductFilterProps {
isOpen: boolean;
groups?: Group[];
options: FilterOptions[];
noFiltersAvailableLabel: string;
selectOption: (value: string) => void;
selectGroupOption: (value: string) => void;
selectedOptions: string[];
filterNarrowCondition?: (value: string, minValue?: number, maxValue?: number) => boolean;
applySelections: () => void;
isMobile: boolean;
shrinkingOptions?: (options: FilterOptions[], type: string) => FilterOptions[];
name?: string;
isShrinking?: number;
orderSettings: RockfonProductFilterOrderSettings;
}
interface ProductFilterPresentationProps extends BaseProductFilterProps {
height: number | string;
handleClose: () => void;
handleOpen: () => void;
filterRows: ReactElement[];
isOpen: boolean;
}
const RockfonDropdownFilterResponsive = (props: PropsWithChildren<ProductFilterPresentationProps>): ReactElement => (
<div className="filterDropdownContainer">
<div>
<div
role="listbox"
className={props.isOpen ? 'btn-dropdown is-open' : 'btn-dropdown'}
onClick={props.isOpen ? props.handleClose : props.handleOpen}
tabIndex={0}
>
<span className="filter-name" title={props.filterTitle}>{props.filterTitle}</span>
<svg height="20" width="20" aria-hidden="true">
<path d="M4.516 7.548c.436-.446 1.043-.481 1.576 0L10 11.295l3.908-3.747c.533-.481 1.141-.446 1.574 0 .436.445.408 1.197 0 1.615-.406.418-4.695 4.502-4.695 4.502a1.095 1.095 0 01-1.576 0S4.924 9.581 4.516 9.163s-.436-1.17 0-1.615z" />
</svg>
</div>
<AnimateHeight className="filter" contentClassName="filter-content" height={props.height} duration={300}>
{props.children && <>{props.children} <div className="divider" /></>}
<div className="filter__option-title">{props.optionsTitle}</div>
{props.filterRows.length > 0 && <div className="filter-scrollbar">{props.filterRows}</div>}
{props.filterRows.length === 0 && (
<div className="no-filters">{props.noFiltersAvailableLabel}</div>
)}
</AnimateHeight>
</div>
<div className="filter-item">
<div
role="listbox"
className={props.isOpen ? 'btn-dropdown is-open' : 'btn-dropdown'}
onClick={props.isOpen ? props.handleClose : props.handleOpen}
tabIndex={0}
>
<span className="filter-name" title={props.filterTitle}>
{props.filterTitle}{props.selectedOptions.length > 0 && <sup>({props.selectedOptions.length})</sup>}
</span>
<svg height="20" width="20" aria-hidden="true">
<path d="M4.516 7.548c.436-.446 1.043-.481 1.576 0L10 11.295l3.908-3.747c.533-.481 1.141-.446 1.574 0 .436.445.408 1.197 0 1.615-.406.418-4.695 4.502-4.695 4.502a1.095 1.095 0 01-1.576 0S4.924 9.581 4.516 9.163s-.436-1.17 0-1.615z" />
</svg>
</div>
<AnimateHeight height={props.height} duration={300}>
<div className="filter">
{props.children}
{props.children && <div className="divider" />}
{props.filterRows.length > 0 && (
<div className="filter-scrollbar">
{props.filterRows}
</div>
)}
{props.filterRows.length === 0 && (
<div className="no-filters">{props.noFiltersAvailableLabel}</div>
)}
</div>
</AnimateHeight>
</div>
</div>
)
export const RockfonDropdownFilter = (props: PropsWithChildren<ProductFilterProps>): ReactElement => {
const [height, setHeight] = useState<0 | 'auto'>(0);
const [displayedOptions, setDisplayedOptions] = useState<FilterOptions[]>([]);
const isMounted = useIsMounted();
const [openTabIndex, setOpenTabIndex] = useState<number>(0);
const ref = useRef();
useOnClickOutside(ref, () => {
if (!props.isMobile && props.isOpen) {
const filterContainer = document
.getElementById('filtersDesktopFrm');
const filterContainerDisplay = getComputedStyle(filterContainer).getPropertyValue('display');
if (filterContainerDisplay != 'none') {
props.handleClose();
}
}
});
useEffect(() => {
if (!isMounted) {
return;
}
if (props.isOpen) {
requestAnimationFrame(() => {
setHeight('auto');
});
}
else {
requestAnimationFrame(() => {
setHeight(0);
});
}
}, [props.isOpen]);
const handleFilterRowChange = (value: string) => {
if (isMounted) {
props.selectOption(value);
if (!props.isMobile) {
props.applySelections();
}
}
};
useEffect(() => {
if (props.isShrinking && props.isMobile) {
if (props.shrinkingOptions) {
setDisplayedOptions(props.shrinkingOptions(props.options, props.name));
}
}
}, [props.isShrinking])
useEffect(() => {
if (props.isMobile && !props.shrinkingOptions) {
setDisplayedOptions(props.options);
}
else {
if (props.shrinkingOptions) {
setDisplayedOptions(props.shrinkingOptions(props.options, props.name));
} else {
setDisplayedOptions(props.filterNarrowCondition ? props.options.filter(f => !props.filterNarrowCondition(f.value, f.minValue, f.maxValue)) : props.options);
}
}
}, [props.options, props.filterNarrowCondition]);
const hasGroups = props.groups && props.groups.length > 0;
const groupsOnOptions: string[] = props.options.map((f) => f.group).filter((g) => g) as string[];
const isAnyFilterWithGroup =
hasGroups && groupsOnOptions.some((f) => props.groups!.map((g) => g.value).includes(f));
let filterRows = [];
if (isAnyFilterWithGroup) {
const groupedFilters = props.isMobile
? generateGroupFiltersMobile(props.groups!, displayedOptions, props.selectedOptions, openTabIndex, setOpenTabIndex, handleFilterRowChange, props.orderSettings)
: generateGroupFilters(props.groups!, displayedOptions, props.selectedOptions, handleFilterRowChange, props.orderSettings)
filterRows.push(groupedFilters);
}
else {
filterRows = generateFilterRows(displayedOptions, props.selectedOptions, props.orderSettings, handleFilterRowChange);
}
const presentationProps: ProductFilterPresentationProps = {
selectedOptions: props.selectedOptions,
filterTitle: props.filterTitle,
filterRows,
height,
handleClose: props.handleClose,
handleOpen: props.handleOpen,
optionsTitle: props.optionsTitle,
isOpen: props.isOpen,
noFiltersAvailableLabel: props.noFiltersAvailableLabel
}
return <div ref={ref}>
<RockfonDropdownFilterResponsive {...presentationProps}>{props.children}</RockfonDropdownFilterResponsive>
</div>
}
有没有人知道如何摆脱这个警告,或者我应该如何找到更多关于这个问题的信息?
编辑:使用 generateGroupFilters 和 generateGroupFiltersMobile 函数更新问题。
export const generateGroupFilters = (
groups: Group[],
filterOptions: FilterOptions[],
checkedFilters: string[],
handleFilterRowChange: (value: string) => void,
orderSettings: RockfonProductFilterOrderSettings
): ReactElement => {
const groupRows = sortGroups(groups, orderSettings).map((gr) => {
const groupOptions = filterOptions.filter((f) => f.group === gr.value);
const groupFilterRows = generateFilterRows(groupOptions, checkedFilters, orderSettings, handleFilterRowChange);
return (
<React.Fragment key={`Group${gr.value}`}>
<GroupNameRow key={gr.value} {...gr} />
{groupFilterRows}
</React.Fragment>
);
});
const groupValues = groups.map((gr) => gr.value);
const filterOptionsWithoutGroup = filterOptions.filter((f) => !groupValues.includes(f.group || ''));
const filterRowsWithoutGroup = generateFilterRows(filterOptionsWithoutGroup, checkedFilters, orderSettings, handleFilterRowChange);
return (
<>
{groupRows}
{filterRowsWithoutGroup && filterRowsWithoutGroup.length > 0 && (
<>
<GroupNameRow key="no-group-indicator" {...{ label: 'Others', value: '' }} />
{filterRowsWithoutGroup}
</>
)}
</>
);
};
export const generateGroupFiltersMobile = (
groups: Group[],
filterOptions: FilterOptions[],
checkedFilters: string[],
openTabIndex: number,
setOpenTabIndex: (number) => void,
handleFilterRowChange: (value: string) => void,
orderSettings: RockfonProductFilterOrderSettings
): ReactElement => {
const getHeight = (index: number) => (index === openTabIndex ? 'auto' : 0);
const groupRows = sortGroups(groups, orderSettings).map((gr, index) => {
const groupOptions = filterOptions.filter((f) => f.group === gr.value);
const groupFilterRows = generateFilterRows(groupOptions, checkedFilters, orderSettings, handleFilterRowChange);
const tabIndex = index + 1;
return (
<React.Fragment key={`Group_${gr.value}`}>
<GroupNameRowMobile
key={gr.value}
{...gr}
count={groupOptions.filter((x) => checkedFilters.includes(x.value)).length}
className={tabIndex === openTabIndex ? '' : 'is-open'}
onClick={() => setOpenTabIndex(openTabIndex === tabIndex ? 0 : tabIndex)}
/>
<AnimateHeight height={getHeight(tabIndex)}>{groupFilterRows}</AnimateHeight>
</React.Fragment>
);
});
const groupValues = groups.map((gr) => gr.value);
const otherIndex = groupRows.length + 1;
const filterOptionsWithoutGroup = filterOptions.filter((f) => !groupValues.includes(f.group || ''));
const filterRowsWithoutGroup = generateFilterRows(filterOptionsWithoutGroup, checkedFilters, orderSettings, handleFilterRowChange);
return (
<div key="group_container">
{groupRows}
{filterRowsWithoutGroup && filterRowsWithoutGroup.length > 0 && (
<>
<GroupNameRowMobile
key="no-group-indicator"
{...{ label: 'Others', value: '' }}
onClick={() => setOpenTabIndex(otherIndex)}
/>
<AnimateHeight height={getHeight(otherIndex)}>{filterRowsWithoutGroup}</AnimateHeight>
</>
)}
</div>
);
};
您的 generateGroupFilters
returns 没有 key
的片段:
return (
<>
...
</>
);
您正在将这些片段推入 filterRows
并使用它。
即使数组中的片段也需要键,所以你会得到错误。
这是一个问题示例(很遗憾,Stack Snippets 仅支持 <React.Fragment>...<React.Fragment>
,不支持 <>...</>
,但没有任何区别):
const Example = () => {
const example = [
<React.Fragment>a</React.Fragment>,
<React.Fragment>b</React.Fragment>,
<React.Fragment>c</React.Fragment>,
];
return <div>{example}</div>;
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Example />);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
当您 运行 时,您会收到有关密钥的标准警告。 Here's 在 CodeSandbox 上使用 <>...</>
的实时副本,显示相同的错误。
向返回的片段添加在 filterRows
中唯一的键。