Refactor Zag.js State Machine Powered Pin Component Functionality to a Custom React Hook

Segun Adebayo
InstructorSegun Adebayo
Share this video with your friends

Social Share Links

Send Tweet
Published a year ago
Updated a year ago

In many projects, it is useful to separate the logic that you right with the view that you create so that you can easily share that logic throughout your application. In React we do this with custom React hooks.

We'll improve on the reusability of the pin input component by refactoring it to a custom React hook. To do this we will utilize the prop getter pattern

This Headless Component pattern is a very useful pattern when building reusable components. It allows you separate functionality from presentation, making it easy to style them as needed.

Segun Adebayo: [0:01] So far, we've been able to build the PinInput component without thinking too much about how it's going to be reused in an application. Now let's refactor all this code to create a usePinInput React custom hook. We're going to start by looking into the machine here. [0:19] Here you see we are literally creating an instance of a machine directly in line 16. What we're going to do is convert this over into a function.

[0:30] We're going to put a function that takes some options. We're going to define a type for that option pretty soon. Here we're going to return that. We're converting it from a variable that returns a machine to a function that actually returns a machine. We're going to close that out now.

[0:52] Here, we see that this actually gives us room to parse in parameters to this function to replace certain parts of this machine.

[1:01] We'll start out by defining a type. Let's just call this machineOptions. Within this type, what we want to define are properties that can be configured by the user or, in this case, by the component.

[1:16] Now based on all the properties here, we can see that it's completed. It's a completed internal property. We don't want to expose it as an option. Same with focusIndex. This is a private property that we currently use within the machine to control the index and refocusing.

[1:32] That leaves us with two properties that could be controlled, value and uncomplete. In here, I'll move over value and uncomplete. I'll make valueOption now. If you look closely at the context, you see that there's another parameter such as this array dot from length of four. Four here determines the number of inputs that can be rendered on the page.

[1:58] What we can do is to create a property in the options. Let's call it numberOfFields and make that a number. There we can use that numberOfFields to actually control this value here which is four. What I'm going to do here is I'll just destructure out numberOfFields from options. Then we can grab in the remaining options and spread that onto the component like so.

[2:24] We can remove uncomplete now because it exists now in the machineOptions as expected. With numberOfFields, we can replace the length here to be equal to numberOfFields.

[2:34] What this opens us up to now is that we can switch over now to the application. You see here, it shows an error because we made this now a function that returns the machine. Let's now supply the arguments for this function here.

[2:50] We can see here, the first one, numberOfFields is four. The next thing we can see instantly is the numberOfFields four. Then the default array we created earlier is also here with a length of four.

[3:03] Now let's see how we can clean this up. We get rid of inputs here. Now we are always sure that the value is always an array of four items because we see numberOfFields here is four.

[3:15] We can switch these inputs now to value and grab the index from the second argument thereof map. We can blank that out. That leaves us with exactly the same effects and results as we had before. Let's switch over to actually verify that. We can see here that the value has four items in there and everything works as expected.

[3:38] The next thing we're going to do now is to refactor everything into a custom hook, which was actually our goal in the first place. Let's come in here and create a hook called usePinInput.

[3:51] In this hook, we're going to create a function here, PinInput, and we are going to parse in the options. The same options we created earlier, we are going to parse it in here.

[4:05] Now let's see how we can make this available in this file. Here, we created machine options. We're going to export that, and that is going to be immediately available here within the hook.

[4:19] Now the next thing we are going to do now is we are going to look into the App.tsx and grab this useMachine signature here into the hook. We're going to import all the missing pieces and immediately parse in options directly into this point here. Now within the app now, let's see what else we can refactor.

[4:49] The first thing we can see immediately here is we've got a label here that actually has an onclick handler. Now when creating a custom hook for React components, a very common pattern is to use prop getters. By prop getters, I mean you return an object that includes something like getLabelProps. getLabelProps will return you all of the properties of a label element.

[5:14] You can also do something like getInputProps which also is a function that returns properties of the input elements as we've seen. Now let's see how we can make this work correctly.

[5:27] Switching over to the app, the only thing here, the only property we parse here is an onclick. We could enhance this a bit more by adding a data part of label because every other part also includes a data part. Now let's copy these properties out here and then just return it as expected here. Then we'll fix the formatting issues. Onclick. There you go.

[5:56] Now to improve the type safety, we can make sure that this component is well-typed. We can bring in React.ComponentProps and parse in the label element. Because we see a TypeScript error here, let's see if we can fix this error. We copy this out into a dedicated type. We're going to call this labelProps.

[6:16] Then we can see here it would include a data part that points to a string. Then we can replace this out now with labelProps. Here you go. TypeScript is happy now. We can keep going. Now with that in place, the label has been refactored correctly.

[6:35] The next thing we see here is what we call the input that has a name and it has a type hidden with a value. We're going to copy all of that attribute, create a new element here called getHiddenInputProps. Now in this scenario, we're just going to return all of those attributes. Let's see how we can make that work. We factor out everything here to match the object syntax.

[7:08] Instantly, you can see here that the value is throwing an error. I believe that comes from state. We can come up here and grab the value, state.context.value. Now that is made available. We can improve the type here by creating a new type for the InputProps. We can say InputProps is the component props of input and the data part. Say that getHiddenInputProps returns that inputProps right there.

[7:35] Immediately, it tells us that the data part is required or is missing. Here, we can actually do a data part and say that this is the hidden input. That is the part of that element.

[7:50] The next thing we're going to see here is that the name is encoded to pin code. That's not what we want. We want the name of that element to be dynamic. Let's head back into the machine options. We're going to add a name attribute here. That's a string. We'll just do the same to the context as well just to store that name attribute within the context.

[8:11] Switching back here now, we can grab the name attribute from the context, and then we can now swap out this with name. Make sure the type is pretty correct. We can head in here and make this optional and make that optional as well. Now we have here the getHiddenInputProps done.

[8:36] The last thing we've got here is the input element. Now let's see how we can refactor all of the properties of this element. We'll start by copying out all the attributes that it has right now. Get into here the usePinInput and add the type for the InputProps. We'll paste in here all of these attributes and format them out pretty quickly.

[9:01] With that done, we start to see that the index here starts to yell at us because we're not providing an index. What we can do then is now to provide the index here in the getInputProps. We can say the index here is a number. But better still, we can convert this into an object. We can destructure out index from there as expected.

[9:27] Now we have getInputProps which gives us all of the properties of the input as expected. Now let's head back into the app and voila, all of the parts are done. The next thing we're going to do now is to replace all of this custom ad hoc implementation with the usePinInput hook we just created.

[9:48] We're going to start by importing the usePinInput hook. We grab here the usePinInput hook, parsing in the number of fields as four. Then we're going to grab all the prop getters we talked about earlier, getHiddenInputProps, getInputProps, and getlabelProps.

[10:07] Next, we're going to remove all the previous references to useMachine. Then we start to spread in all of the properties starting with the label. We spread in here getLabelProps. Then we go in here.

[10:22] This is the hidden input. We can say getHiddenInputProps. Then for each input here, you see here we actually do need the value. If we head back here to the usePinInput hook, we can also provide value.

[10:37] Another useful artifact we can provide is here for the hidden input, we're joining the values. Let's create a new property and return that as well because that is useful information. We'll call this property valueAsString. We join them like so. Then we grab valueAsString and supply that to all the users of this hook.

[11:04] Here now we can grab out the value. Now we're ready to replace the input. We grab all of these properties here, delete them and replace them with getInputProps parsing in the index. Now we're pretty done here with testing, so we can comment out that function. There you have it.

[11:30] Clearing up the code a little bit, we can remove all of that. Remove unused imports. Now let's test it out to confirm that it works as expected. Here is the final result. We can test this out by clicking on the label. It works nicely. Let's type one, two, three, four. It also works nicely. Then let's verify by clicking submit.

[11:54] Here, you see that submit here is no. Let's try to fix that by adding a name property here and let's call it pinCode to actually match the form data as expected. Then we go back again to test this out. We type one, two, three, four and we click submit. There you go. We actually see the values one, two, three, four.

[12:20] Then finally, we can add back the onComplete. We do here a quick log of the value. We switch back again to confirm that it works. We load the page and then we do one, two, three, four. You see there onComplete gets fired as expected.

[12:37] With this pattern now, you can share the usePinInput hook across many components and use that across the organization without any issues.

[12:47] In review, what we've done so far is to extract out all of the different pieces within this application into a custom hook called the usePinInput.

[12:57] We consume the machine within the usePinInput parsing in all the options. Then we return some state properties like value and valueAsString as well as prop getters like getLevelProps, getHiddenInputProps, and getInputProps.

[13:13] With that in place, we can actually use that and consume that within our app very easily like that. All our reform attributes work because we parse in the name attribute and here now we're in control of onComplete. I can imagine wrapping this even more to its own custom PinInput component and it should work as expected.

~ 2 hours ago

Member comments are a way for members to communicate, interact, and ask questions about a lesson.

The instructor or someone from the community might respond to your question Here are a few basic guidelines to commenting on

Be on-Topic

Comments are for discussing a lesson. If you're having a general issue with the website functionality, please contact us at

Avoid meta-discussion

  • This was great!
  • This was horrible!
  • I didn't like this because it didn't match my skill level.
  • +1 It will likely be deleted as spam.

Code Problems?

Should be accompanied by code! Codesandbox or Stackblitz provide a way to share code and discuss it in context

Details and Context

Vague question? Vague answer. Any details and context you can provide will lure more interesting answers!

Markdown supported.
Become a member to join the discussionEnroll Today