You must be thinking - why is there a blog needed for a task as simple as wrapping HTML React Elements, right ?
Well, let’s start by looking at how many of the junior React developers today make wrappers of this kind in TypeScript.
This is something I saw in a project -
interface FieldProps {
label: string;
id: string;
inputProps: any;
}
export default function Field({
label,
id,
inputProps,
}: FieldProps) {
return (
<>
<label htmlFor={id}>{label}</label>
<input id={id} {...inputProps} />
</>
);
}
It’s a wrapper component Field
which includes a label
alongside the input
which can be reused in multiple components like -
export function MyApp() {
const [firstName, setFirstName] = useState("");
return (
<form
onSubmit={(e) => {
e.preventDefault();
console.log("firstname - ", firstName);
}}
>
<Field
label="First Name"
id="first-name"
inputProps={{
value: firstName,
onChange: (e: any) => setFirstName(e.target.value),
}}
/>
</form>
);
}
Issues
Now, let me be clear, the above code will work fine. But you are not taking advantage of TypeScript at all if you code this way.
I’ve got multiple issues with this kind of strategy -
- It’s lazy. Multiple uses of
any
just kills the purpose of TypeScript. Personally I believe, you should never use anyany
. - High chances of unwanted errors and bugs, if
inputProps
are not carrying the valid props for Input Element, as their won’t be any validations due to theany
keyword. - You won’t get any suggestions when filling in
inputProps
in the above example. - This can lead to unnecessary huge interfaces.
A Cleaner Approach
Interfaces are more powerful than you think, and as a matter of fact, React has provided us with interfaces which carries HTML attributes for all kinds of HTML elements.
InputHTMLAttributes
for Input element.ButtonHTMLAttributes
for Button element.TextareaHTMLAttributes
for Textarea element.- and so on…
These interfaces can be extended by the Props interface of the Wrapper component which will essentially provide it with all the attributes of the desired HTML element.
Now, let’s tweak the above component -
import { InputHTMLAttributes } from "react";
interface FieldProps extends InputHTMLAttributes<HTMLInputElement> {
label: string;
}
export default function Field({ label, ...props }: FieldProps) {
return (
<>
<label htmlFor={props.id}>
{label}
</label>
<input {...props} />
</>
);
}
and we then use it like this -
export function MyApp() {
const [firstName, setFirstName] = useState("");
return (
<form
onSubmit={(e) => {
e.preventDefault();
console.log("firstname - ", firstName);
}}
>
<Field
label="First Name"
id="first-name"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
/>
</form>
);
}
Much much cleaner, is it not ? Benefits of this approach -
- All the props that can be passed to an input element can be passed directly to this component, without having to specifically mention any attribute and its type in the interface. All of that data is coming from
InputHTMLAttributes<HTMLInputElement>
interface. - It’s type safe! Whatever prop we try to pass will be validated against its desired type.
- We get to enjoy suggestions while we are filling in props for
Field
component. - Concise, as we only mention the extra props we require for the wrapper in the interface.
How I got to know about this ?
I got to know about this approach from my good friend Ajit who was my colleague back when I was an employee of ECS Infosolutions (now GlobalLogic UK&I).
I am telling you this because valuable ideas and knowledge often emerge through collaboration and interaction with others, specially the people you work with!
Please let me know if there are more ways to further simplify this issue, or any other approaches.