What is React?
Whether you're brand new to building dynamic web applications or you've been working with React for a long time, let's contextualize the most widely used UI framework in the world: React.
Here's a form in JSX
:
function UsernameForm({onSubmitUsername}) { function handleSubmit(event) { event.preventDefault() onSubmitUsername(event.currentTarget.elements.usernameInput.value) } return ( <form onSubmit={handleSubmit}> <div> <label htmlFor="usernameInput">Username:</label> <input id="usernameInput" type="text" /> </div> <button type="submit">Submit</button> </form> )}
Let's type that handleSubmit
function. Here's how some people do it (copying this approach from some blog posts and "semi-official guides" I've seen):
function handleSubmit(event: React.SyntheticEvent<HTMLFormElement>) { event.preventDefault() const form = event.currentTarget const formElements = form.elements as typeof form.elements & { usernameInput: {value: string} } onSubmitUsername(formElements.usernameInput.value)}
The reason we have to have the as
there is because TypeScript isn't quite smart enough to know what elements we're rendering in our form, so we have to use the type cast which I'm not a fan of, but you gotta ship right?
My first improvement to this is to change that usernameInput
type:
- usernameInput: {value: string}+ usernameInput: HTMLInputElement
Definitely take advantage of the type it actually is rather than just cherry-picking the values you need.
The second improvement is:
- function handleSubmit(event: React.SyntheticEvent<HTMLFormElement>) {+ function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
Incidentally, at the time of this writing, there's no substantive difference in those types, but I prefer to be more clear and accurate with the name of the type, so that's what we're going to go with.
So here we are now:
function handleSubmit(event: React.FormEvent<HTMLFormElement>) { event.preventDefault() const form = event.currentTarget const formElements = form.elements as typeof form.elements & { usernameInput: HTMLInputElement } onSubmitUsername(formElements.usernameInput.value)}
But even with those changes, I'm not a fan of this for three reasons:
form
anywhere else, I'll either have to duplicate the code or extract the type and cast it everywhere I use the form. 3. I don't like seeing as
in my code because it is a signal that I'm telling the TypeScript compiler to pipe down (be less helpful). So I try to avoid it when possible. And it is possible!Keep in mind that we're the ones telling TypeScript what that event.currentTarget
type is. We tell TypeScript when we specify the type for our event
. Right now that's set to React.FormEvent<HTMLFormElement>
. So we're telling TypeScript that event.currentTarget
is an HTMLFormElement
but then we immediately tell TypeScript that this isn't quite right by using as
. What if instead we just tell TypeScript more accurately what it is at the start? Yeah, let's do that.
interface FormElements extends HTMLFormControlsCollection { usernameInput: HTMLInputElement}interface UsernameFormElement extends HTMLFormElement { // now we can override the elements type to be an HTMLFormControlsCollection // of our own design... readonly elements: FormElements}
So in reality, our form
is an HTMLFormElement
with some known elements
. So we extend HTMLFormElement
and override the elements
to have the elements we want it to. The HTMLFormElement['elements']
type is a HTMLFormControlsCollection
, so make our own version of that interface as well.
With that, now we can update our type and get rid of all the type casting!
Here's the whole thing altogether:
import * as React from 'react'interface FormElements extends HTMLFormControlsCollection { usernameInput: HTMLInputElement}interface UsernameFormElement extends HTMLFormElement { readonly elements: FormElements}function UsernameForm({ onSubmitUsername,}: { onSubmitUsername: (username: string) => void}) { function handleSubmit(event: React.FormEvent<UsernameFormElement>) { event.preventDefault() onSubmitUsername(event.currentTarget.elements.usernameInput.value) } return ( <form onSubmit={handleSubmit}> <div> <label htmlFor="usernameInput">Username:</label> <input id="usernameInput" type="text" /> </div> <button type="submit">Submit</button> </form> )}
So now:
handleSubmit
code, allowing us to focus on what the function is doing. 2. If we need to use this form
somewhere else, we can give it the UsernameFormElement
type and get all the type help we need. 3. We don't have to use a type cast, so TypeScript can be more useful for us.I hope that's helpful to you!
Delivered straight to your inbox.
Whether you're brand new to building dynamic web applications or you've been working with React for a long time, let's contextualize the most widely used UI framework in the world: React.
Truly maintainable, flexible, simple, and reusable components require more thought than: "I need it to do this differently, so I'll accept a new prop for that". Seasoned React developers know this leads to nothing but pain and frustration for both the maintainer and user of the component.
Speed up your app's loading of code/data/assets with "render as you fetch" with and without React Suspense for Data Fetching
Forms can get slow pretty fast. Let's explore how state colocation can keep our React forms fast.