WellAlly Logo
WellAlly康心伴
Development

Implementing Custom 'Goal Streak' Logic with React Hooks and LocalStorage

Learn how to build a reusable 'goal streak' custom hook in React. This step-by-step tutorial shows you how to use localStorage for persistence and handle date logic to accurately track daily user habits, perfect for adding gamification to your web apps.

W
2025-12-11
8 min read

Gamification is a powerful tool in web development for boosting user engagement. One of the most effective features is the "streak," which encourages users to return daily to maintain their progress. Think of Duolingo's daily lesson streak or GitHub's contribution graph. In this tutorial, we'll build a custom React hook, useGoalStreak, to track a daily goal completion streak.

We will build a simple application that tracks a daily step goal. The core of our logic will be a custom hook that uses the browser's localStorage to persist the streak data. This ensures that when a user revisits the application, their streak is right where they left it. We'll also handle the logic to reset the streak if a day is missed.

Prerequisites

  • Basic understanding of React and React Hooks (useState, useEffect).
  • Node.js and npm (or yarn) installed on your machine.
  • A code editor like VS Code.

Understanding the Problem

Tracking a daily streak seems simple on the surface, but there are some tricky edge cases to consider:

  • Persistence: How do we remember a user's streak when they close the browser tab?
  • Date Changes: How do we know if the last completion was yesterday or a week ago?
  • Timezones: How can we ensure a "day" is consistent for the user?
  • Reusability: How can we make this logic easy to reuse for different goals?

Our solution will be a custom hook that encapsulates all this complexity, providing a clean and straightforward API to any component that needs it. We will use localStorage for persistence, which is a simple, browser-based storage solution perfect for this kind of non-sensitive data.

Prerequisites

Let's set up a new React project to get started.

code
npx create-react-app goal-streak-tutorial
cd goal-streak-tutorial
Code collapsed

No external libraries are needed for this tutorial; we'll be using standard React Hooks and browser APIs.

Step 1: Creating the useGoalStreak Custom Hook

First, create a new file src/useGoalStreak.js. This is where all our logic will live.

What we're doing

We'll define the structure of our custom hook. It will need to:

  1. Initialize its state from localStorage.
  2. Provide a way to get the current streak count.
  3. Offer a function to mark the daily goal as complete.

Implementation

Here's the basic structure of our hook:

code
// src/useGoalStreak.js
import { useState, useEffect }
from 'react';

export const useGoalStreak = (storageKey) => {
    const [streak, setStreak] = useState(0);

    // More logic will go here

    return {
        streak
    };
};```

### How it works

We're starting with a simple custom hook that takes a `storageKey` as an argument. This will allow us to reuse the hook for different goals by storing their streaks under different keys in `localStorage`. For now, it just returns a hardcoded streak of 0.

## Step 2: Reading from LocalStorage

Now, let's make our hook read from `localStorage` on the initial render.

### What we're doing

We'll use `useState`'s lazy initialization to get the initial state from `localStorage`. This is more efficient because it only runs once, on the initial render. We'll also need to handle the case where there's no data in `localStorage` yet.

### Implementation

```javascript
// src/useGoalStreak.js
import {
    useState
} from 'react';

// A helper function to check if two dates are the same day
const isSameDay = (date1, date2) =>
    date1.getFullYear() === date2.getFullYear() &&
    date1.getMonth() === date2.getMonth() &&
    date1.getDate() === date2.getDate();

export const useGoalStreak = (storageKey) => {
    const [streak, setStreak] = useState(() => {
        try {
            const storedData = window.localStorage.getItem(storageKey);
            if (storedData) {
                const data = JSON.parse(storedData);
                const today = new Date();
                const lastCompletion = new Date(data.lastCompletion);

                // If the last completion was today, we're still on the same streak
                if (isSameDay(today, lastCompletion)) {
                    return data.streak;
                }

                // Check if the last completion was yesterday to continue the streak
                const yesterday = new Date();
                yesterday.setDate(today.getDate() - 1);
                if (isSameDay(yesterday, lastCompletion)) {
                    return data.streak;
                }
            }
        } catch (error) {
            console.error("Error reading from localStorage", error);
        }
        // Reset streak if more than a day has passed
        return 0;
    });

    // ...
    return {
        streak
    };
};
Code collapsed

How it works

  1. We use a function inside useState to lazily initialize our state.
  2. We read from localStorage using the provided storageKey.
  3. If there's data, we parse it and compare the lastCompletion date with today's and yesterday's date.
  4. If the last completion was yesterday, we keep the streak. If it was before that, the streak is broken, and we return 0.
  5. The isSameDay helper function is crucial for accurate date comparisons, ignoring the time part of the Date object.

Step 3: Writing to LocalStorage and Updating the Streak

Next, we need a function to let the user mark their goal as complete for the day.

What we're doing

We'll create a function called markGoalAsCompleted that will be returned by our hook. This function will update the streak count and save the new state to localStorage.

Implementation

code
// src/useGoalStreak.js
import {
    useState
} from 'react';

const isSameDay = (date1, date2) =>
    date1.getFullYear() === date2.getFullYear() &&
    date1.getMonth() === date2.getMonth() &&
    date1.getDate() === date2.getDate();

export const useGoalStreak = (storageKey) => {
    const [streakData, setStreakData] = useState(() => {
        try {
            const storedData = window.localStorage.getItem(storageKey);
            if (storedData) {
                const data = JSON.parse(storedData);
                const today = new Date();
                const lastCompletion = new Date(data.lastCompletion);

                // If last completion was yesterday, the streak continues
                const yesterday = new Date();
                yesterday.setDate(today.getDate() - 1);
                if (isSameDay(yesterday, lastCompletion)) {
                    return { ...data, streak: data.streak };
                }

                // If last completion was today, don't change anything
                if (isSameDay(today, lastCompletion)) {
                    return data;
                }
            }
        } catch (error) {
            console.error(error);
        }
        // Default state
        return {
            streak: 0,
            lastCompletion: null
        };
    });


    const markGoalAsCompleted = () => {
        const today = new Date();
        const lastCompletion = streakData.lastCompletion ? new Date(streakData.lastCompletion) : null;

        // If goal was already completed today, do nothing
        if (lastCompletion && isSameDay(today, lastCompletion)) {
            return;
        }

        const yesterday = new Date();
        yesterday.setDate(today.getDate() - 1);

        const newStreak = (lastCompletion && isSameDay(yesterday, lastCompletion)) ?
            streakData.streak + 1 :
            1;

        const newData = {
            streak: newStreak,
            lastCompletion: today.toISOString(),
        };

        setStreakData(newData);
        window.localStorage.setItem(storageKey, JSON.stringify(newData));
    };

    return {
        streak: streakData.streak,
        markGoalAsCompleted
    };
};
Code collapsed

How it works

  1. The markGoalAsCompleted function first checks if the goal has already been completed today.
  2. It then determines if the lastCompletion was yesterday. If so, it increments the streak. Otherwise, it starts a new streak of 1.
  3. A new data object is created with the updated streak and the current date as the lastCompletion.
  4. This new data is saved to both our React state (with setStreakData) and localStorage.

Putting It All Together

Now let's use our new useGoalStreak hook in a component.

Implementation

Update src/App.js with the following code:

code
// src/App.js
import React from 'react';
import {
    useGoalStreak
} from './useGoalStreak';
import './App.css';

function App() {
    const {
        streak,
        markGoalAsCompleted
    } = useGoalStreak('stepGoal');

    return ( <
        div className = "App" >
        <
        header className = "App-header" >
        <
        h1 > Daily Step Goal Tracker < /h1> <
        div className = "streak-display" >
        Your current streak is: <
        span className = "streak-count" > {
            streak
        } < /span> day(s) 🔥 <
        /div> <
        button onClick = {
            markGoalAsCompleted
        } >
        I met my 10, 000 step goal today!
        <
        /button> <
        /header> <
        /div>
    );
}

export default App;
Code collapsed

And for some basic styling, update src/App.css:

code
/* src/App.css */
.App {
    text-align: center;
}

.App-header {
    background-color: #282c34;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    font-size: calc(10px + 2vmin);
    color: white;
}

.streak-display {
    margin: 20px 0;
}

.streak-count {
    font-size: 1.5em;
    font-weight: bold;
    color: #61dafb;
}

button {
    font-size: calc(8px + 2vmin);
    padding: 10px 20px;
    border-radius: 8px;
    border: none;
    background-color: #61dafb;
    cursor: pointer;
    transition: background-color 0.3s;
}

button:hover {
    background-color: #4fa8c5;
}
Code collapsed

Now, run your app:

code
npm start
Code collapsed

You should see your goal tracker! Click the button to start your streak. Refresh the page or close the tab and reopen it – your streak will still be there. Wait until tomorrow, and you can continue your streak. If you miss a day, it will reset to 0.

Security Best Practices

For this application, localStorage is generally safe. However, remember that localStorage is not secure storage. It's accessible via JavaScript on the same domain, so it's vulnerable to cross-site scripting (XSS) attacks. Never store sensitive information like user tokens or personal data in localStorage. For our goal streak, it's perfectly fine.

Alternative Approaches

  • Session Storage: If you only wanted the streak to persist for the current browser session, you could use sessionStorage instead of localStorage. The API is identical.
  • Backend Database: For a more robust solution that syncs across devices, you would store the streak information in a backend database associated with a user account. This is more complex but necessary for production applications with user authentication.
  • Third-party libraries: Libraries like date-fns can make date comparisons more robust and readable, especially when dealing with timezones.

Conclusion

We've successfully built a reusable custom React hook, useGoalStreak, that encapsulates the logic for tracking a daily goal streak. We've seen how to use localStorage for data persistence and how to handle date-based logic to accurately calculate the streak.

This pattern is incredibly versatile. You can use it to track any daily habit, from writing a journal entry to completing a workout.

Next Steps

  • Try adding another goal to the app with a different storageKey.
  • Implement a "last completed" date display in the UI.
  • Add animations or confetti when the streak increases.

Resources

#

Article Tags

reactjavascripttutorialwebdevhooks
W

WellAlly's core development team, comprised of healthcare professionals, software engineers, and UX designers committed to revolutionizing digital health management.

Expertise

Healthcare TechnologySoftware DevelopmentUser ExperienceAI & Machine Learning

Found this article helpful?

Try KangXinBan and start your health management journey

© 2024 康心伴 WellAlly · Professional Health Management