Hoot Front-End Create a Hoot
Learning objective: By the end of this lesson, students will be able to build a component for creating new hoots.
Overview
In this lesson, we’ll implement the following user story:
👤 As a user, I should be able to create a hoot post.
This will require a <form> component that allows users to create new hoots. Upon submitting a new hoot, the user should be redirected back to the ‘List’ page.
We’ll make a POST request to our back-end server to create a hoot. When a request is made, we’ll use the response to update the hoots state held in the App component. This data will then flow down to the HootList component, where we will be able to see our newly added hoot.
Scaffold the component
-
First, let’s add a new link to the
NavBarcomponent. It should direct users to/hoots/new.// src/components/NavBar/NavBar.jsx return ( <nav> {user ? ( <ul> <li><Link to='/'>HOME</Link></li> <li><Link to='/hoots'>HOOTS</Link></li> {/* Add the NEW HOOT link */} <li><Link to='/hoots/new'>NEW HOOT</Link></li> <li><Link to='/' onClick={handleSignOut}>Sign Out</Link></li> </ul> ) : ( <ul> <li><Link to='/'>HOME</Link></li> <li><Link to='/sign-in'>SIGN IN</Link></li> <li><Link to='/sign-up'>SIGN UP</Link></li> </ul> )} </nav> ); -
Next, add a corresponding route to
App.jsx. We don’t have a component to render yet, but that’s ok.If you ever wish to test out your client-side routes before creating the component, you can define the
<Route>and render a single element, like in the example below:<Route path='/hoots/new' element={<h1>New Hoot</h1>} />💡 Notice how we use RESTful/resourceful routing conventions in our client-side routes. This isn’t a requirement, but sticking to familiar conventions can be helpful when collaborating with other developers.
-
Next, create the component.
Run the following commands in your terminal:
mkdir src/components/HootForm touch src/components/HootForm/HootForm.jsxAdd the following to
src/components/HootForm/HootForm.jsx:// src/components/HootForm/HootForm.jsx import { useState } from 'react'; const HootForm = (props) => { const [formData, setFormData] = useState({ title: '', text: '', category: 'News', }); const handleChange = (evt) => { setFormData({ ...formData, [evt.target.name]: evt.target.value }); }; const handleSubmit = (evt) => { evt.preventDefault(); console.log('formData', formData); // We'll update this function shortly... }; return ( <main> <form onSubmit={handleSubmit}> <label htmlFor='title-input'>Title</label> <input required type='text' name='title' id='title-input' value={formData.title} onChange={handleChange} /> <label htmlFor='text-input'>Text</label> <textarea required type='text' name='text' id='text-input' value={formData.text} onChange={handleChange} /> <label htmlFor='category-input'>Category</label> <select required name='category' id='category-input' value={formData.category} onChange={handleChange} > <option value='News'>News</option> <option value='Games'>Games</option> <option value='Music'>Music</option> <option value='Movies'>Movies</option> <option value='Sports'>Sports</option> <option value='Television'>Television</option> </select> <button type='submit'>SUBMIT</button> </form> </main> ); }; export default HootForm;This component is similar to other forms you’ve seen in React, but let’s take a closer look at the
<select>tag:- The
<select>element creates dropdown menus in React. Itsvalueis controlled by theformData.categorystate, meaning it updates automatically when the state changes. - Each
<option>tag has a predefinedvalueattribute (for example,'News','Games'). The initial value of the dropdown is set by the defaultcategoryvalue in theformDatastate. - If your back-end uses an
enumconstraint in the database schema for this field, ensure thevalueattributes on the<option>tags match the values defined in your schema. This consistency prevents errors when submitting the form.
❓ Notice our
handleSubmitfunction. Why do we need e.preventDefault() when we submit a<form>in React? What default behavior are we preventing here? - The
-
Let’s import the new
HootFormcomponent into theAppcomponent:// src/App.jsx import HootForm from './components/HootForm/HootForm'; -
Update your route in
App.jsxto render the newHootFormcomponent.<Route path='/hoots/new' element={<HootForm />} />
Take a moment to verify that you can successfully change the formData state. When you submit the form, you should only see a console.log of state, as we have not yet built the logic to create a new hoot.
Build the handleAddHoot() function
To make our form fully functional, we’ll need to build out a handleAddHoot() function.
First, let’s import the useNavigate() hook from react-router. This will allow us to redirect a user back to the hoot list page after submitting a new hoot.
Import useNavigate at the top of src/App.jsx:
// src/App.jsx
import { Routes, Route, useNavigate } from 'react-router';
Next, create a new instance of the useNavigate() hook within the component function:
// src/App.jsx
const navigate = useNavigate();
Add the following function:
// src/App.jsx
const handleAddHoot = async (hootFormData) => {
console.log('hootFormData', hootFormData);
navigate('/hoots');
};
At this point, we’ll just confirm that the hootFormData is passed to the function and that useNavigate() is functioning correctly.
With the function in place, update your route by passing the new handler function to the HootForm component:
// src/App.jsx
<Route
path='/hoots/new'
element={<HootForm handleAddHoot={handleAddHoot} />}
/>
Now that we are passing down handleAddHoot() as props, we can finish building out the handleSubmit() function in HootForm.jsx:
// src/components/HootForm/HootForm.jsx
const handleSubmit = (evt) => {
evt.preventDefault();
props.handleAddHoot(formData);
};
🚨 Be sure to pass in
formDatastate when callinghandleAddHoot().
Test the form
Verify that the hootFormData is being passed up the component tree to the App component correctly. When you submit the form, you should see a console.log() from the handleAddHoot() function and be redirected to the hoot list page.
Build the service function
Now, let’s create the create() service function, which uses a POST request. Unlike GET requests, POST requests with the Fetch API require additional properties:
method: Specifies the HTTP method. ForPOSTrequests, this must be explicitly set.body: Contains the form data, converted to JSON usingJSON.stringify(). Learn more about the JSON object.'Content-Type': Specifies the data type in theheadersas'application/json', making sure the server correctly interprets the request body.
Let’s add the service:
// src/services/hootService.js
const create = async (hootFormData) => {
try {
const res = await fetch(BASE_URL, {
method: 'POST',
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(hootFormData),
});
return res.json();
} catch (error) {
console.log(error);
}
};
export {
index,
show,
create
};
Call the service
Back in src/App.jsx, update handleAddHoot() with the service function:
// src/App.jsx
const handleAddHoot = async (hootFormData) => {
const newHoot = await hootService.create(hootFormData);
setHoots([newHoot, ...hoots]);
navigate('/hoots');
};
Notice how when we
setHoots(), thenewHootis added to the front of the array, ensuring it appears at the top of the page. This matches the behavior of ourindex()function, which retrieveshootsin descending order (newest first). AddingnewHootto the end would disrupt this order when the page refreshes, as theindex()service re-fetches the data.
Test the form in your browser. You should now be able to add new hoots!