Form in React
This section discuss how to implements a form in React.
Allow User to Create Movie
It would be nice if our users can create a movie themselves.
Prerequisite: API Service for Create Movie
Before you can allow user to create movie, first of all the backend API Service must be able to support that.
Luckily our movies API support that. To able to create a movie, install this Restlet Client, which is a Chrome extension to allow you to make API calls.
After the extension is installed,
- open the extension by clicking the icon
- select “POST” in the METHOD dropdown.
- enter the following URL in the URL bar:
https://react-intro-movies.herokuapp.com/movies
- add the following content in the BODY field (change it to your favourite movie):
{ "name": "More Than Blue", "releaseDate": "2018-12-27" }
- click send
Now when you load your app, you should be see your movie is added.
Add Ajax Call Function to Make the POST Request
Now that we know the actual AJAX call works by using tools, let’s proceed to do that with our code.
Create a createMovie
function in api.js
that will make the request
export const createMovie = (movie) =>
axios.post('https://react-intro-movies.herokuapp.com/movies', movie).then((res) => res.data);
- axios allows you to makes API call with specific method, e.g.
get
,post
,put
etc., corresponding to our REST call methods. - the second parameter of
axios.post
is the body of the data that you want to submit.
Create Form Component
Now that we have function to make the API call, let’s create the form component.
Add a file movie-form.js
with the following content:
import * as React from 'react';
import { createMovie } from './api';
export const MovieForm = () => {
const [name, setName] = React.useState('');
const [releaseDate, setReleaseDate] = React.useState('');
const handleSubmit = (ev) => {
ev.preventDefault();
createMovie({
name,
releaseDate,
}).then(() => {
setName('');
setReleaseDate('');
});
};
return (
<div className="movie-form">
<form onSubmit={handleSubmit}>
<legend>Create Movie</legend>
<div className="field">
<label htmlFor="name" className="label">
Name
</label>
<input
className="input"
value={name}
id="name"
name="name"
onChange={(ev) => setName(ev.target.value)}
required
/>
</div>
<div className="field">
<label htmlFor="releaseDate" className="label">
Release Date
</label>
<input
className="input"
value={releaseDate}
id="releaseDate"
name="releaseDate"
type="date"
onChange={(ev) => setReleaseDate(ev.target.value)}
required
/>
</div>
<div className="button-container">
<button type="submit" className="submit-button">
Create
</button>
</div>
</form>
</div>
);
};
- we declare two states:
name
andreleaseDate
for the 2 values for the form. - the state value is passed to the
value
attribute of the input, while the state setter is called in theonChange
callback. - we define a
handleSubmit
function, which will be passed toonSubmit
props of the form element. When form is submitted, we will callcreateForm
with the state. We callevent.preventDefault
because by default form submission will cause a page refresh, and we doesn’t want that.
Add MovieForm Into App
Now import MovieForm
and include it in App
component:
import { MovieForm } from './movie-form';
// existing code
function App() {
const [moviesShown, toggleShowMovies] = useToggle(false);
const { movies, isLoading } = useMovieData();
return (
<div>
<TitleBar>
<h1>React Movie App</h1>
</TitleBar>
<div className="container">
<div>
<div className="button-container">
<Button onClick={toggleShowMovies}>{moviesShown ? 'Hide' : 'Show'} Movies</Button>
</div>
{moviesShown && (
<BusyContainer isLoading={isLoading}>
{movies.map((movie) => (
<Movie name={movie.name} releaseDate={movie.releaseDate} key={movie.id} />
))}
</BusyContainer>
)}
</div>
</div>
<div>
<MovieForm />
</div>
</div>
);
}
Now try to use the form, you can see the page is making the AJAX call, and after you refresh the page, the new movie will be there!
Refresh Movie List after Submission
Currently the movie list is not updated after you create the new movie, which is a bug.
Therefore, we need to somehow let me App
component know that the MovieForm
has created a record, and it should refresh the list.
We can achieve this by passing a callback from App
to MovieForm
.
-
Update
useMovieData
custom hook inApp
to returnloadMoviesData
:src/app.jsfunction useMovieData() { const [movies, setMovies] = React.useState([]); const [isLoading, setIsLoading] = React.useState(true); const loadMoviesData = () => { setIsLoading(true); loadMovies().then((movieData) => { setMovies(movieData); setIsLoading(false); }); }; React.useEffect(loadMoviesData, []); return { movies, isLoading, loadMoviesData, }; }
-
Pass
loadMoviesData
function toMovieForm
component asonSubmitSuccess
props;function App() { const [moviesShown, toggleShowMovies] = useToggle(false); const { movies, isLoading, loadMoviesData } = useMovieData(); return ( <div> <TitleBar> <h1>React Movie App</h1> </TitleBar> <div className="container"> <div> <div className="button-container"> <Button onClick={toggleShowMovies}>{moviesShown ? 'Hide' : 'Show'} Movies</Button> </div> {moviesShown && ( <BusyContainer isLoading={isLoading}> {movies.map((movie) => ( <Movie name={movie.name} releaseDate={movie.releaseDate} key={movie.id} /> ))} </BusyContainer> )} </div> </div> <div> <MovieForm onSubmitSuccess={loadMoviesData} /> </div> </div> ); }
-
In
MovieForm
, callonSubmitSuccess
whencreateMovie
ajax succeeds:src/movie-form.jsexport const MovieForm = ({ onSubmitSuccess }) => { const [name, setName] = React.useState(''); const [releaseDate, setReleaseDate] = React.useState(''); const handleSubmit = (ev) => { ev.preventDefault(); createMovie({ name, releaseDate, }).then(() => { onSubmitSuccess(); setName(''); setReleaseDate(''); }); }; // existing code };
Now the movie list should be updated once you submit create movie!
Extract Form State to Custom Hook
Let’s extract out some code in MovieForm
:
import * as React from 'react';
import { createMovie } from './api';
// highlight-start
const useMovieForm = () => {
const [name, setName] = React.useState('');
const [releaseDate, setReleaseDate] = React.useState('');
return {
setName,
setReleaseDate,
values: {
name,
releaseDate,
},
};
};
// highlight-end
export const MovieForm = ({ onSubmitSuccess }) => {
const { values, setName, setReleaseDate } = useMovieForm(); // highlight-line
const handleSubmit = (ev) => {
ev.preventDefault();
createMovie(values).then(() => {
onSubmitSuccess();
setName('');
setReleaseDate('');
});
};
return (
<div className="movie-form">
<form onSubmit={handleSubmit}>
<legend>Create Movie</legend>
<div className="field">
<label htmlFor="name" className="label">
Name
</label>
<input
className="input"
value={values.name}
id="name"
name="name"
onChange={(ev) => setName(ev.target.value)}
required
/>
</div>
<div className="field">
<label htmlFor="releaseDate" className="label">
Release Date
</label>
<input
className="input"
value={values.releaseDate}
id="releaseDate"
name="releaseDate"
type="date"
onChange={(ev) => setReleaseDate(ev.target.value)}
required
/>
</div>
<div className="button-container">
<button type="submit" className="submit-button">
Create
</button>
</div>
</form>
</div>
);
};
- we extract out the two form input states into
useMovieForm
custom hook, and use that inMovieForm
component.
Do It: Create Movie Form
- Create
MovieForm
component will will make API call to backend viacreateMovie
function. - Include
MovieForm
inApp
and make sure creation is working. - Enhance application to auto refresh movie list when creation is success.
- Extract out form data to
useMovieForm
custom hook.