#How do I enjoy using FormData?

1 messages · Page 1 of 1 (latest)

dreamy cape
#

I'm trying out the new actions with react router and am trying to learn to love "using the platform" for data, FormData. I'm coming from react query mutations where i keep form data in state so this is a bit different and...not as nice? Some warts I'm running into with FormData:

  • No type safety around data structures. I can't enforce FormData to have certain fields.
  • It's really hard to debug the state of the form. console.log(formData) is useless.
  • Everything is a string. With js/json you have booleans, strings, numbers, arrays, null, etc. I can pass it to zod and easily validate the data
  • Most of the time i need to create a js object from the form data. There does not seem to be a good way to do this especially since everything is a string.
  • Using form names for complex data is awkward, <input name="person[0][firstName]" />. Since all FormData is only one level deep you end up having to convert into complex data yourself.
  • You still need to end up maintaining state for allowing the user to add inputs: <button>Add Receipt Line Item<button>
  • react is all about rendering ui as a function of state. that's what makes react so pleasant to work with. Using defaultValue, only form elements with names, and relying on generated FormData seems to veer away from that paradigm

I know the web platform has all the necessary pieces but using FormData seems like a huge, unnecessary crutch to me right now. Is everybody using some package that makes this more productive, opaque? I can't seem to find what makes this kool-aid palatable. Anyone have some tips that make using FormData more pleasant?

woeful lion
# dreamy cape I'm trying out the new actions with react router and am trying to learn to love ...

Some of the reason to use it is to not manage your state with react and instead use what the browser is already giving you. There are shortcomings as you named, but you shouldn't need to create a new useState and track the value constantly with every single input you have in a form. Sometimes forms are complex and you benefit by tracking and managing state more, but you are always causing a re-render anytime you useState as a form value instead of just letting the browser do what it does. The other benefit of getting used to formData, you are more effective outside of just react. If you ever need to do vanilla forms, or you ever have a job in a different framework, you can always make use of formData and maybe not have to learn the new frameworks data paradigms as much. There is also the fact of when you have an actual server that you're working with and not just react-router's pseudo server, you don't actually get the types of the data you're sending unless you're using an extra npm install that is specifically giving you those types, perhaps like TPC. But those are also "cheating" somewhat using either some form of type casting or because you define the schema for what route you're hitting it can make more assumptions about how to unwrap and use that data.

mint spindle
#

I think @woeful lion explains pretty well already. Many people will find it awkward to work with FormData at first. As there are mindset shift here on how form data could be handled and there are yet many new approaches to be explored.

There are a few issues as you mentioned:

  1. Ensure type safety
  2. Parsing formData to complex data structure
  3. The paradigm

For (1), this is exactly why Zod is one of the most loved tool in the community. You can enjoy the flexibility of the HTML Form without giving up type safety.

For (2), the easiest trick is to use Object.fromEntries(formData) if your forms have no nested data. You can also find many solutions on the remix discord working on parsing formData based on a certain naming convention. All you need is to ensure the name on your inputs are correct.

For (3), there is nothing stopping you from using controlled input with react router / remix form. It is just suggesting the use of defaultValue because you don't always need a state. You can also find react-hook-form suggesting the same thing even it is not relying on formData.

Personally I have been working on Conform to bridge the gap. Maybe you will find it useful: https://github.com/edmundhung/conform

You can also find some examples with react router here:
https://conform.guide/examples/react-router

GitHub

Progressive enhancement first form validaition library for Remix and React Router - GitHub - edmundhung/conform: Progressive enhancement first form validaition library for Remix and React Router

glossy jolt
scarlet scarab
#

No type safety around data structures. I can't enforce FormData to have certain fields.

Even if you could do request.formData<ExpectedType>() or new FormData<ExpectedType>() that would be a lie, because the FormData represents a runtime value, there's no way to statically type it safely, you need to have a runtime validation to ensure the data there matches your expected structure

dreamy cape
#

that's kinda my point..?

#

if i store my data in a useState() i can add type safety to that

scarlet scarab
#

both that's only client-side

#

request.json() doesn't support a generic too for the same reason

dreamy cape
#

i'm using react router, i'm only concerned with client side at the moment

scarlet scarab
#

there's a network between the browser and your server, and the only way to ensure what you receive is what you expect is to validate at runtime

#

oh you had the remix label

dreamy cape
#

sorry. i considered my issues with FormData relevant to remix as well

scarlet scarab
#

nothing prevents you to do the fetch yourself from the component then

#

just don't use RR actions

#

and Form

#

and instead submit JSON from components directly

#

you can then use the useRevalidator hook to ask RR to re-run your loaders

dreamy cape
#

right. that's what i'm considering, just ditching route actions. that doesn't seem like the "recommended" approach though

scarlet scarab
#

btw on my blog I have been working on a Zod schema for FormData

import { z } from "zod";

export function formData() {
    return z.instanceof(FormData).transform((formData) => {
        return z
            .array(
                z.tuple([z.string(), z.string().or(z.instanceof(File)).nullable()])
            )
            .transform((entries) => Object.fromEntries(entries))
            .parse(Object.entries(formData));
    });
}

then you can do formData().pipe(Schema).promise().parse(request.formData())

dreamy cape
#

the idiomatic way to use react router now seems like to put up with FormData

scarlet scarab
#

it will still now allow nested fields, or duplicated fields, I'm still working on that

paper lily
#

Personally I think that declarative forms (and form inputs) and letting the DOM manage state is much easier than a ton of manually managed useState's

dreamy cape
#

eh, respectfully disagree. It's all hidden in FormData, you have to come up with weird input names to do nested data which than have to be parsed back out, i inevitably need to show/hide or do something based on a form value which leads me to tracking that in state and then i'd have a hybrid mess of uncontrolled and controlled inputs. maybe i'm just doing forms all wrong 🤷‍♂️

scarlet scarab
#

hybrid controlled and uncontrolled forms is not a bad thing

#

I actually think it's the best approach

dreamy cape
#

i'd agree that letting the DOM manage the state is less code but i don't think it's easier when your form has complex requirements

scarlet scarab
#

control what you need to control

#

leave to the browser the rest

paper lily
#

react is all about rendering ui as a function of state

I've been thinking about this a lot lately, especially in the context of RSC. It feels like the ui = fn(state) concept doesn't hold true when you bring useEffect/useState into the picture because it's more like ui = fn(state, data fetching, user input, side effects, etc.). With the Remix/RR data APIs I am always asking myself "does this belong in the component or somewhere else?"

So things like parsing, validation, converting user-inputs (inherently strings) to primitives/complex structures don't belong in the component in my eyes. But previously they had no where else to live. The action becomes the perfect place to parse formData, validate it, make your complex structures, and then trigger mutations via fetch calls or RQ or whatever.

scarlet scarab
#

if your form is that complex, you probably want another lib to handle them

#

like React Hooks Form or Formik

dreamy cape
#

i like consistency and that doesn't seem consistent at all

paper lily
#

I think we've collectively been "doing forms wrong" as an industry for a long time now 🙂

dreamy cape
#

i would love to see a react-router/remix guide to complex forms:

  • adding/removing/modifying nested data
  • selects with dynamic options based on other input values
  • typeaheads
  • fields appearing/disappearing based on other input values
  • etc
woeful lion
#

Remix devs or people in the community have often suggested sort of "Full Stack Components" where you get complex data by tying an endpoint to a specific component then you can add or remove that in the spots you need it to be

#

This doesn't fully answer your question but its a piece of that puzzle

dreamy cape
#

even that is awkward with just react router though, defining virtual routes just to be able to use a fetcher on them.

woeful lion
#

But it scales down complexity... you test a single endpoint, single component that you know is good to go, now there is less form complexity since you only have to manage when does that component render?

#

If a form is complex enough you'll have layers and layers of things that need to happen, so why not make it easier on yourself and guarantee pieces of it work in a vacuum, now you can plug that into the form you need later

dreamy cape
dreamy cape
woeful lion
#

Do you have massive productivity changes by using json instead of formData. You should still have to validate everything that is coming in json because you don't know what the user is sending. You also don't truly have types if it becomes a network request, you have a string or a file. Anything that tells you it's a specific type is lying or cheating, you're either providing a schema to be able to typecast or its typecasting for you but that is not guaranteed to be correct

dreamy cape
#

i mean zod makes that trivial

#

in my mind it's just way easier to model data in a structure that natively supports a hierarchy and different data types: js/json.

woeful lion
#

but zod also makes it trivial with formData itself. I honestly don't notice much of a difference between the two, at worst you have the nested data you need to pluck out if you have say an array of values you can expect for a certain name

paper lily
dreamy cape
#

eh, one useReducer. put names on the inputs and and a single onChange handler for all

paper lily
#

We've heard from users that they can delete a lot of code when they lean into Form/action/loader/etc. I agree that FormData is more restrictive than raw JS objects - but that's a price I'm willing to pay to delete 30% of my code for a smaller/faster app 🙂

woeful lion
#

useReducer is not trivial to implement though, you now are crossing into things like Redux or zustand where you have full state machines designed for each and every form

#

It's about how much code do you want to manually roll and manage vs how much do you want to be done for you. Once you learn the standard once and strategies to use it, you have no more custom logic to bake in at every turn. You might have to figure out when to render something new, but that's all. It's not turning every form into a new state machine

dreamy cape
#

i guess it's the lack of consistency that really gets me. As soon as you need to render certain inputs based on others or do almost anything with a form value, you gotta track that yourself leaving with this hybrid approach. Once you do branch off to a controlled input, you're most likely going to want to take advantage of types for those values so that's another disconnect.

woeful lion
#

You're right that it calls for a mix of managed state and unmanaged state. But you keep losing me at saying types for your form data elements. In react-router I can agree that there is more of an argument for typing since it's all happening locally, but if you have a real server that you need to send real network requests to, you do not have types, you have a string or a File. Network requests "sanitize" all types so you reach for libraries like zod to get those types back.

dreamy cape
#

you do have types though if the backend you're using is a json api

woeful lion
#

you do not, extra libraries are involved to make those types real

#

it is not a real type until the extra library gets it

scarlet scarab
#

a JSON is a string, until you parse it

#

and even if you call JSON.parse it can fail if the string is malformed

#

and once you do JSON.parse(jsonString) what you get is any

#

because TS has no way to know what the string contained, it just know it's a valid JSON

dreamy cape
#

i get what you're saying but i think it's irrelevant to my point. If i'm using a json api that expects a json data structure, wouldn't it make more sense to model that as a js object than have to map back and forth to FormData?

scarlet scarab
#

like

let data = JSON.parse("{}") as { name: string }

now data is an object with a name for TS, but that's not true

#

if your form is going to map 1-1 what the API expects, then there's no reason to use actions IMO

#

so just get the data from your inputs using states and send it to your API directly on the onSubmit event

dreamy cape
#

i think other remix folks would disagree since they trigger loader invalidations

scarlet scarab
#

using Remix my forms are not usually a 1-1 map to what my APIs expects, I augment the data of the form with data from the URL, cookies, sometimes even other endpoints

scarlet scarab
#
let revalidator = useRevalidator()
let [form, dispatch] = useReducer(yourFormReducer)
function onSubmit(event: FormEvent<HTMLFormElement>) {
  fetch(endpointUrl, {
    method: "POST",
    body: JSON.stringify(form),
    headers: { "Content-Type": "application/json" }
  }).then(res => res.ok && revalidator.revalidate())
  event.preventDefault();
}
#

send your request, revalidate the loader if the response is a 2xx

woeful lion
#

I will say, you can also make helpers to make formData use a little easier, where you can make the front-end form a 1:1 of what you expect and then have formData get parsed into zod. I've done it for simple forms, but definitely if you have complex relationships to manage with the update it becomes less easy to do that, but I have at least a quarter of my forms solved without any state management and just using zod to parse the formData on the back

dreamy cape
#

this is the approach i'm leaning towards. i was just hoping somebody would point out something that would make FormData click for me since that's the recommended approach

scarlet scarab
#

btw you can use the years old qs Node lib to parse the FormData, instead of doing request.formData() do request.text() and pass the string to qs.parse, it know how to support nested fields

dreamy cape
#

is there a web native way to convert FormData to the encoded string that gets sent over the network?

scarlet scarab
#

I think you can do

let response = new Response(formData)
let body = await response.text()
#

it works, but it uses that format

#

that format is used when you send a multipart/form-data, usually only for file uploads

dreamy cape
#

boo

scarlet scarab
#

this is how non multipart/form-data request are sent

#

the same structure of a search params

dreamy cape
#

yeah it seems i can do this too:

(new URLSearchParams(formData)).toString()
paper lily
#

hah beat me to it

dreamy cape
#

of course TS doesn't like that. sigh lol

scarlet scarab
#

if you used Form to send a form to a RR/Remix action, you can do request.text() directly, unless you have set encType="multipart/form-data it will be key=value shape

dreamy cape
#

ah nice 👍

woeful lion