Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Tracking Button Clicks with `react-tracker`

Hello! Let's dive into the final lesson of our module on Zustand's ecosystem.

Introduction

In our last session, we focused on tracking the continuous flow of data within your application by monitoring changes to your Zustand store. We explored both a manual approach with subscribe and an automated one using a plugin like @openreplay/tracker-zustand. This gave us a powerful way to understand how the application's state evolves over time.

Today, we shift our focus from passive state monitoring to actively tracking discrete user interactions. This lesson addresses the learning outcome: Implement event tracking for discrete user interactions, such as button clicks, using react-tracker hooks.

While state changes can tell you what the application's data looks like, tracking discrete events tells you what the user is doing. We'll explore how to use hook-based tracking libraries to capture actions like button clicks, form submissions, and other specific user-initiated events. This is essential for building user funnels, analyzing feature engagement, and conducting A/B tests.

We will examine two different libraries that exemplify the modern, hook-based approach to event tracking in React.

Tracking Discrete Events with Hooks

In the previous lesson, we saw how to track state changes, which often happen as a result of a user's action. Today, we're interested in tracking the action itself. For example, when a user clicks a "Add to Cart" button:

  • State Change Tracking (Previous Lesson): An analytics event is fired because the cart array in your Zustand store was updated.
  • Event Tracking (This Lesson): An analytics event is fired simply because the button was clicked, regardless of whether the state changed (e.g., the cart might have been full, preventing a state update).

Both are valuable, but they serve different purposes. Event tracking is typically implemented by calling a tracking function directly within an event handler, like onClick or onSubmit. Modern libraries provide a custom hook that makes this process clean and declarative.

A Modern Approach: @offlegacy/event-tracker

Let's start with a library called @offlegacy/event-tracker. It provides a useTracker hook that gives you access to specific, pre-configured tracking functions.

First, you define your tracker, typically in a central file. This is where you connect your analytics service (like Google Analytics, Mixpanel, etc.).

// tracker.js
import { createTracker } from '@offlegacy/event-tracker';
import analytics from './analytics-service'; // Your analytics service wrapper

export const [Track, useTracker] = createTracker({
  // Define an event name, e.g., 'onClick'
  onClick: (params) => {
    // This function sends the data to your analytics service
    analytics.track('user_click', params);
  },
  // You can define other events too
  onImpression: (params) => {
    analytics.track('element_impression', params);
  }
});

Once the tracker is created, you can use the useTracker hook inside any component to get access to the tracking methods.

The following reading from the official documentation explains the useTracker hook and its primary function, track.

Hook - Event Tracker

This documentation for event-tracker introduces the useTracker hook and shows how to use its track object to fire events. It provides clear examples for tracking clicks and other interactions.

Please read the introduction to the useTracker hook, then focus on the track subsection to see how to call event functions like track.onClick. Finally, review the 'Basic' example at the end to see how useTracker is used within a full component.

As you saw in the documentation, the implementation is straightforward.

  1. Import the hook: import { useTracker } from './tracker';
  2. Get the track object: const { track } = useTracker();
  3. Call the event function: In an event handler, you call the method corresponding to the event name you defined, passing any relevant data.

Here is a practical example of a "Follow" button component:

import React from 'react';
import { useTracker } from './tracker';

function FollowButton({ userIdToFollow }) {
  const { track } = useTracker();

  const handleFollowClick = () => {
    // 1. Call the tracking function with a payload
    track.onClick({ 
      buttonName: 'Follow', 
      targetUserId: userIdToFollow 
    });

    // 2. Execute the component's primary logic
    // (e.g., make an API call to follow the user)
    console.log(`Following user ${userIdToFollow}...`);
  };

  return (
    <button onClick={handleFollowClick}>
      Follow
    </button>
  );
}

Advanced Features

Your experience as a front-end developer has likely shown you cases where you don't want to fire an event on every single click. The event-tracker library provides useful options for this. The track function accepts a second argument for configuration:

  • Conditional Tracking: Use enabled to fire an event only if a condition is met.
  • Debouncing/Throttling: Use debounce or throttle to limit how often an event can be fired, which is useful for search inputs or rapid-fire clicks.
// Example from the docs you read
// Only track if the user is a premium member
track.onClick(
  { buttonId: "premium" },
  {
    enabled: (context) => context.user?.isPremium,
  },
);

// Example of debouncing a search input event
track.onSearch(
  { query: '...'},
  {
    debounce: { delay: 300 },
  }
)

The library also offers a trackWithSchema function for type-safe tracking, which helps ensure data consistency—a significant benefit in large-scale applications.

An Alternative: nytimes/react-tracking

Another widely-used library is react-tracking from The New York Times. It takes a slightly different, more flexible approach. Instead of pre-defined event functions, it provides a generic trackEvent function.

Let's examine its hook-based API.

nytimes/react-tracking: 🎯 Declarative tracking for React apps.

The react-tracking README provides a great overview of its hook-based usage. This section will show you how to use the useTracking hook to get a trackEvent function and provide contextual data.

Please read the section 'Usage with React Hooks'. Pay close attention to how useTracking is called with an object to provide context (e.g., { page: 'FooPage' }), how the trackEvent function is used in an onClick handler, and how the returned <Track> component is used to wrap the component's JSX.

Let's break down the pattern from react-tracking:

  1. Get tracking tools: const { Track, trackEvent } = useTracking({ page: 'ProfilePage' });

    • You call useTracking with an object that becomes the context for all events fired within this component and its children.
    • It returns a generic trackEvent function and a Track component.
  2. Wrap your JSX: return <Track>...</Track>;

    • Wrapping your component's output with <Track> makes the context you defined available to all child components. This is how react-tracking handles nested contextual data.
  3. Fire the event: trackEvent({ action: 'click', buttonName: 'Follow' });

    • Inside an event handler, you call trackEvent with a payload describing the specific action.

The final tracking data sent to your analytics service is a deep merge of all contexts from parent components plus the payload from the trackEvent call.

Here's the same "Follow" button, but implemented with react-tracking:

import React from 'react';
import { useTracking } from 'react-tracking';

// Assume a parent component has already set the page context, e.g., { page: 'UserProfile' }

function FollowButton({ userIdToFollow }) {
  // We can add more context here if needed, or leave it empty
  const { trackEvent } = useTracking({ component: 'FollowButton' });

  const handleFollowClick = () => {
    // The final event will be a merge of parent context, this component's context,
    // and this payload.
    trackEvent({ 
      action: 'follow_click', 
      targetUserId: userIdToFollow 
    });
    
    console.log(`Following user ${userIdToFollow}...`);
  };

  // Because this is a 'leaf' component with no children that need tracking,
  // we don't strictly need to wrap with <Track>.
  return (
    <button onClick={handleFollowClick}>
      Follow
    </button>
  );
}

If this FollowButton were used in a UserProfile component, the final event payload might look like this:
{ page: 'UserProfile', component: 'FollowButton', action: 'follow_click', targetUserId: '...' }

Conclusion

Today, we complemented our knowledge of state tracking by learning how to track discrete user interactions. This gives you a complete picture of how to implement comprehensive analytics in a modern React application using Zustand.

Key Takeaways:

  • Event Tracking vs. State Tracking: Event tracking captures specific user actions (like clicks), while state tracking monitors the resulting data changes.
  • Hook-Based Pattern: The common approach is to use a hook (useTracker, useTracking) to get a tracking function that you call inside event handlers.
  • @offlegacy/event-tracker: Offers a structured approach with named event functions (track.onClick), built-in support for debouncing/throttling, and optional schema validation.
  • nytimes/react-tracking: Provides a flexible, generic trackEvent function and uses a powerful context system via a <Track> wrapper component to pass data down the component tree.

Choosing between these libraries depends on your team's preference for structure versus flexibility. Both are powerful tools for understanding user behavior.

Preview of the Next Lesson:

We have now covered Zustand's features, advanced patterns, middleware, performance optimizations, and its integration into the wider analytics ecosystem. With this solid foundation, we are ready to tackle the practical challenges of introducing Zustand into a real-world project.

In our next module, "Migration Strategy and Team Adoption," we will begin by learning how to define criteria for selecting candidate features or component subtrees for an initial migration from React Context to Zustand. This will be our first step in building a strategic, low-risk plan for adopting Zustand in an existing codebase.

Can't find a good explanation? Sign up and we'll make it for you

Sign up