Build a Custom React Button Component With forwardRef API

Project Source Code

Get the project source code below, and follow along with the lesson material.

Download Project Source Code

To set up the project on your local machine, please follow the directions provided in the README.md file. If you run into any issues with running the project source code, then feel free to reach out to the author in the course's Discord channel.

Table of Contents

This lesson preview is part of the The newline Guide to Building a Company Component Library course and can be unlocked immediately with a \newline Pro subscription or a single-time purchase. Already have access to this course? Log in here.

This video is available to students only
Unlock This Course

Get unlimited access to The newline Guide to Building a Company Component Library, plus 70+ \newline books, guides and courses with the \newline Pro subscription.

Thumbnail for the \newline course The newline Guide to Building a Company Component Library
  • [00:00 - 00:12] In this lesson, we'll be improving the API of the original button component we created. Buttons are stateless and are one of the most common patterns on the web, which makes them a great showcase for common shared component patterns.

    [00:13 - 00:27] In this lesson, we'll be covering API patterns that are applicable to all shared components. This includes JSX children pass through, the React Forward Ref API, prop spreading with TypeScript and JSX, and providing opinionated prop defaults.

    [00:28 - 00:36] A core concept of React is the ability to compose elements using the children prop. Shared component design leans heavily on this concept.

    [00:37 - 00:47] Allowing consumers to provide children whenever possible makes it easier for them to provide custom content and enhance other components. It also helps line component APIs with those of native elements.

    [00:48 - 00:59] Let's allow our button component to render its children. Before we start, let's run npm run Storybook to get our Storybook environment running in the background.

    [01:00 - 01:21] The React.FC type definition that we included earlier already includes a children prop that we can reference. And we can pass this directly to the native button element that we're returning .

    [01:22 - 01:38] One thing that this does change, since we were relying on that default text, the button doesn't include any content right now. So let's update our button.stories.tsx to now spread the arguments that it's provided and include some custom content.

    [01:39 - 01:48] So let's do something like my button component. And we can see that updated.

    [01:49 - 01:52] Nice. Next up would be the 4Gref API.

    [01:53 - 02:02] Many components have a one-to-one mapping to an HTML element. To allow consumers access to that underlying element, we can provide a ref prop using the React.4Gref API.

    [02:03 - 02:15] Providing a ref isn't something that is necessary for day-to-day React development, but it can be very useful within shared component libraries. It allows for advanced functionality, like positioning a tooltip relative to our button with a positioning library.

    [02:16 - 02:30] Our button component provides a single HTML button element, which we can provide a reference to with 4Gref. So let's go back to our button.tsx and remove the default type definition that we applied to it originally.

    [02:31 - 02:37] And we can call React.4Gref. And that's going to take two arguments.

    [02:38 - 02:49] It's going to take a function with two arguments, the props, which we can leave the same as what we had before, and ref. Where is the plugin is complaining?

    [02:50 - 03:06] Since we are forwarding the ref, it wants us to provide a display name by default, so we can do button.display name, and we can just name that button to begin with. And then for our ref, we can apply this directly to the button element.

    [03:07 - 03:13] And this is going to error by default. So when we call React.4Gref, it doesn't know what type this ref is.

    [03:14 - 03:21] It isn't able to infer that type from where we place it. So we can provide a generic to 4Gref HTML button element.

    [03:22 - 03:31] And this should match the type of element that's returned from the ref attribute on the element that we're providing. So this is going to be a 4Gref HTML button.

    [03:32 - 03:41] And we can provide that to our native button's ref. The next step is going to be JSX prop spreading.

    [03:42 - 03:52] This pattern really increases the flexibility of our components. And it allows consumers to treat shared components as drop-in replacements for their native counterparts during development.

    [03:53 - 04:00] Prop spreading can help with a lot of different scenarios. Accessibility in ARIA attributes are a really common thing that people don't include in prop sheets.

    [04:01 - 04:15] And if you just extend the existing component set with prop spreading, it just happens automatically. Dynamic data IDs are another comma one, as well as all the native events that you don't use day to day can be added in.

    [04:16 - 04:22] Without props writing, each of the scenarios above would require explicit props to be defined. And this leads to something that I like to call the prop sheet of doom.

    [04:23 - 04:35] It can be common to see components with prop sheets that include hundreds of individual props. And most of these duplicate native functionality or are only relevant for a very specific implementation somewhere in a code base.

    [04:36 - 04:44] So props writing helps ensure that our shared component stays flexible as the native elements that they use internally. So let's add props spreading to our button component.

    [04:45 - 04:58] We can reference any remaining props that we have from our props here with destructuring so we can get props. The issue that we have right now is there are no other elements inferred to this by TypeScript.

    [04:59 - 05:10] So we can add in a second generic to for a graph called react props component props without ref. Since we're using for a graph here, this shouldn't be included in the other props.

    [05:11 - 05:21] And we can pass it our button element here. And so now props will include all of the non children props that are related to a button element.

    [05:22 - 05:32] So we can apply these directly to our button by spreading them here. And this allows a lot of really unique things with TypeScript.

    [05:33 - 05:47] So just to play around in our story here, since this is in a TypeScript environment, we could do something like an ARIA label that is working correctly. We can do an on click event that's typed correctly and includes all the correct targets.

    [05:48 - 05:56] So this would be a massive event of initial button element. And we can include custom data IDs like data test ID for example.

    [05:57 - 06:04] And more importantly, it will work if we include something that's invalid. So type, since this is a button, there's only so many types available.

    [06:05 - 06:20] Let's say we tried a type of select for example, which isn't a thing that would fail because that's an invalid type. And then for our final shared component pattern, let's look at opinionated defaults.

    [06:21 - 06:40] For certain components, you may need to default attributes to specific values, whether it's to reproduce bugs or improve the developer experience providing an opinionated set of defaults is often specific to an organization or team. If you do find the need to default certain props, you should ensure that it's still possible for consumers to override these values if needed.

    [06:41 - 06:59] A common complexity found with button elements is that the default type value is submit, which is unintuitive, especially in a modern reactor world where we have buttons which aren't wrapped in a form often. So this default type can often submit surrounding forms accidentally and can lead to difficult debugging scenarios.

    [07:00 - 07:06] So for our component, we actually want to default our type value to button. So let's go and add it in here.

    [07:07 - 07:15] So we don't want to destructure it from our existing props. What we can do is just include a type of button.

    [07:16 - 07:28] And then if someone provides their own type attribute, it would take precedence because it's after this type element that destructuring would reset it. So by default now, our button will have a type of button.

    [07:29 - 07:42] And then if someone were to provide a type of submit, that would override it locally. So now that we have a basic shared API setup, let's go ahead and commit our changes and save our progress.

    [07:43 - 07:47] And then in the next lesson, we're going to learn how to add style variants to this button component using style components.