DEV Community

Cover image for Debugging this stuff
Chinwuba
Chinwuba

Posted on

Debugging this stuff

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");
}, []);
Enter fullscreen mode Exit fullscreen mode

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();
}, []);
Enter fullscreen mode Exit fullscreen mode

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 () => {
    ...
  };
}, []);
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

The project array isn't inside response.

It's inside:

response.data
Enter fullscreen mode Exit fullscreen mode

This mistake caused me to store the entire Axios response inside React state.

Instead of:

setProjects(response);
Enter fullscreen mode Exit fullscreen mode

it should be:

setProjects(response.data);
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

Yet I was making requests like:

API.get("/api/projects");
Enter fullscreen mode Exit fullscreen mode

Which produced:

/api/api/projects
Enter fullscreen mode Exit fullscreen mode

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" />
}
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

My frontend expected:

const { user, token } = res.data;
Enter fullscreen mode Exit fullscreen mode

Because token didn't exist, Zustand stored:

undefined
Enter fullscreen mode Exit fullscreen mode

Then every protected route saw:

token === undefined
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

with actions like:

login()

logout()

setNotifications()
Enter fullscreen mode Exit fullscreen mode

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:

[]
Enter fullscreen mode Exit fullscreen mode

with:

200 OK
Enter fullscreen mode Exit fullscreen mode

Instead I received:

401 Unauthorized
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
frank_signorini profile image
Frank

Totally agree on the misconception! I've found