How to Design Custom React Field Components With useUniqueID

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.

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] Our API for the field will have two compound components, label and input. The field component will provide a unique ID to fill.label and fill.input using the context API.

    [00:13 - 00:24] To generate our unique IDs, we'll be creating a custom React hook using the Nano ID library. Nano ID is a powerful utility that is able to create your old friendly unique strings.

    [00:25 - 00:32] Let's start by adding it as a new dependency. We can do that with npm install, Nano ID.

    [00:33 - 00:41] Next, we can create a new file for our hook. And since this utility can be useful for future component development, we're going to be placing it in the utils folder.

    [00:42 - 00:57] We create a new file named use unique ID. And in this hook, we're going to actually be creating a custom alphabet using the non-secure import for Nano ID.

    [00:58 - 01:15] Since we don't need cryptographically secure strings, it can be a lot more performant to use this import. So let's start by calling import custom alphabet from Nano ID, non-secure.

    [01:16 - 01:24] And then we're going to create a new Nano ID instance by calling custom alphabet. And we want this to have a length of 10.

    [01:25 - 01:36] And then we can provide the unique strings that we want to see. To make this as clean as possible, we can just do some numbers and then a few characters as well.

    [01:37 - 01:47] And this will create a unique string with a length of 10 characters using these characters that we've provided. Next, let's go and create the hook.

    [01:48 - 01:56] And we want to export this. And we want to use unique ID.

    [01:57 - 02:07] And for this, we're going to be using a stateful variable. So we can do that with use state.

    [02:08 - 02:19] And we will call Nano ID to generate that string. So let's go and include use state from rant.

    [02:20 - 02:36] And then we're going to be returning ID. So this will create a unique ID when this hook is first initialized, store it as a stateful value and return it for each unique render after that.

    [02:37 - 02:46] And this is actually an ESLint warning. So we need to return the strict value that we want to have returned from this function, which in this case would be a string returned from Nano ID.

    [02:47 - 02:59] So we can just make that a string. And so this new hook will generate a new ID when it's first rendered and then return that same value throughout all the consuming components life cycles.

    [03:00 - 03:14] Next, we need to create our wrapping field component, which will provide this value return from use unique ID through context to its children. Let's create a new directory under source called fields, where we're going to be putting all these files.

    [03:15 - 03:30] And then let's create a new field context component. And we're going to be importing the create context utility from rant.

    [03:31 - 03:47] And then exporting our field context component by calling create context and then providing undefined as the default value. Even though this is valid, we want to provide some type completion for this for consumers.

    [03:48 - 04:14] So what we want to have is the preferred value of string and then undefined if someone is receiving context or reading context, but not within a field component. So this will allow a user to see a string if they're within a field element and then undefined if, for example, a field.input is used outside of the field component, which is still a valid use case for the way that we're building this.

    [04:15 - 04:22] Next, let's create a new field.tsx file. And then in here, we can create our new field component.

    [04:23 - 04:39] So let's start by calling import react from react. And then we're going to be using the use unique ID utility that we created earlier.

    [04:40 - 05:00] And then we're also going to be importing the context that we just created. And then here we can export a new component field, which will be a functional component.

    [05:01 - 05:18] And we want to render all of the children within it. Now, this current provider, we're not providing any value to it.

    [05:19 - 05:33] So what we want to do is call our use unique ID hook to return ID. And then provide that as a value to our context provider.

    [05:34 - 05:38] All right, I messed up an import above. Try that.

    [05:39 - 05:54] And this field component will not return any DOM elements, but it will wrap any of its provided children with this context provider and provide that unique ID through context. It's going to save full context and everything should be good there.

    [05:55 - 06:04] So for the first element that we're going to do, let's create the nested label component. So let's create a new label.tsx.

    [06:05 - 06:19] And in these compound components, we can use the same techniques that we learned in the previous modules. So we can include children pass through, gsx prop spreading, and the for growth API, as well as including the custom unique ID.

    [06:20 - 06:34] So let's start by importing react. And then let's export a new label component.

    [06:35 - 07:00] And to start out with, let's return a native label rather than a style component to start. And then let's include some generics to this as well. So this is going to be HTML label element.

    [07:01 - 07:25] And it's going to be able to receive all of the native props for a label. And then we need to include a display name for this as well, since it is a for growth.

    [07:26 - 07:39] And for this, we can actually name this something different than what we've done before. So to make this as accurate as possible, we can name this a field.label since this will be a compound component in the future.

    [07:40 - 08:03] So next, we can customize this to include the unique ID to the HTML for attribute to make this an accessible experience. So let's start by adding in the use context from react, and then importing the shared field context that we created earlier.

    [08:04 - 08:16] And then in here, we can receive that ID from use context. And this will return either the value of string or undefined.

    [08:17 - 08:25] We will receive a unique ID if this is rendered within a parent field component . Otherwise, it would be undefined, which is still a valid prop in this use case.

    [08:26 - 08:46] So here we can do HTML for ID. And similar to how we handle opinionated defaults in the previous module, by including this before we do any prop spreading, this will ensure that if someone ever needs to customize HTML for, they can do that here by just providing the prop directly to label.

    [08:47 - 09:02] Next, let's create an input component. And same process as before, we can import react and use context.

    [09:03 - 09:20] And the shared field context value. And we're going to be exporting an input.

    [09:21 - 09:51] So you're going to return native input. The generics, this will be an HTML input element and receive all the component props without ref for input.

    [09:52 - 10:08] And then in here, we can receive that same ID that would be shared with label. And provide that to the ID value.

    [10:09 - 10:25] So now that we have a separate label and input component, we actually need to export these as properties from the field component. And this can be tricky to type correctly with TypeScript, but we can accomplish this by creating a custom composition interface in our field component.

    [10:26 - 10:39] So let's start by importing the two components that we want to export so label. And input.

    [10:40 - 10:59] And we're going to create a new interface that we'll call field composition, which has a property label. And input.

    [11:00 - 11:15] And we can include this in the functional component type here. So now we can call field.label is equal to label and field.input is equal to input.

    [11:16 - 11:21] And now the type completion is working. So to view this in action, let's create a new story.

    [11:22 - 11:31] And we'll call this input.stories.tsx. And I'm going to paste this in from the docs.

    [11:32 - 11:44] We're going to give this a new title, example/input. It's going to have a root component, which is field.input, just so we can see all the available props, including a sub component, which is the parent field and label.

    [11:45 - 11:59] And for the story, we're going to render a field with nested label and input and a custom placeholder object. So let's call npm run storybook.

    [12:00 - 12:13] And we'll be able to see this new story. And this won't include any styling to begin with. This is just to validate that we're applying the props correctly.

    [12:14 - 12:27] So here we can see a new input story with a placeholder knob. And if we were to inspect this element, we should see a matching unique ID applied to the label and the input.

    [12:28 - 12:37] And this should be a unique ID every time we refresh the page. And this is a new value.

    [12:38 - 12:52] And so now, because it's sharing the ID between the label and the input, if somewhere to click on the label, it would go to the input and for screen readers, this would be read off successfully. So next, let's include some new styling.

    [12:53 - 13:09] So going back to VS code, let's create a new styles.ts relative to the field components. And let's create two new style components for the label and the input.

    [13:10 - 13:24] So we'll start with import style from style components. And then let's create a new styled label.

    [13:25 - 13:40] And styled input component. And for these, we're going to just include the styling directly from what we have in the demo style guide.

    [13:41 - 13:51] So for our label, it's going to have a default font weight and margin bottom. And there's going to be some more advanced styling available for the input.

    [13:52 - 14:16] And then we're going to be including the transparentize method from emotion to include some alpha to our colors. And then we're also going to be including the constants from our source styles or util styles.ts file.

    [14:17 - 14:30] And next, if we go to our label component, we can import that new styled element. And we can just directly replace the label.

    [14:31 - 14:50] And since we use the style style dot label in that style file, this ref should be applied correctly. And we can do the same thing for input where we can import styled input.

    [14:51 - 15:02] From styles and replace our input here. And then if we look in storebook, we should see the styles applied correctly.

    [15:03 - 15:08] Nice. So this is now a accessible compound component.

    [15:09 - 15:21] And the reason this is most useful is if we go to our story and mess around with this, we can actually change the order of elements here. And we can actually apply that and see that referenced in the DOM.

    [15:22 - 15:36] And this isn't necessarily something you would want to do all the time. But this type of flexibility and the ability to style each of these elements individually make it a much more flexible experience than a single wrapping component that you're not able to key into.

    [15:37 - 15:47] So let's go ahead and commit our changes and save our progress. And in the next lesson, we're going to be exploring how to extend these existing style components by creating a new text area component.