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 Zustand State with React-Tracker

Hello! Welcome to your next lesson in our course on Zustand.

Introduction

In previous modules, we've covered Zustand's core functionality, from creating stores and managing complex state to integrating middleware and optimizing performance. Now, we'll shift our focus to the broader ecosystem and how Zustand integrates with other powerful libraries.

This lesson addresses the learning outcome: Integrate react-tracker with a Zustand store to monitor specific state changes for analytics purposes.

We'll explore how to capture changes in your Zustand store and send them to an analytics service. This is crucial for understanding user behavior, debugging issues, and gathering product intelligence. We will examine two primary methods:

  1. A manual, flexible approach using the built-in subscribe function, which you may recall from our lesson on performance optimization.
  2. An automated, integrated approach using a dedicated tracking plugin. We'll use @openreplay/tracker-zustand as a concrete example of this pattern.

By the end of this 60-minute lesson, you'll be able to implement analytics tracking for your Zustand stores, choosing the method that best fits your project's requirements.

The Manual Approach: Using subscribe for Analytics

One of the most direct ways to track state changes is by using Zustand's subscribe method. This function allows you to listen for changes to your store's state and execute a callback function, making it a perfect hook for sending analytics events. It operates outside the React component lifecycle, which is ideal for side effects like analytics.

Let's look at a practical example. The following reading demonstrates how to subscribe to specific state slices and trigger analytics calls.

Zustand and TanStack Query: The Dynamic Duo That ...

This article, 'Zustand and TanStack Query', contains a concise section on using subscribe for analytics. It provides a clear, practical code sample for tracking user authentication and cart updates.

Please read the section titled 'Subscriptions and External Updates'. Focus on how subscribe is used to watch specific parts of the state (state.user, state.items) and how the callback function compares the new and previous state to decide when to fire an analytics.track event.

As you saw in the example, the subscribe method is quite powerful. Let's break down the pattern:

// Subscribe to a specific state slice
const unsubscribe = useAppStore.subscribe(
  (state) => state.user, // 1. Selector function
  (user, previousUser) => { // 2. Listener function
    if (user && !previousUser) {
      analytics.track('user_login', { userId: user.id });
    } else if (!user && previousUser) {
      analytics.track('user_logout');
    }
  }
);
  1. Selector Function: The first argument, (state) => state.user, tells subscribe to only listen for changes to the user property of the store. This prevents the listener from running on every single state change, making it highly efficient.
  2. Listener Function: The second argument is the callback that runs when the selected state changes. It receives the new value and the previous value, allowing you to implement logic based on the transition (e.g., detecting a login event when previousUser was null and user is now defined).

The analytics.track() call is a placeholder for whatever analytics library you might be using, such as Google Analytics, Mixpanel, or a custom solution.

Advantages of this approach:

  • Flexibility: It's library-agnostic and gives you complete control over what to track and when.
  • Precision: You can create highly specific tracking logic based on complex state transitions.
  • Minimalism: It uses a built-in Zustand API, requiring no extra dependencies for the integration itself.

Disadvantages:

  • Manual Effort: You must manually write subscription logic for every piece of state you want to track.
  • Scalability: This can become cumbersome and hard to maintain in a large application with many tracked state properties.

The Integrated Approach: Using a Tracking Plugin

For more comprehensive tracking, especially when using a platform that offers features like session replay, a dedicated plugin is often a better choice. These plugins typically use Zustand's middleware capabilities to automatically monitor all state changes in a store.

While the learning outcome mentions the generic term react-tracker, we will use @openreplay/tracker-zustand as a specific, well-documented example. The integration pattern is representative of how many similar tracking plugins work.

Let's walk through the process of integrating this plugin.

Step 1: Installation and Setup

First, the tracker and its Zustand plugin are installed and configured at the application's entry point.

Tracking state changes with the Zustand plugin

This official OpenReplay documentation details how to set up their Zustand plugin. We'll start with the installation and initial configuration.

Please read the sections 'Installing the plugin' and 'Using the plugin on a Next.js app'. Note how the trackerZustand plugin is imported and added to the plugins array within a TrackerProvider. This provider-based configuration is a common pattern in the React ecosystem.

As the article shows, the setup involves wrapping your application with a TrackerProvider and passing the imported trackerZustand plugin to its configuration. This makes the tracker's functionality available throughout your component tree via context.

Step 2: Instrumenting the Zustand Store

This is the core of the integration. The plugin provides a function that wraps your store's definition, effectively acting as middleware to intercept and log state changes.

Tracking state changes with the Zustand plugin

Now, let's look at how the store itself is modified to enable tracking. This is the key step that connects Zustand to the tracking library.

Read the section 'Creating a store with the Zustand Plugin'. Pay close attention to how the create function is wrapped by a logger function: create(logger((set) => ({...}))). This higher-order function pattern is what enables the automatic tracking.

The key takeaway here is the wrapper function, which the article calls logger. By wrapping your store's initializer, you grant the plugin access to the store's set function and its internal state, allowing it to automatically log every update.

The example also uses a singleton pattern to ensure the store is created only once. This is important because the logger function needs to be passed in during the initial creation.

Step 3: Connecting the Plugin and Store in Components

Finally, you need to get the logger function from the TrackerProvider and pass it to your store creation hook.

Tracking state changes with the Zustand plugin

The last step is to connect the provider, the store, and the components. This reading shows how to retrieve the logger from context and use it to initialize the tracked store.

Read the sections 'Adding products to our store' and 'Reading data from the store'. Focus on how useContext(TrackerContext) is used to get pluginsReturnedValues, which then provides the logger function for the store hook: useProductsStore(pluginsReturnedValues.zustand('products_store')).

This completes the circuit:

  1. TrackerProvider initializes the plugin.
  2. useContext retrieves the plugin's functionality (the logger).
  3. The logger is passed to your store's hook, which uses it to create an instrumented store instance.

Once this is set up, any action that updates the store (like addProduct) will have its resulting state change automatically captured by the analytics platform.

Advantages of this approach:

  • Automation: Tracks all state changes automatically with minimal setup per store.
  • Rich Integration: Often provides deeper integration with the analytics platform, such as the session replay view shown above.
  • Scalability: Easily scales to many stores without repetitive boilerplate.

Disadvantages:

  • Vendor Lock-in: The implementation is specific to an analytics provider's plugin.
  • Less Granularity: May be less straightforward to implement highly custom tracking logic compared to the manual subscribe method.

Conclusion

In this lesson, we've explored two effective methods for integrating analytics with a Zustand store to monitor state changes.

Key Takeaways:

  • Manual Tracking with subscribe: This method is ideal for targeted, custom analytics events. It leverages Zustand's built-in API, offers maximum flexibility, and is library-agnostic. It is best suited for cases where you only need to track a few specific, critical state transitions.
  • Integrated Tracking with Plugins: This method is ideal for comprehensive, application-wide state monitoring. By wrapping your store creation with a plugin's middleware, you get automatic tracking of all state changes, which is highly scalable and often provides a richer view in the analytics tool.

The choice between these two patterns depends on your project's specific needs—whether you require surgical precision for a few events or broad, automated coverage for your entire state.

Preview of the Next Lesson:

Tracking state changes gives you a continuous view of your application's data over time. However, sometimes you need to track discrete user interactions that don't necessarily result in a state change (or where the state change itself isn't the point of interest). In our next lesson, we will cover how to implement event tracking for discrete user interactions, such as button clicks, using react-tracker hooks. This will complement what you've learned today, giving you a complete toolkit for application analytics.

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

Sign up