Making Peace With Multiple Props in Reusable Components

Making Peace With Multiple Props in Reusable Components

M. Zakyuddin Munziri

M. Zakyuddin Munziri

@zakiego

Originally written in Bahasa Indonesia.

Background

This problem started because I was working on many projects with a lot of inputs. It can no longer be counted, from simple inputs to seven levels deep! Complex, one input related to another.

However, these inputs have one common pattern: they always have a <label> and <input> element. Eventually, to keep things tidy and reusable without having to write from scratch, a reusable component was created.

Usually this component is placed in /components/UI/Form/Input.tsx

Components that are agnostic in nature, meaning they are not tied to anything and can be used anywhere, are placed in UI.

Let's call this component <FormInput/>. Setting aside styling issues for now, the code would look like this:

interface FormInputProps {
  label: string
  name: string
}

const FormInput: React.FC<FormInputProps> = (props) => {
  return (
    <div>
      <label htmlFor={props.name}>{props.label}</label>
      <div>
        <input id={props.name} name={props.name} />
      </div>
    </div>
  )
}

const Page = () => {
  return (
    <div>
      <FormInput label="Name" name="name" />
    </div>
  )
}

<FormInput/> is a component that will accept label and name as props.

One problem solved. We can use <FormInput/> anywhere, without having to write <label> and <input> repeatedly.

A New Problem

But then a new problem arises: what if we want to customize the <label> and <input> parts?

For example, we want the label to be blue and the input to be green?

Let's try adding className as a prop:

interface FormInputProps {
  label: string
  name: string
  className?: string
}

const FormInput: React.FC<FormInputProps> = (props) => {
  return (
    <div>
      <label htmlFor={props.name} className={props.className}>
        {props.label}
      </label>
      <div>
        <input id={props.name} name={props.name} className={props.className} />
      </div>
    </div>
  )
}

const Page = () => {
  return (
    <div>
      <FormInput
        label="Name"
        name="name"
        className="text-blue-500 text-green-500" // NOTE THIS
      />
    </div>
  )
}

Look at the code above at the className of the FormInput component. What happens when one className, namely text-blue-500 text-green-500, is placed on the same className, then used for both <label> and <input>?

What actually happens is that text-green-500 will be taken, then changing both the label and input color to green.

What if we separate them, between the label className and input className? So the props would be separate.

interface FormInputProps {
  label: string
  name: string
  classNameLabel?: string
  classNameInput?: string
}

const FormInput: React.FC<FormInputProps> = (props) => {
  return (
    <div>
      <label htmlFor={props.name} className={props.classNameLabel}>
        {props.label}
      </label>
      <div>
        <input
          id={props.name}
          name={props.name}
          className={props.classNameInput}
        />
      </div>
    </div>
  )
}

const Page = () => {
  return (
    <div>
      <FormInput
        label="Name"
        name="name"
        classNameLabel="text-blue-500"
        classNameInput="text-green-500"
      />
    </div>
  )
}

Great!

Now the label will be blue, and the input will be green. The component has become more customizable.

Another Problem

One <FormInput> element is done. But then, I want to create another <FormInput> with type password?

It feels like it will become increasingly complex. I need to add props one by one. Eventually, the component is not as flexible as we want.

interface FormInputProps {
  label: string
  name: string
  classNameLabel?: string
  classNameInput?: string
  type?: string
}

const FormInput: React.FC<FormInputProps> = (props) => {
  return (
    <div>
      <label htmlFor={props.name} className={props.classNameLabel}>
        {props.label}
      </label>
      <div>
        <input
          id={props.name}
          name={props.name}
          className={props.classNameInput}
          type={props.type}
        />
      </div>
    </div>
  )
}

const Page = () => {
  return (
    <div>
      <FormInput
        label="Name"
        name="name"
        classNameInput="text-blue-500"
        classNameLabel="text-green-500"
      />
      <FormInput
        label="Password"
        name="password"
        classNameInput="text-blue-500"
        classNameLabel="text-green-500"
        type="password"
      />
    </div>
  )
}

The code will look like the above, props look unnatural and are not easy to read.

The Way Out

We can make props more natural and flexible, without having to add them one by one. How?

I found this method while doing research to migrate from formik to react-hook-form. When researching how to create reusable components in react-hook-form, I found code from this repository input-control/index.tsx.

We simply create props in the form of labelProps and inputProps which will be filled with an object and then inserted with the spread syntax (...).

interface FormInputProps {
  label: string
  name: string
  labelProps?: React.DetailedHTMLProps<
    React.LabelHTMLAttributes<HTMLLabelElement>,
    HTMLLabelElement
  >
  inputProps?: React.DetailedHTMLProps<
    React.InputHTMLAttributes<HTMLInputElement>,
    HTMLInputElement
  >
}

const FormInput: React.FC<FormInputProps> = (props) => {
  return (
    <div>
      <label htmlFor={props.name} {...props.labelProps}>
        {props.label}
      </label>
      <div>
        <input id={props.name} name={props.name} {...props.inputProps} />
      </div>
    </div>
  )
}

const Page = () => {
  return (
    <div>
      <FormInput
        label="Name"
        name="name"
        inputProps={{
          className: 'text-blue-500',
        }}
        labelProps={{
          className: 'text-green-500',
        }}
      />
      <FormInput
        label="Password"
        name="password"
        inputProps={{
          className: 'text-blue-500',
          type: 'password',
          onChange: (e) => {
            console.log('Password changed:', e.target.value)
          },
          autoComplete: 'password',
        }}
        labelProps={{
          className: 'text-green-500',
        }}
      />
    </div>
  )
}

Done!

Now there are only two props that we need to fill in and can be customized as we please. We can insert props that are commonly used in label and input without fear of overlapping each other. Even in the example above, we can insert onChange and type props.

Closing

Regarding the type in the interface, we can discuss it another time. In closing, when creating reusable components, we need to make them strict yet flexible and free from errors. So we don't write the same code repeatedly, the code is easy to read, yet at the same time there are no errors that disturb our sleep.

More Articles

I Stopped Digging Through Logs

I Stopped Digging Through Logs

Debugging changed when I stopped reading logs manually and started using AI agents to correlate errors across observability data - faster root cause, fewer dead ends.

Speed Was Never the Hard Part in CI CD

Speed Was Never the Hard Part in CI CD

Fast pipelines don't eliminate shipping fear. Confidence comes from safe rollbacks, feature flags, and systems that behave predictably when things go wrong.