Building React Forms With useState

Aaron Schuyler
4 min readOct 2, 2020
Photo by Ferenc Almasi on Unsplash

Here is a simple tutorial on how to build a reusable react form with useState.

Imagine you are building a react app, for creating and editing posts, or anything that requires a basic form. How can we build a form that is reusable for both creating and editing our posts? Let’s start in our Router.

import PostForm from 'postForm.js'...<Router>
<div>
...
<Switch>
<Route
path='/posts/new'
children={
<PostForm
handleSubmit={someCreatePostFunction}
/>
}
/>
<Route
path='/posts/:id/edit'
children={ <PostFormWrapper /> }
/>
</Switch>
</div>
</Router>
...function PostFormWrapper() {
let { id } = useParams()
return (
<>
<PostForm postId={id} handleSubmit={someUpdatePostFunction} />
</>
)
}
...

If you are unsure of what is happening here, be sure to brush up on the react router docs. In essence we’re looking at a snapshot of a sample router file with a route to create and edit posts using the same PostForm component. I intent to explain the creation of just the form component in this blog post.

In order to reuse the same component we have our props, handleSubmit, and id. We can import our functions to handle submit from wherever our API calls are kept or dispatch them as Redux actions, I’ll leave that to you.

First thing first in our PostForm component, we need to import the required react functions.

import React, { useState, useEffect } from 'react'

If you are not familiar with React Hooks yet, you should check out the documentation here, however these two (useState and useEffect) are pretty simple to understand. They allow us to use state with a functional component and to replicate compnentDidMount().

Let’s create our component now.

export default function PostForm(props) {
return (
<form>
<label for="title">Title: </label>
<input
type="text"
name="title"
/>
<br />
<label for="content">Post Content: </label>
<textarea
name="content"
>
</textarea>
<br />
<input
type="submit"
/>
</form>
)
}

Here we have our basic form with title, content and submit inputs. The first thing we need to do is handle our submit. We can add a function for this.

export default function PostForm(props) {
const submit = (e) => {
e.preventDefault()
props.handleSubmit()
}
return (
<form onSubmit={submit}>
<label for="title">Title: </label>
<input
type="text"
name="title"
/>
<br />
<label for="content">Post Content: </label>
<textarea
name="content"
>
</textarea>
<br />
<input
type="submit"
/>
</form>
)
}

As you can see we added an onSubmit attribute to our form and created a function to call props.handleSubmit() but we need to pass it data.

We will do this by creating a constant called post with useState.

const [post, setPost] = useState({})

This creates a constant called post that we can store our post information in, and a function called setPost which we can use to mutate our post state.

We can use setPost() to change post when we are typing into the form fields.

<input
type="text"
name="title"
onChange={e => setPost({ ...post, title: e.target.value })}
/>
...<textarea
name="content"
onChange={e => setPost({ ...post, content: e.target.value })}
>
</textarea>

Now in our submit function, we should pass post to our props.handleSubmit call.

const submit = (e) => {
e.preventDefault()
props.handleSubmit(post)
}

Our final code for the create post form:

export default function PostForm(props) {
const [post, setPost] = useState({})
const submit = (e) => {
e.preventDefault()
props.handleSubmit(post)
}
return (
<form onSubmit={submit}>
<label for="title">Title: {post.content}</label>
<input
type="text"
name="title"
onChange={e => setPost({ ...post, title: e.target.value })}
/>
<br />
<label for="content">Post Content: </label>
<textarea
name="content"
>
</textarea>
<br />
<input
type="submit"
/>
</form>
)
}

That is it for the create post aspect of this form. But how can we make it so that we can use this component for editing posts as well? Remember we pass the id to the component in our router? We can use that to get our post from our API service by using useEffect().

useEffect(() => {
if (props.id) {
someAsyncGetPostById(props.id)
.then(post => setPost(post))
}
})

This will set our post to the return of our get post by id async function, which we can import from elsewhere or include in the component, that is up to you.

Remember we are passing the handleSubmit as a prop so we don’t have to change anything there. We simply need to have our input values reflect our post constant, and we are good to go!

<input
type="text"
value={post.title}
name="title"
onChange={e => setPost({ ...post, title: e.target.value })}
/>
...<textarea
name="content"
onChange={e => setPost({ ...post, content: e.target.value })}
>
{post.content}
</textarea>

One finishing touch that I like is to change the button text.

<input
type="submit"
value={props.id ? "Save Post" : "Create Post"}
/>

Our final code looks like this:

export default function PostForm(props) {  const [post, setPost] = useState({})  useEffect(() => {
if (props.id) {
someAsyncGetPostById(props.id)
.then(post => setPost(post))
}
})
const submit = (e) => {
e.preventDefault()
props.handleSubmit(post)
}
return (
<form onSubmit={submit}>
<label for="title">Title: {post.content}</label>
<input
type="text"
value={post.title}
name="title"
onChange={e => setPost({ ...post, title: e.target.value })}
/>
<br />
<label for="content">Post Content: </label>
<textarea
name="content"
>
{post.content}
</textarea>
<br />
<input
type="submit"
value={props.id ? "Save Post" : "Create Post"}
/>
</form>
)
}

And there we have it, a simple react form that can be used for creating and updating a post. This pattern can be expanded to handle almost anything you can think of, so go crazy. Happy Coding!

--

--