Jacob Paris
← Back to all content

Dynamic form inputs with Remix

When you submit a form with multiple inputs, the browser will check every input for its name and value and send them all to the server.

This is a basic browser behavior that has existed since the dawn of the web in the early 90s.

Despite that, developers often reinvent the form with javascript

They use React's useState to track the value and state of every input in the form, suppress the browser's default submission, and then manually send the data to the server themselves.

By refactoring your React code to output your form in a way that the browser can understand, you can write dynamic forms without switching everything to controlled inputs.

The only part of the form that needs to be dynamic is the number of inputs, so that's the part we'll use React to control.

With useState, track the number of inputs in the form and use that to generate uncontrolled inputs.

tsx
const [inputCount, setInputCount] = useState(1)
return (
<div>
{Array.from({ length: inputCount }, (_, i) => (
<input key={i} type="text" name="input[]" />
))}
<button
type="button"
onClick={() => setInputCount((count) => count + 1)}
>
+
</button>
</div>
)

Twitter allows users to mute words they don't want to see in their timeline using a similar dynamic input pattern. Let's build a similar feature in Remix.

First, we'll need a way to store the muted words on the server. We'll use a global variable to store the words, but in an actual application you would probably use a database.

tsx
declare global {
var mutedWords: string[]
}
if (!global.mutedWords) {
global.mutedWords = []
}

Next, we'll need to update the global variable when the form is submitted.

tsx
export async function action({
request,
}: ActionFunctionArgs) {
const formData = await request.formData()
const mutedWords = formData.getAll("mutedWords[]")
global.mutedWords = mutedWords.filter(Boolean) as string[]
return null
}

Finally, we'll need to load the muted words from the server and pass them to the client.

tsx
export async function loader({
request,
}: LoaderFunctionArgs) {
return json({
mutedWords: global.mutedWords,
})
}

Having access to the existing muted words allows two things:

  • We can set the inputCount to the number of muted words, so that the form will always have the right number of inputs
  • We can set the defaultValue of each input to the existing muted word

Here is a full code example with a live demo

tsx
import type {
ActionFunctionArgs,
LoaderFunctionArgs,
} from "@remix-run/node"
import { json } from "@remix-run/node"
import { useLoaderData } from "@remix-run/react"
import { useState } from "react"
declare global {
var mutedWords: string[]
}
if (!global.mutedWords) {
global.mutedWords = []
}
export async function action({
request,
}: ActionFunctionArgs) {
const formData = await request.formData()
const mutedWords = formData.getAll("mutedWords[]")
global.mutedWords = mutedWords.filter(Boolean) as string[]
return null
}
export async function loader({
request,
}: LoaderFunctionArgs) {
return json({
mutedWords: global.mutedWords,
})
}
export default function Example() {
const { mutedWords } = useLoaderData<typeof loader>()
const [inputCount, setInputCount] = useState(
mutedWords.length + 1,
)
return (
<div>
<h1>Example: Dynamic Form Inputs</h1>
<form method="post">
<fieldset>
<legend>Muted words</legend>
{Array.from({ length: inputCount }, (_, i) => (
<input
key={i}
type="text"
name="mutedWords[]"
defaultValue={mutedWords[i]}
/>
))}
<button
type="button"
aria-label="Add another word"
onClick={() =>
setInputCount((count) => count + 1)
}
>
+
</button>
</fieldset>
<button type="submit">Save</button>
</form>
</div>
)
}
Professional headshot
Moulton
Moulton

Hey there! I'm a developer, designer, and digital nomad building cool things with Remix, and I'm also writing Moulton, the Remix Community Newsletter

About once per month, I send an email with:

  • New guides and tutorials
  • Upcoming talks, meetups, and events
  • Cool new libraries and packages
  • What's new in the latest versions of Remix

Stay up to date with everything in the Remix community by entering your email below.

Unsubscribe at any time.