Three React APIs That Make Components More Flexible

JSX makes React components pleasant to write, but TypeScript makes them pleasant to use. A few lesser-known React APIs can push that usability much further, letting you build components that accept arbitrary elements, update state safely, and extend native HTML elements without reimplementing them.

Cloning Elements With React.cloneElement

React.cloneElement clones an element while letting you merge new props into it — modifying, overriding, or even replacing its children. This is the modern replacement for the deprecated cloneWithProps.

function cloneElement( 
   element: ReactElement, 
   props?: HTMLAttributes, 
   ...children: ReactNode[]): ReactElement

Consider a TabBar component. A naive approach passes in a data structure with titles and URLs:

function App() {
 return (
   <Tabbar links={[
     {title: 'First', url: '/first'},
     {title: 'Second', url: '/second'}]
   } />
 )
}

That works, but what if a consumer needs button elements instead of links? You would start adding props for every possible variation, and it quickly becomes unwieldy.

A better approach accepts ReactNode props instead — a type covering anything React can render, from JSX elements to strings and null:

export interface ITabbarProps {
 links: ReactNode[]
}

Consumers now pass elements directly, and the component renders them:

function Tabbar(props: ITabbarProps) {
 return (
   <>
     {props.links.map((e, i) =>
       e // simply return the element itself
     )}
   </>
 )
}

But this loses key props for lists, and there is no way to inject styling. That is where cloneElement comes in, alongside React.isValidElement for runtime validation:

function Tabbar(props: ITabbarProps) {
 return (
   <>
     {props.links.map((e, i) =>
       isValidElement(e) && cloneElement(e, {key: `${i}`, className: 'bold'})
     )}
   </>
 )
}

Now each element receives a key and a className automatically:

function App() {
 return (
   <Tabbar links={[
     <a href='/first'>First</a>,
     <button type='button'>Second</button>
   ]} />
 )
}

Because cloneElement can override any prop, you can also set custom handlers like onClick on whatever element type the consumer supplies. Accepting React elements as props is a powerful way to keep component APIs open-ended.

The Setter Form of useState

The useState hook is a standard way to add state, but the returned value can go stale in asynchronous callbacks and loops. Closures capture the value at render time, and without a new function scope, the variable may be dereferenced long after its original context:

setTimeout(() => {
 setMyValue(newVal) // this will not work
}, 1000)

Passing a function to the setter fixes this. React invokes that function with the current state as its argument, so updates always operate on fresh values:

setTimeout(() => {
 setMyValue((currentVal) => {
   return newVal
 })
}, 1000)

This functional-update form is also useful whenever you need to react to the current value, not just the value captured at render time.

Inline Functions in JSX

JSX allows anonymous functions that return JSX, as long as they are wrapped in parentheses. This is handy for inline logic beyond what a simple .map() can express:

function App() {
  return (
    <>
     {(() => {
       const darkMode = isDarkMode()
       if (darkMode) {
         return (
           <div className='dark-mode'></div>
         )
       } else {
         return (
           <div className='light-mode'></div>
         ) // we can declare JSX anywhere!
       }
      
     })()} // don't forget to call the function!
    </>
  )
}

You can run arbitrary code inside the block — conditionals, variable declarations, or loop iterations — as long as the block returns a JSX element:

function App() {
  return (
    <>
      {(() => {
        let str = ''
        for (let i = 0; i < 10; i++) {
          str += i
        }
        return (<p>{str}</p>) 
      })()}
    </>
  )
}

Arguments can also be passed into the inline function from the surrounding scope, which makes this useful when you need to process a collection with more logic than a one-line mapping permits.

Extending Native Element Types

TypeScript lets you create component interfaces that extend existing HTMLElement props, which is excellent for autocomplete while keeping the full native API available.

Suppose you want a Button component that adds an extra prop but still accepts every standard button attribute:

import React, { ButtonHTMLAttributes } from 'react'

Import ButtonHTMLAttributes, which types all props of an HTMLButtonElement, and extend it with your own interface:

interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
 status?: 'primary' | 'info' | 'danger'
}

The component destructures the props it cares about, collects the rest, and spreads them onto the rendered element:

function Button(props: ButtonProps) {
 const { status, children, ...rest } = props // rest has any other props
 return (
   <button
     className={`${status}`}
     {...rest} // we pass the rest of the props back into the element
   >
     {children}
   </button>
 )
}
import React, { ButtonHTMLAttributes } from 'react'
 
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
 status?: 'primary' | 'info' | 'danger'
}
 
export default function Button(props: ButtonProps) {
 const { status, children, ...rest } = props
 return (
   <button
     className={`${status}`}
     {...rest}
   >
     {children}
   </button>
 )
}

Consumers retain type, disabled, and anything else a button normally supports. You can also allow extra className values to pass through, with a safety check when none are provided:

import React, { ButtonHTMLAttributes } from 'react'
 
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
 status?: 'primary' | 'info' | 'danger'
}
 
export default function Button(props: ButtonProps) {
 const { status, children, className, ...rest } = props
 return (
   <button
     className={`${status || ''} ${className || ''}`}
     {...rest}
   >
     {children}
   </button>
 )
}

This pattern is ideal for building reusable internal components that follow your style guidelines without reimplementing every HTML attribute from scratch.

When to Reach For These

These APIs are not needed in every component, but they shine when standard prop drilling becomes restrictive. cloneElement and isValidElement give you the flexibility to accept and transform arbitrary React elements. The functional setter form of useState prevents stale-state bugs in async code. And extending native element types via TypeScript interfaces gives consumers autocomplete and type safety without locking them out of the underlying HTML spec. All three are worth keeping in your toolkit for building components that integrate smoothly into other people's apps.