不使用 .map 的 React 列表
在 React 中渲染数据时,我们经常会获取一个数组,然后通过.map()
JSX 标记来写出界面。然而,在 JSX 标记中包含指令语句可能会变得笨重,所以我更喜欢用组件来代替过多的代码结构。
我将向您展示我使用的组件,并且在检查它时,我们将同时学习如何操作 JSX 元素。
问题
以这个损坏的代码为例,它不仅在我们修改列表时出现错误,而且还很复杂:
function App1() {
const [render, setRender] = useState(items)
return (
<Box>
<List className="App">
{/* WRITE THE LIST TO THE UI */}
{render.map((item, index) => {
const [on, setOn] = useState(item.on)
return (
<ListItem key={index + item.name}>
<ListItemText primary={item.name} />
<ListItemSecondaryAction>
<Box display="flex">
<Box>
<Switch
checked={on}
onChange={() => setOn((on) => !on)}
/>
</Box>
<Box ml={1}>
<IconButton
color="secondary"
onClick={() => remove(item)}
>
<MdClear />
</IconButton>
</Box>
</Box>
</ListItemSecondaryAction>
</ListItem>
)
})}
</List>
<Button variant="contained" color="primary" onClick={add}>
Add
</Button>
</Box>
)
function add() {
setRender((items) => [
{ name: "Made up at " + Date.now(), on: false },
...items
])
}
function remove(item) {
setRender((items) => items.filter((i) => i !== item))
}
}
我们有一个项目列表,需要渲染并操作每个项目。第一次渲染时没问题,但点击“添加或删除”图标就会崩溃。我们没有在地图中使用组件,所以无法使用钩子。试试看:
我看到很多像这样的丑陋代码,如果没有钩子的话它们可能会很好地工作,但我一点也不喜欢它。
无论如何,为了使我们的示例起作用,我们首先要提取要渲染的项目,这将使我们的代码更容易推理,并为 React Hooks 创建一个边界,以便它们不再失败。
function RenderItem({ item, remove }) {
const [on, setOn] = useState(item.on)
return (
<ListItem>
<ListItemText primary={item.name} />
<ListItemSecondaryAction>
<Box display="flex">
<Box>
<Switch
checked={on}
onChange={() => setOn((on) => !on)}
/>
</Box>
<Box ml={1}>
<IconButton
color="secondary"
onClick={() => remove(item)}
>
<MdClear />
</IconButton>
</Box>
</Box>
</ListItemSecondaryAction>
</ListItem>
)
}
一旦我们有了这个,我们就会更新我们的应用程序来使用它:
function App2() {
const [render, setRender] = useState(items)
return (
<Box>
<List className="App">
{render.map((item, index) => (
<RenderItem
remove={remove}
key={item.name + index}
item={item}
/>
))}
</List>
<Button variant="contained" color="primary" onClick={add}>
Add
</Button>
</Box>
)
function add() {
setRender((items) => [
{ name: "Made up at " + Date.now(), on: false },
...items
])
}
function remove(item) {
setRender((items) => items.filter((i) => i !== item))
}
}
这要好得多,但仍然有点混乱,当添加或删除项目时,我们的关键结构将创建我们不需要的重新渲染,并且我们仍然需要承担等等的认知{
负荷render.map
。
这样写会更好:
function App4() {
const [render, setRender] = useState(items)
return (
<Box>
<List className="App">
<Repeat list={render}>
<RenderItem remove={remove} />
</Repeat>
</List>
<Button variant="contained" color="primary" onClick={add}>
Add
</Button>
</Box>
)
function add() {
setRender((items) => [
{ name: "Made up at " + Date.now(), on: false },
...items
])
}
function remove(item) {
setRender((items) => items.filter((i) => i !== item))
}
}
这需要对列表中的每个项目重复 RenderItem。
解决方案
好的,让我们编写一个Repeat
可以做我们喜欢的事情的组件。
首先要知道的是,当我们写入时,const something = <RenderItem remove={remove}/>
我们会得到一个如下所示的对象:{type: RenderItem, props: {remove: remove}}
。有了这些信息,我们可以使用如下附加道具来渲染该项目:
const template = <RenderItem remove={remove}/>
return <template.type {...template.props} something="else"/>
让我们使用它来制作一个重复组件:
function Repeat({
list,
children,
item = children.type ? children : undefined,
}) {
if(!item) return
return list.map((iterated, index) => {
return (
<item.type
{...{ ...item.props, item: iterated, index }}
/>
)
})
}
我们为要渲染的元素使用 item prop,并将其默认设置为 Repeat 组件的子元素。然后,我们遍历这个列表。对于列表中的每个元素,我们根据传入的参数添加一个index
and属性。item
.map()
children
这很好,但如果我们没有指定或 ,返回“something”也许会更好item
。我们可以创建一个 Simple 组件,并将其作为后备,而不是undefined
。
function Simple({ item }) {
return <div>{typeof item === "object" ? JSON.stringify(item) : item}</div>
}
这个函数确实有问题,它没有指定键。所以首先让我们创建一个默认键函数,使用WeakMap
为列表项创建唯一的键。
const keys = new WeakMap()
let repeatId = 0
function getKey(item) {
if (typeof item === "object") {
const key = keys.get(item) ?? repeatId++
keys.set(item, key)
return key
} else {
return item
}
}
此函数会为遇到的每个对象类型的项创建一个唯一的数字键,否则返回该项。我们可以增强 Repeat 函数,使其接受一个 key 函数从当前项中提取键,或者使用这个通用函数作为默认值:
function Repeat({
list,
children,
item = children.type ? children : <Simple />,
keyFn = getKey
}) {
return list.map((iterated, index) => {
return (
<item.type
key={keyFn(iterated)}
{...{ ...item.props, item: iterated, index }}
/>
)
})
}
也许最后一步是允许除了“item”之外的其他 prop 用于内部组件。这很简单……
function Repeat({
list,
children,
item = children.type ? children : <Simple />,
pass = "item", // Take the name for the prop
keyFn = getKey
}) {
return list.map((iterated, index) => {
return (
<item.type
key={keyFn(iterated)}
// Use the passed in name
{...{ ...item.props, [pass]: iterated, index }}
/>
)
})
}
最终结果是功能齐全的,并且比使用版本更容易推理.map()
- 至少在我看来:)
这是文章中的所有代码。
-
附录:
针对评论中提到的几个问题,我原本打算进行优化,让它Repeat
比之前的版本使用更少的内存和分配.map()
。我还删除了.map()
内部函数,这样就不会“隐藏”它了 :) 老实说,我觉得这没必要,因为如果列表很长,而且垃圾回收功能已经很强大了(说实话,那些 .map 函数复制的是数组,而新版本没有),那么应用程序逻辑就需要做更多修改了。
function Repeat({
list,
children,
item = children.type ? children : <Simple />,
pass = "item",
keyFn = getKey
}) {
const [keys] = useState({})
const [output] = useState([])
let index = 0
for (let iterated of list) {
let key = keyFn(iterated) ?? index
output[index] = keys[key] = keys[key] || {
...item,
key,
props: { ...item.props, [pass]: iterated }
}
output[index].props.index = index
index++
}
output.length = index
return output
}
关于此版本的一个抱怨是,它保存了在组件挂载后不再可见的列表项结构。移除这些结构是可能的,但似乎有点矫枉过正,如果你真的担心分配问题,那么这是一个权衡。.map()
无论如何,自然的做法是每次都创建数组和子项——所以如果这是一个问题,此版本可以作为一种避免这种情况的模式。