
Making Peace With Multiple Props in Reusable Components
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.tsxComponents 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.


