In React, we often build small components to encapsulate specific behaviors. This allows for composition and high re-usability.
Sometimes for instance, it makes sense to wrap a basic HTML tag like an <input>
or a <button>
in a Component, to add conditional styling or some extra piece of functionality. However, in that case it stops being a real DOM element and accepting html attributes.
Wouldn't it be nice to keep the original interface of the element we're wrapping ?
We'll learn how to do that in today's article !
Making a Button
Let's say you want to make a button that takes a valid
property and displays a βοΈ if this prop evaluates to true, and a β otherwise. Sounds easy enough, let's get to it!
First let's define the interface for our Button
interface ButtonProps {
valid: Boolean;
}
It takes a single valid
boolean property.
The component itself looks like this :
const Button: React.FunctionComponent<ButtonProps> = ({
valid,
children,
}) => {
return (
<button
disabled={!valid}
>
{valid ? <Valid /> : <Invalid />}
// Rendering the children allows us to use it like
// a normal Button
{children}
</button>
);
};
export default Button;
Perfect ! The component renders correctly depending on the value of valid
:
Lovely ! However we probably want for something to happen when we click on the button, and that's where we run into some trouble. For example, let's try to trigger a simple alert on click :
<Button
onClick={() => {
alert("hello");
}}
valid={valid}
>
Valid button
</Button>
If you try to use your Button that way, typescript will complain:
Type '{ children: string; onClick: () => void; valid: boolean; }' is not assignable to type 'IntrinsicAttributes & ButtonProps & { children?: ReactNode; }'.
Property 'onClick' does not exist on type 'IntrinsicAttributes & ButtonProps & { children?: ReactNode; }'. TS2322
And indeed, while the onClick
property is part of a button
interface, our custom Button
only accepts the valid
props !
Let's try to fix that.
Typing the component
First it's important to know that working with React type declarations can be a little tricky. Like most libraries, React delegates its types to the Definetly Type Repository. If you head over to the React section, you'll find a lot of similar interfaces.
Take React.HTMLAttributes<T>
for instance. It is a generic interface that accepts all kind of HtmlElement
types. Let's try it with HTMLButtonElement
. It seems to be what we're looking for, and if we use it, Typescript will stop complaining.
interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
valid: Boolean;
}
Now that works like a charm, so what am I going on about ? Well, say we want to set the type of our Button ...
<Button
onClick={() => {
alert("hello");
}}
valid={valid}
type="submit"
>
Valid button
</Button>
We get a compilation error !
Type '{ children: string; onClick: () => void; type: string; valid: boolean; }' is not assignable to type 'IntrinsicAttributes & ButtonProps & { children?: ReactNode; }'.
Property 'type' does not exist on type 'IntrinsicAttributes & ButtonProps & { children?: ReactNode; }'. TS2322
And indeed, taking a closer look to React.HTMLAttribute it doesn't actually define any html attribute specific to the HtmlElement
type it receives:
interface HTMLAttributes<T> extends AriaAttributes, DOMAttributes<T> {
// <T> is not used here
}
The DOMAttributes
that it extends only deals with event handlers. This is is just one example, but there a many others, and since I am a great guy, I'm going to save you some time and give you the correct interface to use. The one that actually does what we want is React.ComponentProps
. Note that we pass it a string and not a type.
interface ButtonProps extends React.ComponentProps<"button"> {
valid: Boolean;
}
No more compilation error ! Our Button
component now has the exact same interface as a normal button
, with the additional valid
property.
Done ? Almost but we still have a little problem left to solve.
Passing the props down to the DOM
Indeed, if we click on the button nothing happens. It should display an alert, but it doesn't, something is missing.
The problem is, while we have the correct interface, we need to pass the props down to the actual <button>
element. Now, we could set the onClick handler explicitely by adding the props to our component. However, we want our Button
to be able to handle any props that a <button>
might receive. Doing that for every possible HTML attribute would be hard to read and way too time consuming.
That's where the spread operator comes in handy ! Have look :
const Button: React.FunctionComponent<ButtonProps> = ({
valid,
children,
...buttonProps
}) => {
return (
<button
{...buttonProps}
disabled={!valid}
>
{valid ? <Valid /> : <Invalid />}
{children}
</button>
);
};
Since our component is typed correctly, Typescript can infer that ButtonProps
only contains HTML attributes, and we can pass it directly to the actual DOM
element.
And now our Button
finally works as expected :
Not only that but we can pass it any props as you would a <button>
. type
, name
.. you name it !
It's magic !
Conclusion
That's all for today ! Hope your found this article useful, more will come soon !
Top comments (0)