Profile Page
Separate Concerns
Let us refactor the conditional logic to be cleaner
Let us look at the code with the problem highlighted:
import { useEffect, useState } from 'react';
import './App.css';
type Profile = {
firstName: string;
lastName: string;
age: number;
companyName: string;
};
function App() {
const [profile, setProfile] = useState<Profile | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [editMode, setEditMode] = useState(false);
const [formData, setFormData] = useState<Profile>({
firstName: '',
lastName: '',
age: 0,
companyName: '',
});
useEffect(() => {
setTimeout(() => {
const fakeData: Profile = {
firstName: 'John',
lastName: 'Doe',
age: 30,
companyName: 'OpenAI',
};
setProfile(fakeData);
setFormData(fakeData);
setIsLoading(false);
}, 1000);
}, []);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData((prev) => ({
...prev,
[e.target.name]: e.target.value,
}));
};
const handleSave = () => {
setTimeout(() => {
setProfile(formData);
setEditMode(false);
}, 1000);
};
if (!profile || isLoading) return <p>Loading profile...</p>;
return (
<main>
<section className="section">
<h1>Profile Page</h1>
{editMode ? (
<div className="profile-form">
<label>
First Name:
<input
name="firstName"
value={formData.firstName}
onChange={handleChange}
/>
</label>
<label>
Last Name:
<input
name="lastName"
value={formData.lastName}
onChange={handleChange}
/>
</label>
<label>
Age:
<input
name="age"
type="number"
value={formData.age}
onChange={handleChange}
/>
</label>
<label>
Company Name:
<input
name="companyName"
value={formData.companyName}
onChange={handleChange}
/>
</label>
<button onClick={handleSave}>Save</button>
</div>
) : (
<div className="profile-display">
<p>
<strong>First Name:</strong> {profile.firstName}
</p>
<p>
<strong>Last Name:</strong> {profile.lastName}
</p>
<p>
<strong>Age:</strong> {profile.age}
</p>
<p>
<strong>Company:</strong> {profile.companyName}
</p>
<button onClick={() => setEditMode(true)}>Edit Profile</button>
</div>
)}
</section>
</main>
);
}
export default App;Separating out the UI
The editMode state is used to determine whether the form or the display is rendered. Meaning we can create two dedicated components for the form and the display.
import { useEffect, useState } from 'react';
import './App.css';
type Profile = {
firstName: string;
lastName: string;
age: number;
companyName: string;
};
function ProfileDisplay({
profile,
setEditMode,
}: {
profile: Profile;
setEditMode: (value: boolean) => void;
}) {
return (
<div className="profile-display">
<p>
<strong>First Name:</strong> {profile.firstName}
</p>
<p>
<strong>Last Name:</strong> {profile.lastName}
</p>
<p>
<strong>Age:</strong> {profile.age}
</p>
<p>
<strong>Company:</strong> {profile.companyName}
</p>
<button onClick={() => setEditMode(true)}>Edit Profile</button>
</div>
);
}
function ProfileForm({
formData,
handleChange,
handleSave,
}: {
formData: Profile;
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
handleSave: () => void;
}) {
return (
<div className="profile-form">
<label htmlFor="firstName">First Name:</label>
<input
type="text"
id="firstName"
name="firstName"
value={formData.firstName}
onChange={handleChange}
/>
<label htmlFor="lastName">Last Name:</label>
<input
type="text"
id="lastName"
name="lastName"
value={formData.lastName}
onChange={handleChange}
/>
<label htmlFor="age">Age:</label>
<input
type="number"
id="age"
name="age"
value={formData.age}
onChange={handleChange}
/>
<label htmlFor="companyName">Company:</label>
<input
type="text"
id="companyName"
name="companyName"
value={formData.companyName}
onChange={handleChange}
/>
<button onClick={handleSave}>Save</button>
</div>
);
}
function App() {
const [profile, setProfile] = useState<Profile | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [editMode, setEditMode] = useState(false);
const [formData, setFormData] = useState<Profile>({
firstName: '',
lastName: '',
age: 0,
companyName: '',
});
useEffect(() => {
setTimeout(() => {
const fakeData: Profile = {
firstName: 'John',
lastName: 'Doe',
age: 30,
companyName: 'OpenAI',
};
setProfile(fakeData);
setFormData(fakeData);
setIsLoading(false);
}, 1000);
}, []);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData((prev) => ({
...prev,
[e.target.name]: e.target.value,
}));
};
const handleSave = () => {
setTimeout(() => {
setProfile(formData);
setEditMode(false);
}, 1000);
};
if (!profile || isLoading) return <p>Loading profile...</p>;
return (
<main>
<section className="section">
<h1>Profile Page</h1>
{editMode ? (
<ProfileForm
formData={formData}
handleChange={handleChange}
handleSave={handleSave}
/>
) : (
<ProfileDisplay profile={profile} setEditMode={setEditMode} />
)}
</section>
</main>
);
}
export default App;Feature Folder
Great! But let us put them in a dedicated feature folder for better organization.
App.tsx
App.css
main.tsx
index.css
- We will also separate out the
Profiletype to its own file and export it asProfileType. The suffix ofTypeis a convention to indicate that it is a type and helps not cause naming conflicts. - We will then create two files,
profile-display.tsxandprofile-form.tsx, which will hold the UI components for the profile page. - Finally, we import them in
App.tsxand use them accordingly.
export type ProfileType = {
firstName: string;
lastName: string;
age: number;
companyName: string;
};import type { ProfileType } from './types';
export default function ProfileDisplay({
profile,
setEditMode,
}: {
profile: ProfileType;
setEditMode: (value: boolean) => void;
}) {
return (
<div className="profile-display">
<p>
<strong>First Name:</strong> {profile.firstName}
</p>
<p>
<strong>Last Name:</strong> {profile.lastName}
</p>
<p>
<strong>Age:</strong> {profile.age}
</p>
<p>
<strong>Company:</strong> {profile.companyName}
</p>
<button onClick={() => setEditMode(true)}>Edit Profile</button>
</div>
);
}import type { ProfileType } from './types';
export default function ProfileForm({
formData,
handleChange,
handleSave,
}: {
formData: ProfileType;
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
handleSave: () => void;
}) {
return (
<div className="profile-form">
<label htmlFor="firstName">First Name:</label>
<input
type="text"
id="firstName"
name="firstName"
value={formData.firstName}
onChange={handleChange}
/>
<label htmlFor="lastName">Last Name:</label>
<input
type="text"
id="lastName"
name="lastName"
value={formData.lastName}
onChange={handleChange}
/>
<label htmlFor="age">Age:</label>
<input
type="number"
id="age"
name="age"
value={formData.age}
onChange={handleChange}
/>
<label htmlFor="companyName">Company:</label>
<input
type="text"
id="companyName"
name="companyName"
value={formData.companyName}
onChange={handleChange}
/>
<button onClick={handleSave}>Save</button>
</div>
);
}import { useEffect, useState } from 'react';
import './App.css';
import ProfileDisplay from './features/profile/profile-display';
import ProfileForm from './features/profile/profile-form';
import type { ProfileType } from './features/profile/types';
function App() {
const [profile, setProfile] = useState<ProfileType | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [editMode, setEditMode] = useState(false);
const [formData, setFormData] = useState<ProfileType>({
firstName: '',
lastName: '',
age: 0,
companyName: '',
});
useEffect(() => {
setTimeout(() => {
const fakeData: ProfileType = {
firstName: 'John',
lastName: 'Doe',
age: 30,
companyName: 'OpenAI',
};
setProfile(fakeData);
setFormData(fakeData);
setIsLoading(false);
}, 1000);
}, []);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData((prev) => ({
...prev,
[e.target.name]: e.target.value,
}));
};
const handleSave = () => {
setTimeout(() => {
setProfile(formData);
setEditMode(false);
}, 1000);
};
if (!profile || isLoading) return <p>Loading profile...</p>;
return (
<main>
<section className="section">
<h1>Profile Page</h1>
{editMode ? (
<ProfileForm
formData={formData}
handleChange={handleChange}
handleSave={handleSave}
/>
) : (
<ProfileDisplay profile={profile} setEditMode={setEditMode} />
)}
</section>
</main>
);
}
export default App;Can we improve the code further?
Great, but can we further improve the code?
- Our
App.tsxfile does not need to worry about the formhandleChangeand that logic should be moved to theProfileFormcomponent. - We can also move the respective
formDatastate to theProfileFormcomponent. - Some minor changes will also be required to match the component APIs.
import { useState } from 'react';
import type { ProfileType } from './types';
export default function ProfileForm({
initialFormData,
handleSave,
}: {
initialFormData: ProfileType;
handleSave: (formData: ProfileType) => void;
}) {
const [formData, setFormData] = useState<ProfileType>({
...initialFormData,
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData((prev) => ({
...prev,
[e.target.name]: e.target.value,
}));
};
return (
<div className="profile-form">
<label htmlFor="firstName">First Name:</label>
<input
type="text"
id="firstName"
name="firstName"
value={formData.firstName}
onChange={handleChange}
/>
<label htmlFor="lastName">Last Name:</label>
<input
type="text"
id="lastName"
name="lastName"
value={formData.lastName}
onChange={handleChange}
/>
<label htmlFor="age">Age:</label>
<input
type="number"
id="age"
name="age"
value={formData.age}
onChange={handleChange}
/>
<label htmlFor="companyName">Company:</label>
<input
type="text"
id="companyName"
name="companyName"
value={formData.companyName}
onChange={handleChange}
/>
<button onClick={() => handleSave(formData)}>Save</button>
</div>
);
}We made the following changes to the ProfileForm component:
- It now only takes two props:
initialFormDataandhandleSave. - The
formDatastate is initialized with theinitialFormDataprop. - The
handleSaveprop is updated to pass the form data to the parent component. - The
handleChangelogic is directly moved here.
import { useEffect, useState } from 'react';
import './App.css';
import ProfileDisplay from './features/profile/profile-display';
import ProfileForm from './features/profile/profile-form';
import type { ProfileType } from './features/profile/types';
function App() {
const [profile, setProfile] = useState<ProfileType | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [editMode, setEditMode] = useState(false);
useEffect(() => {
setTimeout(() => {
const fakeData: ProfileType = {
firstName: 'John',
lastName: 'Doe',
age: 30,
companyName: 'OpenAI',
};
setProfile(fakeData);
setIsLoading(false);
}, 1000);
}, []);
const handleSave = (formData: ProfileType) => {
setTimeout(() => {
setProfile(formData);
setEditMode(false);
}, 1000);
};
if (!profile || isLoading) return <p>Loading profile...</p>;
return (
<main>
<section className="section">
<h1>Profile Page</h1>
{editMode ? (
<ProfileForm initialFormData={profile} handleSave={handleSave} />
) : (
<ProfileDisplay profile={profile} setEditMode={setEditMode} />
)}
</section>
</main>
);
}
export default App;We made the following changes in the App.tsx file:
- We removed all the references to the
formDatastate. - We removd the
handleChangelogic. - We added the
initialFormDataprop to theProfileFormcomponent. - The
handleSaveprop is updated to receive the form data from the child component.
Great, in the next chapter we will improve the fake APIs.
At this point, your code should be a good match to the branch of the repository: 1-solution-part-1
Last updated on