每个与React打交道的人都知道这个警告。**警告。列表中的每个孩子都应该有一个独特的 "key "道具。**它显示在你的浏览器的开发工具中,是你在React职业生涯中很早就遇到的警告之一。下面的列表组件会出现这个警告。
const list = ['Learn React', 'Learn GraphQL'];
const ListWithoutKey = () => (
<div>
<ul>
{list.map((item) => (
<li>{item}</li>
))}
</ul>
</div>
);
该警告说我们只需要给我们的每个列表项元素添加一个key属性。由于我们使用的是内置的JavaScript数组映射方法,我们可以访问列表中每个渲染项的索引。这应该可以解决这个问题,不是吗?
const ListWithoutKey = () => (
<div>
<ul>
{list.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
的确,警告消失了,我们现在应该没事了。**但是要小心。**使用数组项的索引并不是最好的解决方法。React要求键值不是没有原因的。在幕后,React使用key属性将渲染的元素与它在渲染的列表中的位置联系起来。基本上它是列表元素中项目元素的标识符。
例如,在重新排序列表的情况下,使用索引的解决方案会失败,因为索引会保持不变,但真正的项目已经改变了它的位置。如果我们将我们的数组颠倒过来,'Learn React' 条目将有索引2 ,'Learn GraphQL' 条目将有索引1 。这与实际呈现的项目不再匹配,从而导致问题。
当我在网上搜索这个问题时,很难找到一个能说明这个问题的真实场景。大多数教程只解释了如何解决这个问题,但没有解释在最坏情况下会发生什么。这就是为什么我想出了下面这个例子,它非常适合作为一个简短的演练来展示这个问题。
const initialList = ['Learn React', 'Learn GraphQL'];
const ListWithUnstableIndex = () => {
const [list, setList] = React.useState(initialList);
const handleClick = event => {
setList(list.slice().reverse());
};
return (
<div>
<ul>
{list.map((item, index) => (
<li key={index}>
<label>
{item}
</label>
</li>
))}
</ul>
<button type="button" onClick={handleClick}>
Reverse List
</button>
</div>
);
};
这个例子展示了同一个列表,但这次是用React Hooks作为状态来管理。新的按钮元素反转了我们的列表,并将其作为状态存储。如果你试试这个例子,一切都能正常工作,而且看起来很好。这个错误一直隐藏着,因为我们在这里没有渲染很多东西。然而,如果我们在渲染的列表项中添加另一个不受控制的元素,我们可以看到错误的发生。
const initialList = ['Learn React', 'Learn GraphQL'];
const ListWithUnstableIndex = () => {
const [list, setList] = React.useState(initialList);
const handleClick = event => {
setList(list.slice().reverse());
};
return (
<div>
<ul>
{list.map((item, index) => (
<li key={index}>
<label>
<input type="checkbox" />
{item}
</label>
</li>
))}
</ul>
<button type="button" onClick={handleClick}>
Reverse List
</button>
</div>
);
};
这个复选框--因为它是一个不受控制的元素,为了演示这里发生的事情--管理着它自己的内部状态。如果你用复选框检查两个项目中的第一个,然后用按钮把它们颠倒过来,你会注意到复选框被呈现在同一个地方,而列表项目的顺序却发生了变化。
// the initially rendered list of items
[x] Learn React
[ ] Learn GraphQL
// becomes this after the reverse button click
[x] Learn GraphQL
[ ] Learn React
现在,这个缺陷--之前并不明显--在我们的浏览器中被揭开了。问题在于,我们使用数组中每个项目的索引作为关键属性。但是每次渲染时,即使我们把列表倒过来,索引也保持不变。
// the initially rendered list of items
[x] Learn React (index = 1)
[ ] Learn GraphQL (index = 2)
// becomes this after the reverse button click
[x] Learn GraphQL (index = 1)
[ ] Learn React (index = 2)
这就是为什么重新排序的元素仍然被分配给相同的key属性,而这个key属性是React的标识符。React不会改变复选框元素,因为它认为渲染的项目仍然有相同的顺序。你可以通过使用稳定的标识符来解决这个问题,为你渲染的列表项使用稳定的标识符。
const initialList = [
{ id: 'a', name: 'Learn React' },
{ id: 'b', name: 'Learn GraphQL' },
];
const ListWithStableIndex = () => {
const [list, setList] = React.useState(initialList);
const handleClick = event => {
setList(list.slice().reverse());
};
return (
<div>
<ul>
{list.map(item => (
<li key={item.id}>
<label>
<input type="checkbox" />
{item.name}
</label>
</li>
))}
</ul>
<button type="button" onClick={handleClick}>
Reverse List
</button>
</div>
);
};
现在,随着列表的每一次重新排序,关键属性保持不变,因为id是连接到列表中的实际项目:
// the initially rendered list of items
[x] Learn React (id = a)
[ ] Learn GraphQL (id = b)
// becomes this after the reverse button click
[ ] Learn GraphQL (id = b)
[x] Learn React (id = a)
这就是它所需要的,在React中没有任何错误地渲染列表。在这种情况下,我们使用了一个编造的标识符,但它也可以用每个项目的name 属性作为键;只要它们是唯一的名字,不能被改变。
无论如何,仍然值得注意的是,只要你的列表的顺序和大小没有改变,使用索引就可以了。那么每个项目在列表中的位置就不会改变--它和索引一样稳定--因此,使用索引是可以的。这个演示的例子可以在GitHub仓库的其他React列表组件例子中找到。