After this exercise, you will be able to:
- Create controlled components in React to manage form inputs.
- Handle form submissions to capture user data.
- Update state immutably by adding new items to an array.
- "Lift state up" to a common parent component.
- Pass functions as props from a parent to a child component to enable child-to-parent communication.
- You will be creating a new React project from scratch for this lab.
- No repository is provided. You will be creating your own.
- You need Node.js installed on your machine.
- Upon completion, create a new repository on GitHub.
- Add your local project to the remote repository.
- Run the following commands:
$ git add .
$ git commit -m "Solved lab"
$ git push origin main
- Share the repository link with your TAs.
For this lab, you will test your code by running the application and observing its behavior in the browser. There are no pre-written unit tests.
To run your React application, run the following command from the root of the project:
$ npm run dev
To see the outputs of console.log
in your JavaScript code, open the Console in the Developer Tools of your browser.
Building on our last lesson where we fetched and displayed data, we'll now focus on adding new data to our application. You will build a simple student management dashboard. The dashboard will display a list of students and include a form to add new students to the list.
This exercise is split into multiple iterations:
- Setup: You will create a new React project and set up the initial student list.
- Controlled Form: You will build a form component with controlled inputs.
- Lifting State Up: You will implement the logic to add a new student from the form to the main list in the parent component.
Let's get our project ready.
-
Create the project. In your terminal, create a new React project using Vite. Name it
lab-react-student-list
.$ npm create vite@latest lab-react-student-list -- --template react-ts
-
Navigate and install.
$ cd lab-react-student-list $ npm install
-
Clean up the project.
- Delete everything inside
src/App.css
. - Replace the content of
src/App.tsx
with a basic functional component.
- Delete everything inside
-
Create initial data.
- Create a new file
src/students.ts
. - Add the following array of student data to this file. We'll use this to populate our initial list.
// src/students.ts export const studentsData = [ { _id: '1a', fullName: 'Anna Smith', image: 'https://randomuser.me/api/portraits/women/85.jpg', phone: '123-456-7890', email: 'anna.smith@example.com', program: 'Web Dev', graduated: true }, { _id: '2b', fullName: 'Ben Carter', image: 'https://randomuser.me/api/portraits/men/32.jpg', phone: '234-567-8901', email: 'ben.carter@example.com', program: 'UX/UI', graduated: false } ];
- Create a new file
First, let's display the initial list of students.
-
Define the Student type. It's a good practice to define the shape of our data. At the top of
src/App.tsx
, create aStudent
type. -
Import and manage state.
- In
App.tsx
, importuseState
from React and thestudentsData
from./students.ts
. - Create a state variable called
students
and initialize it withstudentsData
.
- In
-
Render the list.
- Map over the
students
state array. - For each student, render their image, full name, program, email, and phone number.
- Remember to use the student's
_id
as thekey
for each list item. - Add some basic CSS in
App.css
to style the list and student cards.
- Map over the
Click for Solution
src/App.jsx
import { useState } from 'react';
import './App.css';
import { studentsData } from './students';
function App() {
const [students, setStudents] = useState(studentsData);
return (
<div className="App">
<h1>Student List</h1>
<div className="student-list">
{students.map(student => (
<div key={student._id} className="student-card">
<img src={student.image} alt={student.fullName} />
<h3>{student.fullName}</h3>
<p>Program: {student.program}</p>
<p>Email: {student.email}</p>
<p>Phone: {student.phone}</p>
<p>Graduated: {student.graduated ? 'Yes' : 'No'}</p>
</div>
))}
</div>
</div>
);
}
export default App;
src/App.css
.student-list {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 20px;
padding: 20px;
}
.student-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1rem;
width: 250px;
text-align: center;
background-color: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.student-card img {
width: 100px;
height: 100px;
border-radius: 50%;
object-fit: cover;
}
Now, let's build the form for adding new students. We'll create a separate component for this.
-
Create the
AddStudentForm
component.- Create a new folder
src/components
. - Inside it, create a new file
AddStudentForm.jsx
. - In this file, create a functional component that returns a
<form>
.
- Create a new folder
-
Build the form structure. The form should have
input
fields for:- Full Name (
text
) - Image URL (
url
) - Phone (
tel
) - Email (
email
) - A
select
dropdown for the Program (with options like "Web Dev", "UX/UI", "Data"). - A
checkbox
for "Graduated". - A
button
of typesubmit
.
- Full Name (
-
Create state for each input.
- Inside
AddStudentForm
, useuseState
to create a separate state variable for each form field (fullName
,image
,phone
,email
,program
,graduated
).
- Inside
-
Make the form controlled.
- For each input, connect it to its corresponding state variable.
- The
value
of the input should be the state variable. - The
onChange
event handler should update the state variable. For the checkbox, you'll useevent.target.checked
.
Tip
This pattern is called a controlled component. React state is the "single source of truth" for the form's data. This makes it easy to read, validate, and manage form data.
Click for Solution
src/components/AddStudentForm.jsx
import { useState } from 'react';
function AddStudentForm() {
const [fullName, setFullName] = useState('');
const [image, setImage] = useState('');
const [phone, setPhone] = useState('');
const [email, setEmail] = useState('');
const [program, setProgram] = useState('');
const [graduated, setGraduated] = useState(false);
return (
<form>
<h2>Add New Student</h2>
<div>
<label>Full Name:</label>
<input type="text" value={fullName} onChange={e => setFullName(e.target.value)} />
</div>
<div>
<label>Image URL:</label>
<input type="url" value={image} onChange={e => setImage(e.target.value)} />
</div>
<div>
<label>Phone:</label>
<input type="tel" value={phone} onChange={e => setPhone(e.target.value)} />
</div>
<div>
<label>Email:</label>
<input type="email" value={email} onChange={e => setEmail(e.target.value)} />
</div>
<div>
<label>Program:</label>
<select value={program} onChange={e => setProgram(e.target.value)}>
<option value="">-- Please select --</option>
<option value="Web Dev">Web Dev</option>
<option value="UX/UI">UX/UI</option>
<option value="Data">Data</option>
</select>
</div>
<div>
<label>Graduated:</label>
<input type="checkbox" checked={graduated} onChange={e => setGraduated(e.target.checked)} />
</div>
<button type="submit">Add Student</button>
</form>
);
}
export default AddStudentForm;
The form is ready, but it doesn't do anything yet. The AddStudentForm
component has the form data, but the App
component owns the list of students. We need to get the data from the child (AddStudentForm
) to the parent (App
). This is a classic React pattern called "Lifting State Up".
-
Create an
addStudent
function inApp.jsx
.- This function will accept one argument: a new student object.
- Inside the function, use the
setStudents
state setter to update the list. Create a new array containing all the old students plus the new one.
[!CAUTION] Never mutate state directly (e.g.,
students.push(newStudent)
). Always create a new array:setStudents([...students, newStudent])
. This is crucial for React to detect changes and re-render correctly. -
Pass the function as a prop.
- Render the
<AddStudentForm />
component insideApp.jsx
. - Pass the
addStudent
function you just created as a prop to the form component. You can name the proponAddStudent
.
- Render the
-
Handle the form submission in
AddStudentForm.jsx
.- Accept the
onAddStudent
function from the props. - Create a
handleSubmit
function that will be called when the form is submitted. - In
handleSubmit
:- Call
event.preventDefault()
to stop the page from reloading. - Create a
newStudent
object using the data from your form's state variables. - Generate a unique ID for the new student. You can use
crypto.randomUUID()
. - Call the
onAddStudent
prop function, passing thenewStudent
object as the argument. - Reset all the form state variables to clear the input fields for the next entry.
- Call
- Accept the
Click for Solution
src/App.jsx
(updated)
import { useState } from 'react';
import './App.css';
import { studentsData } from './students';
import AddStudentForm from './components/AddStudentForm'; // Import the form
function App() {
const [students, setStudents] = useState(studentsData);
// Function to add a new student
const addStudent = newStudent => {
setStudents([...students, newStudent]);
};
return (
<div className="App">
{/* Pass the function as a prop */}
<AddStudentForm onAddStudent={addStudent} />
<h1>Student List</h1>
<div className="student-list">
{students.map(student => (
<div key={student._id} className="student-card">
<img src={student.image} alt={student.fullName} />
<h3>{student.fullName}</h3>
<p>Program: {student.program}</p>
<p>Email: {student.email}</p>
<p>Phone: {student.phone}</p>
<p>Graduated: {student.graduated ? 'Yes' : 'No'}</p>
</div>
))}
</div>
</div>
);
}
export default App;
src/components/AddStudentForm.jsx
(updated)
import { useState } from 'react';
function AddStudentForm({ onAddStudent }) {
const [fullName, setFullName] = useState('');
const [image, setImage] = useState('');
const [phone, setPhone] = useState('');
const [email, setEmail] = useState('');
const [program, setProgram] = useState('');
const [graduated, setGraduated] = useState(false);
const handleSubmit = event => {
event.preventDefault(); // Prevent page reload
const newStudent = {
_id: crypto.randomUUID(), // Generate unique ID
fullName,
image,
phone,
email,
program,
graduated
};
onAddStudent(newStudent); // Call the function from props
// Reset form fields
setFullName('');
setImage('');
setPhone('');
setEmail('');
setProgram('');
setGraduated(false);
};
return (
<form onSubmit={handleSubmit}>
{/* ... form inputs remain the same ... */}
<h2>Add New Student</h2>
<div>
<label>Full Name:</label>
<input type="text" value={fullName} onChange={e => setFullName(e.target.value)} required />
</div>
<div>
<label>Image URL:</label>
<input type="url" value={image} onChange={e => setImage(e.target.value)} required />
</div>
<div>
<label>Phone:</label>
<input type="tel" value={phone} onChange={e => setPhone(e.target.value)} required />
</div>
<div>
<label>Email:</label>
<input type="email" value={email} onChange={e => setEmail(e.target.value)} required />
</div>
<div>
<label>Program:</label>
<select value={program} onChange={e => setProgram(e.target.value)} required>
<option value="">-- Please select --</option>
<option value="Web Dev">Web Dev</option>
<option value="UX/UI">UX/UI</option>
<option value="Data">Data</option>
</select>
</div>
<div>
<label>Graduated:</label>
<input type="checkbox" checked={graduated} onChange={e => setGraduated(e.target.checked)} />
</div>
<button type="submit">Add Student</button>
</form>
);
}
export default AddStudentForm;
Happy coding! ❤️