React Top Level API
01 March 2022
Updated: 03 September 2023
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
positionprop
When this is done, we want to render a component that results in the following markup:
1<Wrapper>2 <Count />3 <ItemWrapper>4 <Item name="" position="" />5 </ItemWrapper>6 <ItemWrapper>7 <Item name="" position="" />8 </ItemWrapper>9 <ItemWrapper>10 <Item name="" position="" />11 </ItemWrapper>12</Wrapper>But a consumer can be used like:
1<Wrapper>2 <Item name="">3 <Item name="">4 <Item name="">5</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:
1interface ItemProps {2 name: string3 position?: number4}5
6const Item: React.FC<ItemProps> = ({ name, position }) => (7 <div>8 {name}, {position}9 </div>10)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:
1const 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:
1interface CountProps {2 count: number3}4
5const 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:
1const Wrapper: React.FC = ({ children }) => {2 const count = React.Children.count(children)3
4 return (5 <div>6 <Count count={count} />7 </div>8 )9}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:
1const items = React.Children.map(children, (child) => {2 return <ItemWrapper>{child}</ItemWrapper>3})Based on the above, we can define an ItemWrapper as so:
1const ItemWrapper: React.FC = ({ children }) => (2 <li3 style={{4 backgroundColor: 'lightgrey',5 padding: '10px',6 margin: '20px',7 }}8 >9 {children}10 </li>11)And we can update the Wrapper to make use of React.children.map:
1const Wrapper: React.FC = ({ children }) => {2 const count = React.Children.count(children)3
4 const items = React.Children.map(children, (child) => {5 return <ItemWrapper>{child}</ItemWrapper>6 })7
8 return (9 <div>10 <Count count={count} />11 <ul>{items}</ul>12 </div>13 )14}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:
1const childProps = child.props2const newProps = { ...child.props, position: index }3
4const newChild = React.cloneElement(child, newProps)Integrating this into the React.Children.map function above will result in our Wrapper looking like so:
1const Wrapper: React.FC = ({ children }) => {2 const count = React.Children.count(children)3
4 const items = React.Children.map(children, (child, index) => {5 const childProps = child.props6 const newProps = { ...child.props, position: index }7
8 const newChild = React.cloneElement(child, newProps)9
10 return <ItemWrapper>{newChild}</ItemWrapper>11 })12
13 return (14 <div>15 <Count count={count} />16 <ul>{items}</ul>17 </div>18 )19}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:
1const items = React.Children.map(children, (child, index) => {2 if (!React.isValidElement(child)) return null3
4 const childProps = child.props5 const newProps = { ...child.props, position: index }6
7 const newChild = React.cloneElement(child, newProps)8
9 return <ItemWrapper>{newChild}</ItemWrapper>10})Which results in our Wrapper now being:
1const Wrapper: React.FC = ({ children }) => {2 const count = React.Children.count(children)3 const items = React.Children.map(children, (child, index) => {4 if (!React.isValidElement(child)) return null5
6 const childProps = child.props7 const newProps = { ...child.props, position: index }8
9 const newChild = React.cloneElement(child, newProps)10
11 return <ItemWrapper>{newChild}</ItemWrapper>12 })13
14 return (15 <div>16 <Count count={count} />17 <ul>{items}</ul>18 </div>19 )20}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:
1const App: React.FC = () => {2 return (3 <Wrapper>4 <Item name="Apple" />5 <Item name="Banana" />6 <Item name="Chocolate" />7 </Wrapper>8 )9}And the rendered HTML:
1<div>2 <p>Total: 3</p>3 <ul>4 <li style="background-color: lightgrey; padding: 10px; margin: 20px;">5 <div>Apple, 0</div>6 </li>7 <li style="background-color: lightgrey; padding: 10px; margin: 20px;">8 <div>Banana, 1</div>9 </li>10 <li style="background-color: lightgrey; padding: 10px; margin: 20px;">11 <div>Chocolate, 2</div>12 </li>13 </ul>14</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