不使用 .map 的 React 列表

2025-06-07

不使用 .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))
    }
}
Enter fullscreen mode Exit fullscreen mode

我们有一个项目列表,需要渲染并操作每个项目。第一次渲染时没问题,但点击“添加或删除”图标就会崩溃。我们没有在地图中使用组件,所以无法使用钩子。试试看:

我看到很多像这样的丑陋代码,如果没有钩子的话它们可能会很好地工作,但我一点也不喜欢它。

无论如何,为了使我们的示例起作用,我们首先要提取要渲染的项目,这将使我们的代码更容易推理,并为 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>
    )
}
Enter fullscreen mode Exit fullscreen mode

一旦我们有了这个,我们就会更新我们的应用程序来使用它:

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))
    }
}

Enter fullscreen mode Exit fullscreen mode

这要好得多,但仍然有点混乱,当添加或删除项目时,我们的关键结构将创建我们不需要的重新渲染,并且我们仍然需要承担等等的认知{负荷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))
    }
}
Enter fullscreen mode Exit fullscreen mode

这需要对列表中的每个项目重复 RenderItem。

解决方案

好的,让我们编写一个Repeat可以做我们喜欢的事情的组件。

首先要知道的是,当我们写入时,const something = <RenderItem remove={remove}/>我们会得到一个如下所示的对象:{type: RenderItem, props: {remove: remove}}。有了这些信息,我们可以使用如下附加道具来渲染该项目:


    const template = <RenderItem remove={remove}/>
    return <template.type {...template.props} something="else"/>

Enter fullscreen mode Exit fullscreen mode

让我们使用它来制作一个重复组件:

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 }}
            />
        )
    })
}
Enter fullscreen mode Exit fullscreen mode

我们为要渲染的元素使用 item prop,并将其默认设置为 Repeat 组件的子元素。然后,我们遍历这个列表。对于列表中的每个元素,我们根据传入的参数添加一个indexand属性。item.map()

children这很好,但如果我们没有指定或 ,返回“something”也许会更好item。我们可以创建一个 Simple 组件,并将其作为后备,而不是undefined

function Simple({ item }) {
    return <div>{typeof item === "object" ? JSON.stringify(item) : item}</div>
}
Enter fullscreen mode Exit fullscreen mode

这个函数确实有问题,它没有指定键。所以首先让我们创建一个默认键函数,使用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
    }
}
Enter fullscreen mode Exit fullscreen mode

此函数会为遇到的每个对象类型的项创建一个唯一的数字键,否则返回该项。我们可以增强 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 }}
            />
        )
    })
}
Enter fullscreen mode Exit fullscreen mode

也许最后一步是允许除了“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 }}
            />
        )
    })
}
Enter fullscreen mode Exit fullscreen mode

最终结果是功能齐全的,并且比使用版本更容易推理.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
}
Enter fullscreen mode Exit fullscreen mode

关于此版本的一个抱怨是,它保存了在组件挂载后不再可见的列表项结构。移除这些结构是可能的,但似乎有点矫枉过正,如果你真的担心分配问题,那么这是一个权衡。.map()无论如何,自然的做法是每次都创建数组和子项——所以如果这是一个问题,此版本可以作为一种避免这种情况的模式。

文章来源:https://dev.to/miketalbot/react-lists-without-map-25d5
PREV
十年过去了,服务器发送事件仍然未能达到生产环境的要求。这对我来说是一个教训,对你来说也是一个警告!
NEXT
你好!感谢你在 DEV 的第一个月的疯狂体验 :)