DEV Community

Alexander Kazanski
Alexander Kazanski

Posted on

I Rebuilt My Habit Tracker's Core Logic as One Hook. Here's What I Learned.

Habit and streak tracking looks simple until you actually build it: timezones, missed days, "does logging at 1am count for yesterday," and the eternal question of whether a streak breaks at midnight or at the user's personal reset time. Most of that complexity belongs in a hook, not scattered across your components.

Extracting the logic

function useStreak(entries, { resetHour = 0 } = {}) {
  const adjustedDate = (timestamp) => {
    const d = new Date(timestamp);
    d.setHours(d.getHours() - resetHour);
    return d.toDateString();
  };

  const streak = useMemo(() => {
    const days = [...new Set(entries.map((e) => adjustedDate(e.loggedAt)))].sort();
    let current = 0;
    for (let i = days.length - 1; i >= 0; i--) {
      const expected = new Date();
      expected.setDate(expected.getDate() - (days.length - 1 - i));
      if (days[i] === adjustedDate(expected.getTime())) current++;
      else break;
    }
    return current;
  }, [entries, resetHour]);

  return { streak };
}
Enter fullscreen mode Exit fullscreen mode

The resetHour parameter matters more than it looks. A user who works night shifts and logs a midnight workout shouldn't lose their streak because your app assumes everyone's day ends at 12am.

Why this belongs in a hook, not a utility function

You could write this as a plain function and call it from useMemo in every component that needs it. The reason to wrap it as a hook is composability — you can layer useStreak with other hooks (useNotificationSchedule, useMilestone) without threading the same recalculation logic through five components. It also makes this logic trivially unit-testable in isolation from any UI.

The honest limitation

Streaks are a motivational device, not a health metric, and they can push people toward all-or-nothing thinking — one missed day feels like failure instead of a data point. If you're building this for a health-adjacent product, consider surfacing "longest streak" alongside "current streak," so a broken streak doesn't erase the user's sense of progress. Small framing decision, real behavioral impact.

Top comments (0)