all topics

Forms

Handling form submission

jsx
<form onSubmit={handleAddMovie}>
    <input value={title} onChange={event => setTitle(event.target.value)} placeholder="Movie title" />
    <input value={genre} onChange={event => setGenre(event.target.value)} placeholder="Genre" />
    <button type="submit">Add Movie</button>
</form>
jsx
function handleAddMovie(event) {
    event.preventDefault()
    // ...
}
  • A <form>'s default behavior on submit is to reload the page. event.preventDefault() stops that, so the submit is handled entirely in JS instead.
  • type="submit" on the button is what makes clicking it, or pressing Enter inside one of the inputs, actually trigger the form's onSubmit.
  • Both inputs are controlled the same way as the search box in Filtering, value + onChange, just with their own state (title, genre) instead of search.

Validating before adding

jsx
if (!title.trim() || !genre.trim()) {
    setError("Please enter both title and genre")
    return
}
  • .trim() strips whitespace, so " " doesn't count as a real title. !title.trim() is true for an empty, or whitespace-only, string.
  • Either field failing sets an error message in state and returns early. The rest of handleAddMovie never runs, no movie gets added.

Conditional rendering with &&

jsx
{error && <p>{error}</p>}
  • error empty (falsy) → the whole expression short-circuits to "", React renders nothing.
  • error has text (truthy) → the expression evaluates to the <p>, and that's what renders.
  • This is the standard React way to say "render this, but only if that's true", no if statement needed inside JSX.

Adding a new item to the array

jsx
const newMovie = {
    id: Date.now(),
    title: title.trim(),
    genre: genre.trim(),
    rating: 0,
    watched: false
}
 
setMovies([...movies, newMovie])
  • Date.now() here is just a quick way to get an id that's very unlikely to collide, the current time in milliseconds.
  • [...movies, newMovie] is the "add" version of the same rule from State: never mutate the array directly, movies.push(newMovie) would be a mutation, build a new array instead. Spreading movies copies all the existing items in, then newMovie lands after them.

Resetting the form after a successful add

jsx
setTitle("")
setGenre("")
setError("")
  • Three separate pieces of state, three separate resets. Since the inputs are controlled, setting title and genre back to "" immediately clears the boxes on screen too, there's no separate "clear the input" step.
  • Clearing error matters just as much here: if a previous attempt had failed and left a message behind, a successful add should make it disappear too.

Clamping a value with disabled buttons

jsx
<button disabled={movie.rating <= 0} onClick={() => onRating(movie.id, movie.rating - 1)}>-</button>
<button disabled={movie.rating >= 5} onClick={() => onRating(movie.id, movie.rating + 1)}>+</button>
  • disabled takes a boolean straight from the current state, no separate tracking needed for "can this be clicked". Once rating hits 0, the - button disables itself on the very next render, same idea for + at 5.
  • handleRating is the exact same update pattern as handleWatched back in State, movies.map(...) with a ternary, just overwriting rating instead of watched.