React Controlled Inputs
Like most React devs I use controlled inputs, where you supply a value and an onChange
handler to each <input />
.
<input
id='name'
value='Zelda'
type='text'
onChange={({target}) => changeHandler(target.value)}
/>
The alternative is to use un-controlled inputs, which I overlooked because, controlled inputs work just fine. Controlled inputs perform slightly less well (each key press causes all inputs to re-render), but you'd probably need 50 inputs before you even notice!
Then I started using Remix...
Remix
Remix embraces HTML forms.
<input />
tags inside forms don't need event handlers or fancy state management:
<form>
<input id="name" type="text" />
<button type="submit">
Submit Form
</button>
</form>
The HTML form posts the input values back to the server.
Server Postback, 100% Free!
Remix provides the Form
component, from the @remix-run/react
namespace, which builds on a standard HTML form
to provide extra functionality, such as, automatically hooking up to a server side function:
import { Form } from "@remix-run/react";
export const action = async ({ request }) => {
const formData = await request.formData();
const name = formData.get("name");
//TODO: Save to Database
}
export default function MyForm() {
return (
<Form method="post">
<input id="name" type="text" />
<button type="submit">
Submit Form
</button>
</Form>
)
}
It's not a meme, with Remix it really is that easy!
The input
above is an un-controlled input.
This gives us a form for adding data, but what about edit? If you supply a value to those input elements, React will complain:
Warning: A component is changing an uncontrolled input of type text to be controlled.
You've probably seen this React error, when you supply an input
with a value but no onChange
handler!
Wrap our input
elements in a component, so we can also handle edit...
To get the simplicity and performance of un-controlled inputs with the convenience of controlled ones you can use a ref
.
import React, {useEffect, useRef} from 'react'
const UncontrolledInput = ({
id,
label,
value = '',
type = 'text',
...rest
}) => {
const input = useRef();
useEffect(() => {
input.current.value = value
}, [value])
return (
<p>
<label>
{
label
}
<input
ref={input}
id={id}
name={id}
type={type}
{...rest}
/>
</label>
</p>
)
}
The input
value is set with the useEffect
and useRef
hooks from React; and Remix provides Form
to handle the server post-back:
<Form method="post">
<UncontrolledInput
id='name'
label='Name'
value={'Zelda'}
/>
</Form>
We can now set values in our input
elements and post that back to the server without event handlers or state management. Next, we only need to load the data from the server.
Full Server Round Trip, also 100% Free!
Let's complete the picture with Remix:
import { Form, useLoaderData } from "@remix-run/react";
export const loader = async () => {
//TODO: Load name from Database...
return json({ name: 'Zelda' });
};
export const action = async ({ request }) => {
const formData = await request.formData();
const name = formData.get("name");
//TODO: Save to Database
}
export default function MyForm() {
const { name } = useLoaderData();
return (
<Form method="post">
<UncontrolledInput
id='name'
label='Name'
value={name}
/>
<button type="submit">
Submit Form
</button>
</Form>
)
}
That's the easiest full-stack I've ever seen!
What about form validation?
Since we're "using the platform", remember "event bubbling"?
DOM events, like onChange
, bubble up the DOM tree, hitting each parent node, until they reach the Body
tag or an event handler cancels that event.
Event Bubbling in React
Here's a simple React component to demonstrate. The first button triggers both the button.onClick and the form.onClick. The second button only triggers its own onClick handler.
const MultiEventHandler = () => (
<form
onClick={() => console.log('Form click handler')}
>
<button
onClick={() => console.log('Button click handler')}
>
Fire Both Event Handlers!
</button>
<button
onClick={(e) => {
console.log('Button click handler');
e.stopPropagation()
}}
>
Fire My Event Handler
</button>
</form>
)
Taken to an extreme, you could have single event handlers on the body tag, to handle all events, such as, onchange and onclick (don't try)
This Remix example uses a single onChange handler on the Form
tag to handle all events for any nested input
controls:
<Form method="post"
onChange={(e) => {
const {id, name, value} = e.target;
// Perform validation here!
e.stopPropagation()
}}
>
<UncontrolledInput
id='name'
label='Name'
value={name}
/>
<UncontrolledInput
id='jobTitle'
label='Job Title'
value={jobTitle}
/>
<button type="submit">
Submit Form
</button>
</Form>
The onChange
event from each nested input
bubbles up to the Form
where it is "captured" by the event handler. By default, after running the code inside our event handler, the event would continue bubbling up the DOM tree, triggering any event handlers it encounters along the way, but we call stopPropagation()
to prevent the event from bubbling up any further.
Top comments (2)
Hi Dave. Thanks for the blog post :)
I'm experiencing some issues with Remix, React, Re-render custom uncontrolled inputs (that lose their values after submitting the remix form) but that's not why I'm submitting a comment here.
You mentioned the possibility to use a ref that we create inside, but if we want to use that ref outside?
Hi Rafael,
To pass a ref from a parent component to a child component, like UncontrolledInput, so that the parent can "see" inside the child component I would use a Forwarding Ref
You could rewrite UncontrolledInput to:
And call it like this: