Problem
Where can you encounter this problem?
Starter Code
The starter code for this series is here.
After you clone the repo, navigate into the project directory, install the dependencies and start the app:
pnpm install
pnpm devThis is a common problem in many codebases. The problem is essentially the tight coupling of the UI and business logic.
Here is the file with the problem:
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;This was indeed a real problem one of our community members encountered. We replicated the issue for a larger audience.
Depending on your experience, you might find several problems here. Here are the problems we found:
1. Conditional Rendering
// Some of the code has been omitted for brevity
function App() {
// Some of the code has been omitted for brevity
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>
);
}
// Some of the code has been omitted for brevity- The check for
!profile || isLoadingis a common pattern. However, it currently renders the same loading message for both conditions, potentially missing a distinct state for an empty profile (e.g., if the API returns no data after loading). - Although conditional rendering is necessary to display different UI elements, the amount of code within the conditional blocks can make the component difficult to read.
2. useEffect
React's useEffect is one of the deadliest hooks for a codebase in our
opinion. It should be avoided whenever possible. From our experience, a
significant percentage of bugs are caused by useEffect.
// Some of the code has been omitted for brevity
function App() {
// Some of the code has been omitted for brevity
useEffect(() => {
setTimeout(() => {
const fakeData: Profile = {
firstName: 'John',
lastName: 'Doe',
age: 30,
companyName: 'OpenAI',
};
setProfile(fakeData);
setFormData(fakeData);
setIsLoading(false);
}, 1000);
}, []);
// Some of the code has been omitted for brevity
}
// Some of the code has been omitted for brevity- The
useEffecthere is necessary for fetching initial data (in this case, from our fake API). However, it should still be abstracted out of the component as it adds business logic directly to the UI component.
3. Missing state and state updates that can go out of sync
// Some of the code has been omitted for brevity
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>;
// Some of the code has been omitted for brevity
}
// Some of the code has been omitted for brevity- The
formDatastate can go out of sync with theprofilestate. - The
isLoadingstate can go out of sync with theprofilestate. - There is no
errorstate. - These issues are quite common when working with API calls and data fetching.
4. Bad Fake APIs
// Some of the code has been omitted for brevity
function App() {
// Some of the code has been omitted for brevity
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);
};
// Some of the code has been omitted for brevity
}
// Some of the code has been omitted for brevity- The fake API implementation is simplistic and does not accurately represent a real API.
setTimeoutcan also result in memory leaks if not handled properly (e.g., if the component unmounts before the timeout completes, a cleanup function would be needed).- When real APIs are introduced, this simplistic mock can lead to increased frontend development and testing time because the mock didn't accurately reflect API behavior.
So, what are the solutions?
In the next few chapters, we will refactor the code using some of the best practices to make it more maintainable.
We will try our best to keep the chapters short and simple, but refactoring always involves a lot of going back and forth.
Roughly speaking, we will:
- Separate out the UI from the business logic.
- Bring in Mock Service Worker (MSW) for mocking API calls.
- Use React Query for fetching and mutating data.
We recommend you attempt the refactoring yourself before reading the next chapter. Then come back and read the next chapters to learn more.
Last updated on