React lists without .map

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 co…


This content originally appeared on DEV Community and was authored by Mike Talbot

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:


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:

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.


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:

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:

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:


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

Let's use that to make a Repeat component:

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.

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.

const keys = new WeakMap()
let repeatId = 0
function getKey(item) {
    const key = keys.get(item) ?? repeatId++
    keys.set(item, key)
    return key
}

This function creates a unique numeric key for each item it encounters. 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:

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...

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

Here's all the code from the article.


This content originally appeared on DEV Community and was authored by Mike Talbot


Print Share Comment Cite Upload Translate Updates
APA

Mike Talbot | Sciencx (2021-08-04T15:44:35+00:00) React lists without .map. Retrieved from https://www.scien.cx/2021/08/04/react-lists-without-map/

MLA
" » React lists without .map." Mike Talbot | Sciencx - Wednesday August 4, 2021, https://www.scien.cx/2021/08/04/react-lists-without-map/
HARVARD
Mike Talbot | Sciencx Wednesday August 4, 2021 » React lists without .map., viewed ,<https://www.scien.cx/2021/08/04/react-lists-without-map/>
VANCOUVER
Mike Talbot | Sciencx - » React lists without .map. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2021/08/04/react-lists-without-map/
CHICAGO
" » React lists without .map." Mike Talbot | Sciencx - Accessed . https://www.scien.cx/2021/08/04/react-lists-without-map/
IEEE
" » React lists without .map." Mike Talbot | Sciencx [Online]. Available: https://www.scien.cx/2021/08/04/react-lists-without-map/. [Accessed: ]
rf:citation
» React lists without .map | Mike Talbot | Sciencx | https://www.scien.cx/2021/08/04/react-lists-without-map/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.