React lists without .map
Tags: react, javascript, webdev, tutorial
Published: 2021-08-04 15:44:35

When we are rendering data in React we often grab an array and do a `.map()` to write out our interface.  The inclusion of instructional statements in the JSX markup can start to get unwieldy however and I like to replace too many code constructs with components instead.

I'll show you the component I use and as we examine it, we'll learn how to manipulate JSX Elements at the same time.

## The problem

Take this **broken** code, it not only has a bug that rears its head when we modify the list, it's also complicated:

```js

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

We've got a list of items and we want to render them and manipulate each one.  This will render fine the first time, but click on the Add or remove icon and it will crash.  We aren't using a component in the map and so we can't use hooks.  Try it:

{% codesandbox y5z66 initialpath=/app1 %}

I see a lot of ugly code like this which may well work if there aren't hooks involved, but I don't like it one bit.

In any case, to make our example work we would first extract out the item to be rendered, which will make our code easier to reason with and create a boundary for the React Hooks so that they no longer fail.

```js

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

Once we have this we update our app to use it:

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

```

This is much better, but it's still a bit of a mess, our key structure is going to create re-renders we don't need when items are added or removed and we still have to take the cognitive load of the `{` and the `render.map` etc.

It would be nicer to write it like this:

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

This would need to have the RenderItem repeated for each item in the list.

## A solution

Ok so let's write a `Repeat` component that does what we like.

The first thing to know is that when we write `const something = <RenderItem remove={remove}/>` we get back an object that looks like: `{type: RenderItem, props: {remove: remove}}`.  With this information we can render that item with additional props like this:

```js

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

```

Let's use that to make a Repeat component:

```js
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 }}
            />
        )
    })
}
```
We make use an item prop for the thing to render and default it to the children of the Repeat component.  Then we run over this list. For each item in the list we append an `index` and an `item` prop based on the parameters passed by the `.map()`

This is fine, but perhaps it would be nicer to return "something" if we don't specify `children` or `item`.  We can do that by making a Simple component and use that as the fall back rather than `undefined`.

```js
function Simple({ item }) {
    return <div>{typeof item === "object" ? JSON.stringify(item) : item}</div>
}
```

This function does have a problem, it's not specifying a key.  So firstly lets create a default key function that uses a `WeakMap` to create a unique key for list items.

```js

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

This function creates a unique numeric key for each object type of item it encounters, otherwise it returns the item.  We can enhance our Repeat function to take a key function to extract a key from the current item, or use this generic one as a default:

```js
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 }}
            />
        )
    })
}
```

Maybe the final step is to allow some other prop apart from "item" to be used for the inner component.  That's pretty easy...

```js
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 }}
            />
        )
    })
}
```

The end result is fully functional and a lot easier to reason with than versions that use `.map()` - at least to my mind :)

{% codesandbox y5z66 initialpath=/app4 %}

Here's all the code from the article.

{% codesandbox y5z66 view=editor %}

-

## Addendum:

In answer to a couple of the points made in the comments, I thought I'd just optimise `Repeat` to use less memory and allocations that the `.map()` version. I also removed the `.map()` inside so I'm not "hiding" it :) TBH I don't think this is necessary as there need to be more changes to the application logic if the lists are super long and Garbage Collection is pretty powerful anyhow (lets face it those .maps are copying arrays that this new version isn't).

```js
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
}
```

One complaint about this version could be that it holds structures for list items that are no longer seen while the component is mounted. Removing those would be possible but seems like overkill and if you're that worried about allocations then it's a trade off.  The natural `.map()` is creating arrays and sub items every time in any case - so now if that's an issue, this version is a pattern to avoid it.

{% codesandbox n4dpo initialpath=/KFkw3Alev825ViCzw13UD/embed %}