Skip to main content

Level 7: Rendering a Dynamic Amount of Elements

Introduction

In the previous level we have seen a possibility to only show a component if a certain condition is met. But what if we need some more flexibility on which items are displayed, by using a function such as map to render each item of a dataset as a component. And what if the items or the order of the items in the dataset changes? It becomes clear that the current way to identify items just with the order in which they have been added will no longer work after a change in the dataset. That's why we need a more advanced way to identify our items, even if the underlying dataset has changed. For that, we look at the possibility to define a key for elements, which will be necessary to solve problems like the one described before.

Goal

Let's look at the demo we have set up for this level. We try to recreate a situation similar to the one described in the introduction.

We have two arrays of objects, primary and alternate, which represent our changing dataset. The objects in the dataset contain two properties key and name. Additionally, we have a button, that changes the state of toggle on every click. Based on the value of toggle, the value of mapped is assigned: If toggle is true, we create a TextField component for every item in primary, else we create a TextField for every item in alternate. In the return statement of the function, we spread all items of mapped, so they will be added as children to the div wrapping the items.

You can see the source code of this demo below.

index.js

import { createElement } from '../../lib/js/fiber.js'
import { useState } from '../../lib/js/hooks.js'
import { render } from '../../lib/js/render.js'
import { parseSjdon } from '../../lib/js/sjdon.js'


const App = () => {
const [toggle, setToggle] = useState(true)

const primary = [
{ key: 'first', name: 'First (primary)' },
{ key: 'second', name: 'Second (primary)' },
{ key: 'third', name: 'Third (primary)' },
{ key: 'fourth', name: 'Fourth (primary)' },
{ key: 'fifth', name: 'Fifth (primary)' },
{ key: 'sixth', name: 'Sixth (primary)' },
]
const alternate = [
{ key: 'third', name: 'Third (alternate)' },
{ key: 'fourth', name: 'Fourth (alternate)' },
{ key: 'second', name: 'Second (alternate)' },
]

const mapped = toggle
? primary.map((item, idx) => [TextField, { name: item.name, key: item.key }])
: alternate.map((item, idx) => [TextField, { name: item.name, key: item.key }])

return [
'div',
['button', { onclick: () => setToggle(!toggle) }, `Toggled:${toggle}`],
...mapped,
]
}

const TextField = props => {
const [text, setText] = useState(props?.initialValue ?? '')
return [
'div',
['label', `Key: ${props?.key}, Name: ${props?.name} `,
[
'input',
{
value: text,
oninput: event => setText(event.target.value),
},
],
],
['p', 'Value: ', ['strong', text]],
]
}

render(
parseSjdon([App], createElement),
document.getElementById('app')
)

If we render this app and enter some values into the text fields and then click the toggle button, we realize that some text fields will disappear, some change their position and others are added. You should understand what changes happen when you compare primary and alternate in the example above. If we think back to the previous level, you might realize that something like this would not have been possible using the conditional rendering we've looked at so far, as SuiWeb now seems to recognize that something is the same item, even though the order has changed. The reason why this is possible now, is that we assigned an explicit key to our items. You can see inside the assignment of mapped, where we create a TextField for every item, that we set the property key of the TextField component to the value of item.key.

Implementation in mapChildren

Below, we can see an excerpt of the mapChildren function, at which we already looked in previous levels. The highlighted lines show that we insert a child fiber with the key e-${childProps?.key} into the childrenMap on its parent fiber, in case childProps contains the property key. This is the case in the example of this level, as we passed a property key in the props of the component. Otherwise, the default key d-${defaultKey++} would be used, what we have already seen in the previous level.

const childrenMap: Map<string, Fiber> = new Map()
let defaultKey = 0
children.forEach(child => {
const childProps = isFunctionalFiber(child) ? child.functionProps : child.props
// Keys are prefixed e-(xplicit) or d-(efault), so a custom key can never
// accidentally match a generated key.
const key = childProps?.key ? `e-${childProps?.key}` : `d-${defaultKey++}`
childrenMap.set(key, child)
})

Note that by prefixing our keys with either e (for explicit) or d (for default), we make sure that we never accidentally assign an explicit key that is identical to a default key.

As we now have a unique key to differentiate all the sibling elements (in the example the TextFields), the function expandChildFibers, which we looked at in the previous levels, does not have any problem finding the correct child elements in the previousVersion, even if the order of the children has completely changed. By setting the key manually, the order is now completely irrelevant (in contrast to default keys, where the keys are created incrementally in the order of the children).

Review

This has already been it for this level. As you have seen, assigning explicit keys internally works almost the same as generating a default key. The only difference is that if we want to use an explicit key, we need to pass it via the props of the component.

With this level, you should also understand why it's required in some cases to assign an explicit key, while we can omit it in other cases, because we can do some internal tricks, such as using placeholder fibers.

By the way: In React, about the same rules apply on when a key can be generated automatically, and when one needs to be set explicitly.

Now, we have completed basically all the functionality of SuiWeb. In the next level, we are going to wrap everything up, by looking at the full chain of function calls that occur if we render an app with SuiWeb.