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

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/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.