For the past 2–3 days, I worked on a small React project called Workout Timer.
At first glance, the project looks simple:
A timer that calculates total workout duration based on:
- workout type
- number of sets
- speed
- break length
It also shows a live clock and can optionally play a click sound whenever the duration changes.
But the real goal of this project wasn't building a timer.
The real goal was understanding how useEffect actually works and how React performance can be improved by writing cleaner effects.
Why This Project Was Interesting
Many beginners (including me in the past) use useEffect for almost everything.
But this project forced me to think deeper about questions like:
- Should this really be an effect?
- Is this state actually necessary?
- Am I creating unnecessary re-renders?
- Could this be calculated during render instead?
Instead of just writing code, I had to analyze how React behaves internally.
The First Realization: Not Everything Needs useEffect
One of the first tasks was to identify derived state.
Derived state is when a value can be calculated from existing state instead of storing it separately.
Example idea:
If totalWorkoutTime can be calculated from:
- sets
- speed
- break time
Then storing it in state is unnecessary.
Instead, it can be computed during render or with memoization.
This small change alone removes extra effects and unnecessary renders.
That was one of the biggest lessons from this project.
Understanding the Real Purpose of useEffect
Another important takeaway:
useEffect is not for general logic.
It is specifically for synchronizing React with external systems, such as:
- timers (
setInterval) - browser APIs
- media playback
- subscriptions
- DOM interactions
In this project, effects were mainly used for things like:
- updating the live clock
- controlling sound playback
- managing timers
Anything that didn't interact with an external system was a candidate for removal.
The Hidden Problem: Stale Closures
Another interesting concept was closures inside effects.
React effects capture values from the render where they were created.
This can cause bugs when dealing with long-running callbacks like:
- intervals
- timeouts
- event listeners
If the callback uses outdated state, the timer can behave incorrectly.
The solution is usually one of these:
- functional state updates
useRef- properly updated dependency arrays
Understanding this concept helped me see why some React bugs happen in real apps.
Dependency Arrays Are More Important Than I Thought
Another big part of this challenge was auditing dependency arrays.
The rule is simple but strict:
Every value used inside an effect should be listed in the dependency array.
Instead of disabling the ESLint rule, the correct approach is to:
- add missing dependencies
- remove unnecessary ones
- rethink the effect entirely if dependencies keep changing
Sometimes the best fix is removing the effect completely.
Performance Improvements
After fixing the logic, the next step was improving performance.
Some improvements included:
- memoizing expensive values
- stabilizing props passed to memoized components
- avoiding recreated objects during render
- keeping state minimal
- splitting components to control re-render boundaries
Even in a small project like this, you can see how these patterns reduce unnecessary renders.
Measuring Renders
To verify improvements, I temporarily added:
- console logs
- render counters
This helped track how often components were rendering.
After confirming the improvements, those logs were removed.
This simple technique helped me visually understand React rendering behavior.
What This Project Taught Me
This challenge wasn't about building a feature.
It was about thinking like a React developer.
Key lessons:
• useEffect should only sync with external systems
• derived state should not live in React state
• dependency arrays must be accurate
• stale closures can cause subtle bugs
• performance optimization often means writing less state and fewer effects
Final Thoughts
Even though the Workout Timer project is small, it helped me understand some very important concepts:
- React rendering behavior
- effect lifecycle
- closure behavior in hooks
- practical performance optimization
These are the kinds of concepts that separate beginner React code from production-quality React code.
And I still feel like this is just the beginning of mastering them.
Top comments (0)