As I’ve said in another article, it’s more than possible to handle forms in React without using state. But what about formatted inputs? Let’s say I have an input that’s intended to take a person’s phone number (or some other meaningful numeric value). You’re probably used to seeing solutions that look something like this:
(Feel free to interact with the codesandboxes as you read through this article. Note that you may have to go digging through the codesandbox file tree sometimes to find the code I’m referring to. These codesandbox embeds weren’t as flawless as I’d hoped they’d be. 😅 Thankfully, I only use a couple of files in this article. But if you’re having trouble, you can always click the “Open Sandbox” button within the embed.)
Okay… This works… But this brings us back to the problem of needing state variables to handle our inputs. The problem is worse if we have several formatted inputs. If you’re like me and you don’t want to inherit all of the disadvantages of relying on state for your forms, there is another way…
(If you’re unfamiliar with the
beforeinput event, I encourage you to check out the MDN docs. But essentially,
beforeinput gives you access to an input’s value before any changes have occurred. This is incredibly useful for keeping the
input’s value valid.)
This approach works. But is it really worth it? I mean… The code is more verbose… I still have to pollute 2
input props… and this doesn’t look very re-usable. Is all of that really worth it in order to avoid unnecessary re-renders, large amounts of state variables, and the other problems that come with controlled inputs?
If that’s what you’re thinking, then you’re asking the right questions. :) Thankfully, there is a solution that beautifully resolves this concern.
I’ve been playing around with
Svelte for a bit recently, and I quite honestly love it. I highly encourage you to try it out for yourself. One of the brilliant features that
Svelte has is actions. Actions enable you to add re-usable functionality to an HTML element without having to create a separate component. And it’s all done using plain old JS functions.
I used to think that adding re-usable functionality to HTML elements was only possible in Svelte, but it’s actually still possible in React thanks to
refs. Check this out!
(Note: If you’re unfamiliar with React refs, you should see their documentation before continuing.)
I call these… React Actions. (Did that give you a strong reaction? 😏)
Basically, I take advantage of the HTMLInputElement reference that React exposes, and I hook up all the useful handlers that I need to get the formatting job done. Because the
ref prop that React exposes accepts a function that acts on the DOM element, I’m able to create the re-usable utility function that you see above. I’m even able to update meaningful HTML attributes, such as pattern!
Notice that — just as with Svelte Actions — we have to take responsibility for cleaning up the event listeners in our React Actions. According to the React docs:
React will call the
refcallback with the DOM element when the component mounts, and call it with
nullwhen it unmounts. Refs are guaranteed to be up-to-date before
Thus, in our function, we’re making sure to add the event listeners when the react reference exists (i.e., during mounting), and remove the event listeners when the
null (i.e., during unmounting).
Note: You probably noticed that this time I’m adding an
oninput handler instead of an
onchange handler to the input element. This is intentional, as there is a difference between
oninput handlers and
oninput handler instead of an
onchange handler (which is a good thing). This article is not intended to fully explain this React oddity, but I encourage you to learn more about it soon if you aren’t familiar with it. It’s pretty important. (That React GitHub link I just gave you is a good start.)
What Are the Benefits to This?
This is game changing! And for a few reasons, too!
First, it means that we don’t run into the issues I mentioned in my first article about using controlled inputs. This means we reduce code redundancy, remove unnecessary re-renders, maintain code and skills that are transferrable between frontend frameworks, and more!
Second, we have a re-usable solution to our formatting problem. Someone may say, “Couldn’t we have added re-usability via hooks or components?” And the answer is yes. However, in terms of re-usability, I prefer this approach over creating a custom hook or creating a re-usable component. Regarding the former, it just seems odd to use hooks for something so simple. The latter option can get you in trouble if you want more freedom over how your inputs are styled. (Plus, if you aren’t using TypeScript, then redeclaring ALL the possible
HTMLInputElement attributes as props would be a huge bother.) So much for “re-usability”. Also, both of those approaches are very framework specific, and they still leave you with unnecessary re-renders in one way or another. React Actions remove the re-rendering problem without removing re-usability. They’re the best way to go for re-usability and efficiency.
Third, we unblock our event handlers. What do I mean? Well, unlike Svelte, React doesn’t allow you to define multiples of the same event handler on a JSX element. So once you take up a handler, that’s it. Sure, you can simulate defining multiple handlers at once by doing something like this:
But… that approach is rather bothersome — especially when you only want to do something as simple as format an input. By using React Actions, we’ve freed up that
onChange prop! (Admittedly, it trades the
onChange prop for the
ref prop. However,
ref is much less commonly used. And if you really need to use the
ref more than once, you can get around the problem by using a similar approach to the one showed above.)
Fourth this approach is compatible with state variables! Consider this:
This situation acts almost exactly the same as if we were just controlling a regular input. The difference? Our
handleChange event handler will only see the formatted value whenever the value changes. This means you can setup an event handler that’s only intended to interact with the formatted value. And you can do this without creating an intermediary “re-usable component”.
Since this is a React article, I imagine there are a few people who might complain about how this approach includes mutations (not on state… just on
event.target). But honestly, after playing around with some other frameworks, I’ve learned that there are times to mutate, and there are times not to mutate. Better to learn both and master the different scenarios than to impose standards impractically and make code more difficult to handle. There’s a time and place for everything…
And the Possibilities Don’t Stop with Inputs…
You can create whatever kind of React Action you need to get the job done for your inputs. But you can go even further beyond! For instance, have you ever had to make HTML Elements act as if they’re buttons? Please don’t tell me you’re still under the slavery of using “re-usable components”:
Nope. Don’t like it. It’s a little lame that with the “re-usable component” approach, we’re forced to create a prop that represents the type of element to use (whether we default its value or not). I also found that making a clean, re-usable, flexible TS interface was difficult to do without running into problems, hence the one disugsting use of
any. (There might be a solution that works without
any, but it wasn’t worth excavating for. Try tinkering with the types and you’ll see what I mean.)
We can make things much simpler with React Actions:
By adding a JSDoc comment, we can add some IntelliSense to our React Action so that new developers know what this guy is doing! And by placing
handleKeydown on the outside of our action, we guarantee that the function is only defined once. (This will not always be possible, depending on how complex your React Action is. But this is still much better than having to create an unnecessary component that could potentially perform unnecessary re-renders.)
This is only the beginning! I encourage everyone who reads this article to explore the new possibilities for their React applications with this approach!
Don’t Forget Your Tests!
Before wrapping up, I just wanted to make sure it was clear that actions are testable too!
Yep. Pretty straightforward. Note that — as is the case for all kinds of testing — your testing capabilities are limited to your testing tools. For instance, version 14 (or higher) of User Event Testing Library is needed to support tests for anything that relies on
beforeinput. (When using this version of the package for your tests, remember to
await all of your calls to the
userEvent functions.) If you’re determined to use an earlier version of the package, you’ll have to run with Cypress Testing Library instead.
And that’s a wrap! Hope this was helpful! Let me know with a clap or a shoutout on Twitter maybe? 😄
I want to give a HUUUUUUUUUUGE thanks to Svelte! That is, to everyone who works so hard on that project! It really is a great framework worth checking out if you haven’t done so already. I definitely would not have discovered this technique if it wasn’t for them.
I want to give a special, specific shout out to @kevmodrome from Svelte Society. He helped me out with a problem related to formatting inputs in Svelte land. He has a great article on actions if you’re interested in learning more about what you can do with them. It’s technically for Svelte, but you can still apply what’s there to “React Actions”. :)
I want to extend another enormous thanks to @willwill96! He caught an implementation bug in an earlier version of this article. 😬
Almost finally, I want to extend another big thanks to my current employer (at the time of this writing), MojoTech. (“Hi Mom!”) Something unique about MojoTech is that they give their engineers time each week to explore new things in the software world and expand their skills. Typically, I learn most and fastest on side projects (when it comes to software, at least). If it wasn’t for them, I probably wouldn’t have been able to explore Svelte, which means I wouldn’t have fallen in love with the framework and learned about actions, which means this article never would have existed. 😩
And last but farthest from least, I want to thank Jesus Christ. He’s the One who ultimately enables me to do anything meaningful that I do.
Give honor where honor is due, right? 😄