React  

Wrapper Component to Consumer Component: That’s a Wrap!

What is a Wrapper Component?

In React, it’s common to create small, reusable components to make your code more modular and maintainable. Let’s walk through a simple example of this with an InputLabel wrapper component.

A wrapper component is a component in React that wraps other elements or components, bundling common structure, styles, or behavior in one place. It helps you avoid repeating yourself and keeps your code clean.

The Wrapper Component: InputLabel

type InputLabelProps = {
    label: string,
    id: string
};

const InputLabel = ({ label, id } : InputLabelProps) => {
    return (
        <div>
            <label htmlFor={id}>{label}</label>
            <input id={id}></input>
        </div>
    );
}

export default InputLabel;

This component accepts two props: label and id. It renders a <label> element linked to an <input> element through the htmlFor attribute.

The Usage. Of the Wrapper Component

Here’s how you might use this InputLabel wrapper component:

<InputLable label='User Name' id='username'/>
<InputLable label='Password' id='password'/>

In this example, we’re using the InputLabel component twice to create two input fields: one for the username and one for the password. By wrapping the <label> and <input> together in the InputLabel component, we’ve ensured consistency in the structure and styling of the form fields throughout your application.

Usage of Wrapper component

You know what, let me just type the password.

Login page

Wait a second, there’s a new requirement: we need to set the input type to password for the second input. Let’s just do that.

First, we need to add a new property named type to the props, and then pass it from the parent component.

type InputLabelProps = {
    label: string,
    id: string,
    type?: string; 
};

const InputLabel = ({ label, id, type } : InputLabelProps) => {
    return (
        <div>
            <label htmlFor={id}>{label}</label>
            <input id={id} type={type}></input>
        </div>
    );
}

//Usage
<InputLable label='User Name' id='username' />
<InputLable label='Password' id='password' type='password'/>

Username-password

The Problem with Hardcoding Props

You might think: Fine, I’ll just add more props. So you add type, placeholder, maybe required, and so on.

That works for a while, but the more props you add, the messier it gets.

The Solution: Spreading Props

React’s {...props} syntax is built exactly for this. Instead of specifying every possible prop, you let the user of your wrapper decide which props to pass:

type InputLabelProps = {
    label: string,
    id: string,
    [x: string]: any
};

const InputLabel = ({ label, id, ...inputProps} : InputLabelProps) => {
    return (
        <div>
            <label htmlFor={id}>{label}</label>
            <input id={id} {...inputProps}></input>
        </div>
    );
}

This makes your wrapper flexible. It works for text inputs, password fields, disabled fields, and anything you need.

Going a Step Further: Type Safety

Now let’s go one step further. What if you want type safety, specifically for props that belong to an <input> element?

TypeScript’s Answer: ComponentPropsWithoutRef

This utility type is a shortcut for saying, “I want all the valid props for an <input> element.” Instead of letting anything through, it restricts you to what’s valid for <input>, so you keep autocomplete and type checking.

import { type ComponentPropsWithoutRef } from 'react';

type InputLabelProps = {
    label: string,
    id: string,
} & ComponentPropsWithoutRef<'input'>;;

const InputLabel = ({ label, id, ...inputProps} : InputLabelProps) => {
    return (
        <div>
            <label htmlFor={id}>{label}</label>
            <input id={id} {...inputProps}></input>
        </div>
    );
}

What About Label-Specific Props?

This prop spreading is very specific to <input>. But what if you also want to pass props to the <label> element? No problem, React’s got you covered. You simply add a new property like labelProps with the type of <label>:

type InputLabelProps = {
    label: string,
    id: string,
    labelProps?: ComponentPropsWithoutRef<'label'>; // New prop for label
} & ComponentPropsWithoutRef<'input'>;;


const InputLabel = ({ label, id, labelProps, ...inputProps} : InputLabelProps) => {
    return (
        <div>
            <label {...labelProps}>{label}</label>
            <input id={id} {...inputProps}></input>
        </div>
    );
}

And how do you use it? Simple:

<InputLable label='User Name' 
            id='username'  
            labelProps={{ htmlFor: 'username', title: 'This is the username label' }} />

<InputLable label='Password' 
            id='password' 
            type='password'  labelProps={{ htmlFor: 'password', title: 'This is the password label' }}/>
  • labelProps={{ title: 'This is the username label' }}  which adds a native title tooltip to the label.
  • InputWithLabel component adds htmlFor={id}

Label-Specific Props?

And What About ComponentPropsWithRef?

You’ve heard of ComponentPropsWithoutRef, so there must be something called ComponentPropsWithRef, right? There is! Let’s see what that’s all about.

Dramatic Entrance: forwardRef

In your wrapper component, we use forwardRef. Instead of ComponentPropsWithoutRef, we use ComponentPropsWithRef. If you look at the function signature, you’ll see the parameters have changed.

What is forwardRef?

forwardRef is a React utility that lets you pass a ref through a component to one of its children usually a DOM element like <input>. This is how your parent component can directly access the underlying DOM node inside a child component.

import { forwardRef, type ComponentPropsWithRef } from "react";


type InputLabelProps = {
    label: string,
    id: string
} & ComponentPropsWithRef<'input'>;

function InputLabel(
    { label, id, ...inputProps }: InputLabelProps,
    ref: React.Ref<HTMLInputElement>
) {
    return (
        <>
            <label htmlFor={id}>{label}</label>
            <input id={id} {...inputProps} ref={ref} />
        </>
    );
}

export default forwardRef(InputLabel);

The function takes two parameters:

  1. The props object (InputLabelProps)
  2. The ref (React.Ref<HTMLInputElement>)

Export the wrapped function:

export default forwardRef(InputLabel);

In the Parent Component

Here’s what you need to do in the parent (App.tsx):

 Create a ref using useRef to get access to the child’s DOM element (the <input> inside InputLabel):

const usernameRef = useRef<HTMLInputElement>(null);

Pass that ref to the child component as a ref prop:

<InputLabel ref={usernameRef} ... />

Use the ref inside parent functions to manipulate the input directly:

usernameRef.current?.focus();
usernameRef.current!.value = "From this day forward, you put your faith in me!";

 Why is this necessary?

Because InputLabel is wrapped in forwardRef, the ref you pass from App will point to the actual <input> DOM element inside InputLabel, enabling direct DOM manipulation.

Summary in One Line

In the parent (App), create a ref with useRef and pass it as a ref to InputLabel. Then use that ref to interact with the input element inside the child component.

Let’s See It in Action!

Here’s how it all comes together:

function App() {
  const usernameRef = useRef<HTMLInputElement>(null);

  function handleSetInputContent() {
    usernameRef.current?.focus();
    if (usernameRef.current) {
      usernameRef.current.value = 'From this day forward, you put your faith in me!';
    }
  }

  return (
    <div className="input-button-wrapper">
      <InputLabel
        label="User Name"
        id="username"
        placeholder="Enter username"
        ref={usernameRef}
      />
      <button onClick={handleSetInputContent}>Set Content</button>
    </div>
  )
}

Conclusion

Look, here’s the bottom line: Wrapper components like InputLabel are all about making your code clean, consistent, and reusable.

We took it up a notch by using ComponentPropsWithoutRef to keep our props type-safe and focused. Then, we got real with forwardRef, giving your parent component the power to directly manipulate the child’s DOM node.

Now go forth and build something awesome.