Reusable Forms with React Hooks

Seth Massarsky
3 min readMay 9, 2021

This week I made something of a reusable form for my Matchup class in my fantasy hockey app. I’ve been wanting to do something like this, but hadn’t really had the time to think it through during the course of the bootcamp. Pretty much the idea comes from Ruby on Rails’ form helpers, which let you create one form that can be used for creating a new object or editing an existing object. If you’d like to follow along with the code for reference, here’s a link to the GitHub page.

Like most of the forms I’ve created since starting with React, this one is inside of a Modal. Previously, I’d create two completely separate modals, which would have nearly identical functionality outside of their action. This time, in MatchupForm.js, I export functional components NewModal and EditModal, which both wrap the MatchupForm functional component. The New and Edit modals pretty much just pass their props through to MatchupForm, but add a verb, which I’m tacking on the submit button:

export const EditModal = props => {
return <MatchupForm
{...props}
verb={'Update'}
/>
}
export const NewModal = props => {
return <MatchupForm
{...props}
verb={'Create'}
/>
}
.
.
.
<Button
variant="primary"
type="submit"
>
{verb} Matchup
</Button>

Even though they’re nearly identical, I thought it would make it easier to read through on the index page. Instead of two MatchupForms with different verb props, I thought naming them and hiding the verb prop was clearer.

From here, MatchupForm takes in the following props:

show: boolean, modal’s show attribute is set to this value

hide: function to reset state in the index component to change show prop

teams: list of teams passed to choose from

verb: for the button

submitAction: create or update function from index component

matchup: the matchup to edit, if editing

errors as serverError: I have some client side validations. This prop lets me pass errors from the server to the form to update error fields (pretty much just if the name is already taken)

The main difference between the new and update forms is whether a matchup already exists. To cover this, I used a useEffect hook to set the form fields when the matchup prop changes. So when it initially goes from undefined to a matchup object, the fields will set. Then, when the modal is hidden and matchup prop removed, the fields are cleared:

useEffect(() => {
if (matchup) {
setFields({
teamId: matchup.attributes.team.id,
name: matchup.attributes.name,
startDate: dateFormatter.toDateInputStr(matchup.attributes.startDate),
endDate: dateFormatter.toDateInputStr(matchup.attributes.endDate)
})
} else {
setFields(initialFieldsState)
}
}, [matchup])

The form uses controlled inputs tied to the fields state object:

const initialFieldsState = {
teamId: '',
name: '',
startDate: '',
endDate: '',
}
const [fields, setFields] = useState(initialFieldsState)

and has InputError components that render when provided by a message from the error state:

const [errors, setErrors] = useState({})

Fields are updated via the handleChange function:

const handleChange = e => setFields({ ...fields, [e.target.name]: e.target.value })

On submit, client side validations are run, then submitAction (create or update fetch) if validations pass:

const handleSubmit = e => {
e.preventDefault()
const tempErrors = validateMatchup(fields)
setErrors(tempErrors)
if (Object.keys(tempErrors).length === 0) {
submitAction(fields)
}
}

Last, in case the name was taken (or for whatever reason the client side validations missed something), I use another useEffect to set form errors to the server errors, set in the index component:

useEffect(() => {
setErrors(serverError || {})
}, [serverError])

And yeah that’s really it for the differences. This allows you to use one form for new and edit, reducing time spent updating nearly identical code in two places. Next week I think we’ll go over .fetch, .then, .catch and .finally, because I’ve been working on handling responses from the server better. In my previous React app I had some unhandled errors (mostly just when the Rails server wasn’t up). I’m spending the time on this current project to make sure I do it right from the ground up.

--

--