A deep dive on forms with modern React
React 19 introduces terrific primitives for building great forms. Let's dive deep on forms for the web with React.
Before we get started, here's a brief look of valid (today) ways to import React and use the useState
hook:
// globalwindow.React.useState()// CommonJSconst React = require('react')React.useState()// ESModules default importimport React from 'react'React.useState()// ESModules named importimport {useState} from 'react'useState()// ESModules namespace importimport * as React from 'react'React.useState()
Below I'll explain where each of these mechanisms came from and why I prefer the last one.
I started writing React code back in the React.createClass
days. Here's how we did things back then:
var React = require('react')var Counter = React.createClass({ propTypes: { initialCount: React.PropTypes.number.isRequired, step: React.PropTypes.number, }, getDefaultProps: function () { return {step: 1} }, getInitialState: function () { var initialCount = this.props.hasOwnProperty('initialCount') ? this.props.initialCount : 0 return {count: initialCount} }, changeCount: function (change) { this.setState(function (previousState) { return {count: previousState.count + change} }) } increment: function () { this.changeCount(this.props.step) }, decrement: function () { this.changeCount(-this.props.step) }, render: function () { return ( <div> <div>Current Count: {this.state.count}</div> <button onClick={this.decrement}>-</button> <button onClick={this.increment}>+</button> </div> ) },})
Yup, var
, React.createClass
, require
, function
. Good times.
Eventually, we got ES6 (and later) which included modules, classes, and some other syntax niceties:
import React from 'react'class Counter extends React.Component { state = {count: this.props.initialCount ?? 0} changeCount() { this.setState(({count}) => ({count + change})) } increment = () => this.changeCount(this.props.step) decrement = () => this.changeCount(-this.props.step) render() { return ( <div> <div>Current Count: {this.state.count}</div> <button onClick={this.decrement}>-</button> <button onClick={this.increment}>+</button> </div> ) }}
This is when the "how should I import React" question started popping up. A lot of people preferred doing this:
Normally this sort of thing isn't even a question. You just do what the library exposes. But React never actually exposed ESModules. It exposes itself as either a global variable called React
or a CommonJS module which is the React
object which has Component
on it (along with other things). But because of the way the code is compiled, both approaches technically work and neither is technically "wrong."
One other note... Looking at the code above you might wonder why we can't change that to:
- import React, {Component} from 'react'+ import {Component} from 'react'
The reason you need React
in there is because (at the time) JSX was compiled to use React:
- <button onClick={this.increment}>+</button>+ React.createElement('button', {onClick: this.increment}, '+')
So if you use JSX, you need to have React
imported as well.
It wasn't long after this that function components became a thing. Even though at the time these couldn't actually be used to manage state or side-effects, they became very popular. I (and many others) got very accustomed to refactoring from class to function and back again (many people just decided it was easier to use class components all the time).
For me, I preferred using function components as much as possible and this is likely the reason that I preferred using import React from 'react'
and React.Component
rather than import React, {Component} from 'react'
. I didn't like having to update my import statement any time I refactored from class components to function components and vice-versa. Oh, and I know that IDEs/editors like VSCode and WebStorm have automatic import features, but I never found those to work very well (I ran into stuff like this all the time).
Oh, and one other interesting fact. If you were using TypeScript instead of Babel, then you'd actually be required to do import * as React from 'react'
unless you enabled allowSyntheticDefaultImports
.
Then hooks took the scene and the way I wrote components evolved again:
import React from 'react'function Counter({initialCount = 0, step}) { const [count, setCount] = React.useState(initialCount) const decrement = () => setCount((c) => c - step) const increment = () => setCount((c) => c + step) return ( <div> <div>Current Count: {count}</div> <button onClick={decrement}>-</button> <button onClick={increment}>+</button> </div> )}
This brought up another question on how to import React. We had two ways to use hooks:
import React from 'react'// ...const [count, setCount] = React.useState(initialCount)// alternatively:import React, {useState} from 'react'// ...const [count, setCount] = useState(initialCount)
So again, should we do named imports, or just reference things directly on React
? Again, for me, I prefer to not have to update my imports every time I add/remove hooks from my files (and again, I don't trust IDE auto-import), so I prefer React.useState
over useState
.
Finally, React 17 was released with basically no real breaking changes but an announcement of great import (pun intended): There's a new JSX transform that means you no longer need to import React to transform JSX. So now you can write:
function App() { return <h1>Hello World</h1>}
And this will get compiled to:
// Inserted by a compiler (don't import it yourself!)import {jsx as _jsx} from 'react/jsx-runtime'function App() { return _jsx('h1', {children: 'Hello world'})}
So the import happens automatically now! That's great, but that also means that if you want to migrate to use this new capability, you'll need to remove any import React from 'react'
that's just for JSX. Luckily the React team made a script to do this automatically, but they had to make a decision on how to handle situations where you're using hooks. You have two options:
import * as React from 'react'const [count, setCount] = React.useState(initialCount)// orimport {useState} from 'react'const [count, setCount] = useState(initialCount)
Both of these work today, are technically correct, and will continue to work into the future when React finally exposes an official ESModules build.
The React team decided to go with the named imports approach. I disagree with this decision for reasons mentioned above (having to update my import all the time). So for me, I'm now using the import * as React from 'react'
which is a mouthful for my hands to type, so I have a snippet:
// snippets/javascript.json{ "import React": { "prefix": "ir", "body": ["import * as React from 'react'\n"] },}
For me, I'm sticking with import * as React from 'react'
so I don't have to worry about my imports. And that's my overly long blog post answer for a very common question I get. Hope it was helpful!
Delivered straight to your inbox.
React 19 introduces terrific primitives for building great forms. Let's dive deep on forms for the web with React.
Some common mistakes I see people make with useEffect and how to avoid them.
How and why you should use CSS variables (custom properties) for theming instead of React context.
It wasn't a library. It was the way I was thinking about and defining state.