DEV Community

Dave
Dave

Posted on • Updated on

Remix React with Uncontrolled Inputs

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)}
/>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
    )
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
)
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
rjdmacedo profile image
Rafael Macedo • Edited

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?

const inputRef = useRef();

useEffect(() => {
   ...
   inputRef.current?.focus() // this owouldn't be possible, right?
}, [])

<UncontrolledInput
    ref={inputRef}
    id='jobTitle'
    label='Job Title'
    value={jobTitle}
/>
Enter fullscreen mode Exit fullscreen mode
Collapse
 
neohed profile image
Dave • Edited

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:

const UncontrolledInput = React.forwardRef((props, ref) => {
    const {
        id,
        label,
        value = '',
        type = 'text',
        ...rest
    } = props;

    useEffect(() => {
        ref.current.value = value
    }, [value])

    return (
        <p>
            <label>
                {
                    label
                }
                <input
                    ref={ref}
                    id={id}
                    name={id}
                    type={type}
                    {...rest}
                />
            </label>
        </p>
    )
});
Enter fullscreen mode Exit fullscreen mode

And call it like this:

<UncontrolledInput
   ref={ref}
   id={'my-input'}
   value={'Hello Form'}
/>
Enter fullscreen mode Exit fullscreen mode