There are plenty of ways to build forms in React, but messing up the fundamentals can cause a lot of pain and suffering
input
element, so we have a Input
component. We have a select
element, so we have a Select
components, etc. When defining these, they should behave like the native elements but apply your core styles to them.
ComponentProps
type to make prop definitions easier.label
, fieldset
, etc. The idea here is to build your layer of abstraction on top of the native elements. This allows you to build your own components that are composable and reusable.
forwardRef
does make the component a little more complex to read, but not too bad in the end as this should be a set and forget type of component.
The best thing about this is, once you’ve created this small library of native abstractions you can continue to transfer them across projects. Since they contain no business logic or library dependencies, they are very portable.
select
element, keep state out of it, and keep it to forwarding props and state.
Where React shines is in its ability to define, build & compose components. — me, just beforeSo lets compose some elements to make building our form a bit easier and quicker. When building forms, you usually have a few components sitting next to each other, for example a
label
and input
/select
. I like to call these control
components, form-{element}-control
, the props they take are the same as your abstraction we created above. This is a good place to put fancy radio/checkbox controls as well.
forwardRef
, but I’ll leave that up to you. This is also the time you can add a small bit of UI rendering logic if you need to. For example, you might want to add a required
indicator to the label if the required
prop is passed to the Input
component.
type="email"
will ensure the value is email-esque and the min="6"
will ensure the password is at least 6 characters long. You can also use the pattern attribute if you want to add some more “complex” validation on the field. But this approach will get you quite far, without needing to reach for 2 libraries to do the same thing, a form library (Formik, react-hook-form, etc) and a validation library (zod, yup, etc).
On submission you can use FormData to pull the data out and send that to your backend to validate & process.
catch(err) { console.error(err) }
, shouldn’t be your error handling approach. If you’re in charge of the backend you can should return a 400
error with a JSON body containing the errors. If you’re using a validation library, they usually go a good job at giving you a key/value
pair of field name and the error message. You can use this on the frontend to display the error message next to the field, or you can use it to display a generic error message at the top/bottom of the form.
200
with a status code in the body of 400
. Please… I beg you.
FormData
in the same way as before. Using the form data to start a Stripe Checkout flow or something similar.
password
and confirm password
field. You can extract these into their own component and manage the state of them together. Moving these outside of the form component also allows you to use them in other places, and keeps the form component simple.
input
for our Input
or FormInputControl
component.
PriceControl
example above, you can take a similar approach but instead of managing state through built-in React methods, you’d use the library’s methods to control how the value is managed. For example, useFormContext
exposes a setValue
method, you can override the onChange
behaviour and call this method to update the value.
useReducer
gives you a nice dispatch
pattern to update the state, providing more control to perform complex state updates when required without needing to introduce any useEffect
calls to monitor value changes.
There are a few steps in this process so check out this how-to guide for more information.
useEffect
, I recently worked on a project with a donation form. When the user switched between a One-time and Monthly payment we wanted to clear any pricing information they’ve set. I create a dispatch action called SET_FREQUENCY
which would handle this for me.
useEffect
, since that can cause some weird side effects if you’re not careful. Additionally with StrictMode running useEffect
twice, it can sometimes cause issues depending on what you’re doing in the effect. If you can avoid a useEffect
it is always a win, one less thing to worry about and keep track of.