React Top Level API
01 March 2022
For a reference on the React Top-Level API you can take a look at the React Docs
Introduction
React allows us to do lots of different things using concepts like composition and higher order components. Most of the time these methods are good enough for us to do what we want, however there are some cases where these methods can prove to be insufficient such as when building complex library components or components that need to allow for dynamic composition or do runtime modification of things like child component props, etc.
For the above purposes we can make use of the React Top-Level API. For the purpose of this writeup I'll be making use of the parts of this API that allow us to modify a component's children and modify their props as well as how they're rendered
Our Goal
For the purpose of this doc I'll be using the React API to get to do the following:
- Show the count of children (
Items
) passed to a component (Wrapper
) - Render each child in a sub-wrapper
ItemWrapper
- Modify the props of the children by adding a
position
prop
When this is done, we want to render a component that results in the following markup:
<Wrapper>
<Count />
<ItemWrapper>
<Item name="" position=""/>
</ItemWrapper>
<ItemWrapper>
<Item name="" position=""/>
</ItemWrapper>
<ItemWrapper>
<Item name="" position=""/>
</ItemWrapper>
</Wrapper>
But a consumer can be used like:
<Wrapper>
<Item name="">
<Item name="">
<Item name="">
</Wrapper>
Using React.Children
to work with a component's children
The React.Children
API (see docs) provides us with some utilities for traversing the children passed to a component
Before we can do any of the following, we need to define the structure of an item. Our Item
component is defined as follows:
interface ItemProps { name: string; position?: number; }
const Item: React.FC<ItemProps> = ({ name, position }) =>
<div>{name}, {position}</div>
Use React.Children.count
to get the count
The React.Children.count
function counts the number of child nodes passed to a React component,
we can use it like so:
const count = React.Children.count(children)
For our example, let's start off by creating a Count
component that simply takes a count
prop and displays some text:
interface CountProps { count: number; }
const Count: React.FC<CountProps> = ({ count }) => <p>Total: {count}</p>
Next, we can define our Wrapper
which will take children
and pass the count
to our Count
component:
const Wrapper: React.FC = ({ children }) => {
const count = React.Children.count(children)
return <div>
<Count count={count} />
</div>
}
Use React.Children.map
to wrap each child
Next, the React.Children.map
function allows us to map over the children of an element and do stuff with it, for example:
const items = React.Children.map(children, (child) => {
return <ItemWrapper>{child}</ItemWrapper>
})
Based on the above, we can define an ItemWrapper
as so:
const ItemWrapper: React.FC = ({ children }) =>
<li style= >{children}</li>
And we can update the Wrapper
to make use of React.children.map
:
const Wrapper: React.FC = ({ children }) => {
const count = React.Children.count(children)
const items = React.Children.map(children, (child) => {
return <ItemWrapper>{child}</ItemWrapper>
})
return <div>
<Count count={count} />
<ul>{items}</ul>
</div>
}
Use React.cloneElement
to change child props
Lastly, we want to append a position
prop to the Item
. To do this we can make use of the React.cloneElement
function which allows us to clone an element and modify the props of it. Using this function looks like so:
const childProps = child.props
const newProps = {...child.props, position: index}
const newChild = React.cloneElement(child, newProps)
Integrating this into the React.Children.map
function above will result in our Wrapper
looking like so:
const Wrapper: React.FC = ({ children }) => {
const count = React.Children.count(children)
const items = React.Children.map(children, (child, index) => {
const childProps = child.props
const newProps = {...child.props, position: index}
const newChild = React.cloneElement(child, newProps)
return <ItemWrapper>{newChild}</ItemWrapper>
})
return <div>
<Count count={count} />
<ul>{items}</ul>
</div>
}
Use React.isValidElement
We've completed most of what's needed, however if for some reason our child
is not a valid react element our component may still crash. To get around this we can use the React.isValidElement
function
We can update our map
function above to return null
if the element is not value:
const items = React.Children.map(children, (child, index) => {
if (!React.isValidElement(child)) return null
const childProps = child.props
const newProps = {...child.props, position: index}
const newChild = React.cloneElement(child, newProps)
return <ItemWrapper>{newChild}</ItemWrapper>
})
Which results in our Wrapper
now being:
const Wrapper: React.FC = ({ children }) => {
const count = React.Children.count(children)
const items = React.Children.map(children, (child, index) => {
if (!React.isValidElement(child)) return null
const childProps = child.props
const newProps = {...child.props, position: index}
const newChild = React.cloneElement(child, newProps)
return <ItemWrapper>{newChild}</ItemWrapper>
})
return <div>
<Count count={count} />
<ul>{items}</ul>
</div>
}
The Result
Lastly, we'll render the above using the App
component, the API for the above components should be composable as we outlined initially. The App
component will now look like so:
const App: React.FC = () => {
return (
<Wrapper>
<Item name="Apple" />
<Item name="Banana" />
<Item name="Chocolate" />
</Wrapper>
)
}
And the rendered HTML:
<div>
<p>Total: 3</p>
<ul>
<li style="background-color: lightgrey; padding: 10px; margin: 20px;">
<div>Apple, 0</div>
</li>
<li style="background-color: lightgrey; padding: 10px; margin: 20px;">
<div>Banana, 1</div>
</li>
<li style="background-color: lightgrey; padding: 10px; margin: 20px;">
<div>Chocolate, 2</div>
</li>
</ul>
</div>
Total: 3
-
Apple, 0
-
Banana, 1
-
Chocolate, 2
And lastly, if you'd like to interact with the code from this sample you can see it in this Repl