Sometimes the best way to understand React patterns is to see them emerge from fixing anti-patterns. When I took on the Atomize Pro refactor, I found a scenario that many React developers will recognize: a working app with increasingly fragile state management.
Understanding Atomize Pro
Atomize Pro is a productivity app that elevates goal tracking beyond simple checkboxes. Users can track progress in four different ways:
Simple checklists for binary tasks
Progress bars for meeting specific numeric goals
Sets for tracking activity repetitions
Three-level blocks for measuring staged progress
These different tracking methods feed into an XP system that gamifies progress, making goal achievement more engaging. Goals are organized into lists, which live under customizable tabs for easy navigation.
Mapping the Current State
Before touching any code, I needed to understand how state was flowing through the application. I mapped out the current structure:
This visualization revealed several critical issues:
State scattered across multiple components
useEffect hooks trying to keep everything synchronized
Derived state being stored unnecessarily
No clear data flow pattern
The code confirmed these problems. Here's the main App component:
This pattern cascaded down to child components. Each Tab maintained its own derived state:
Even individual goal types had their own state management:
The result? Race conditions everywhere. Tab switches would show stale data, XP calculations would be incorrect, and users had to refresh to see updates.
Starting with Tests
Before any refactoring, I needed to document the current behavior. I started with the Tab component, testing the core functionality:
These tests revealed that even basic functionality was fragile - goal filtering and list organization could break depending on the order of state updates.
Designing a Better Architecture
With a clear understanding of the problems, I designed a new state structure:
The key improvements:
Single source of truth for all state
Clear, predictable state updates through reducer
Derived state calculated at render time
Minimal use of effects
Here's the implementation:
The Tab component became much simpler:
The only remaining useEffect handles initial data loading:
What I Actually Learned
Map your state before changing it - visualization reveals patterns
Tests document behavior and catch regressions
Race conditions often indicate poor state management
Derived state rarely belongs in useEffect
A single source of truth eliminates entire categories of bugs
Components are simpler when they focus on rendering
Looking Forward
While the refactor significantly improved the codebase, there's room for optimization. The next step would be implementing Tanstack Query for better:
Cache management
Background updates
Loading states
Error handling
But more importantly, this project reinforced a crucial lesson: question every useEffect. Is it really needed? Could this be derived state? Could we handle this update differently? Often, the answer leads to simpler, more maintainable code.