One of the biggest misconceptions beginners have is that experienced developers don't make mistakes.
The reality is the exact opposite.
Experienced developers simply know how to investigate mistakes faster.
While building a full-stack project management application with React, Zustand, Express, Prisma, PostgreSQL, Axios, and JWT authentication, I encountered dozens of small bugs.
None of them were particularly difficult.
But every single one taught an important lesson about how modern frontend applications actually work.
This article walks through those lessons.
1. React Doesn't Execute Async Effects
One of the first mistakes I made was writing:
useEffect(async () => {
const res = await API.get("/projects");
}, []);
It looks innocent.
It even feels natural.
But React doesn't allow an async function to be passed directly into useEffect.
Why?
Because React expects the callback to either return nothing or return a cleanup function.
An async function always returns a Promise.
Instead, the correct pattern is:
useEffect(() => {
const fetchProjects = async () => {
const res = await API.get("/projects");
setProjects(res.data);
};
fetchProjects();
}, []);
This pattern appears everywhere in React applications.
Learning it early saves a lot of frustration.
2. Defining a Function Isn't the Same as Calling It
Another bug was surprisingly simple.
I wrote:
useEffect(() => {
const fetch = async () => {
...
};
}, []);
Everything looked correct.
Except...
I never called fetch().
React happily mounted the component.
Nothing crashed.
Nothing loaded.
Sometimes bugs aren't complicated.
They're just forgotten function calls.
3. Understanding Axios Responses
Axios returns an object.
Not your data directly.
For example:
const response = await API.get("/projects");
The project array isn't inside response.
It's inside:
response.data
This mistake caused me to store the entire Axios response inside React state.
Instead of:
setProjects(response);
it should be:
setProjects(response.data);
Understanding the structure of third-party libraries is just as important as understanding JavaScript itself.
4. Why API Base URLs Matter
My Axios instance already had:
baseURL: "http://localhost:5000/api"
Yet I was making requests like:
API.get("/api/projects");
Which produced:
/api/api/projects
That tiny duplication resulted in requests hitting endpoints that didn't exist.
A simple reminder:
Know what your abstractions are already doing.
5. React Router Navigation
Initially I attempted:
const enterProject = () => {
return <Navigate to="/projects/1" />
}
Nothing happened.
That's because <Navigate /> is a React component.
It only works during rendering.
Inside event handlers, navigation should use:
const navigate = useNavigate();
navigate("/projects/1");
React Router provides different APIs for different situations.
Choosing the wrong one silently breaks navigation.
6. Protected Routes Aren't Magic
A protected route is simply conditional rendering.
Mine looked like:
const token = useAuthStore(state => state.token);
if (!token) {
return <Navigate to="/login" />;
}
return children;
The important realization was this:
Protected routes don't "know" whether someone is logged in.
They simply check whether authentication state exists.
If your store isn't updated correctly, the protected route behaves exactly as instructed.
7. One Incorrect Property Name Broke Authentication
This was probably my favorite debugging moment.
The backend returned:
{
user,
accessToken
}
My frontend expected:
const { user, token } = res.data;
Because token didn't exist, Zustand stored:
undefined
Then every protected route saw:
token === undefined
and redirected me back to login.
Nothing was wrong with React.
Nothing was wrong with Zustand.
Nothing was wrong with React Router.
The frontend and backend simply disagreed on a property name.
This reinforces an important lesson:
Frontend and backend contracts must always match.
8. Zustand Made Authentication Surprisingly Clean
One thing I really appreciated was Zustand's simplicity.
Instead of passing props through multiple components, authentication lived inside a global store.
A simplified version looked like:
{
user,
token,
notifications
}
with actions like:
login()
logout()
setNotifications()
The login action simultaneously:
- updated React state
- stored credentials
- persisted authentication
Components simply subscribed to the state they needed.
No reducers.
No boilerplate.
No unnecessary complexity.
9. Empty Data Isn't the Same as Unauthorized
While building the notification system, I initially assumed this:
"No notifications probably caused the request to fail."
Not quite.
If there are no notifications, the backend should return:
[]
with:
200 OK
Instead I received:
401 Unauthorized
Why?
Because the Navbar mounted before login.
The notification request fired immediately.
There was no JWT token.
The request failed.
The solution wasn't checking whether notifications existed.
The solution was checking whether the user was authenticated before making the request.
10. Debugging Is a Skill
Perhaps the biggest lesson from this project had nothing to do with React.
It was learning a debugging workflow.
Instead of guessing:
- Check browser console.
- Check Network tab.
- Inspect API response.
- Verify backend contract.
- Inspect state.
- Confirm component lifecycle.
- Follow the data from database to UI.
Every bug became much easier once I stopped guessing and started tracing data.
Final Thoughts
Building software isn't about typing code as quickly as possible.
It's about understanding how data moves through a system.
In this single project I strengthened my understanding of:
- React Hooks
- Zustand
- Axios
- JWT Authentication
- Protected Routes
- Express APIs
- Prisma
- Component lifecycles
- API design
- Navigation
- Debugging techniques
Every small bug revealed another piece of how modern web applications work.
That's what makes building projects so valuable.
Tutorials show you the happy path.
Projects teach you everything else.
If you're learning React, my biggest advice is simple:
Build something real.
The bugs will become your best teachers.
And always build and code as art
Top comments (1)
Totally agree on the misconception! I've found