Why React needs a key prop
Why can't React just magically know what to do without a key?
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.
Why can't React just magically know what to do without a key?
When was the last time you saw an error and had no idea what it meant (and therefore no idea what to do about it)? Today? Yeah, you're not alone... Let's talk about how to fix that.
Is your app as fast as you think it is for your users?
Some common mistakes I see people make with useEffect and how to avoid them.