Skip to main content

State Inspection

Limelight’s state inspector lets you view, track, and debug your application state in real-time. Connect your Zustand or Redux stores with a single line of code and get full visibility into every state change.
State Inspector

Quick Start

Add your stores to the Limelight.connect() call:
import { Limelight } from "@getlimelight/sdk";
import { useUserStore } from "./stores/user";
import { useCartStore } from "./stores/cart";

Limelight.connect({
  stores: {
    user: useUserStore,
    cart: useCartStore,
  },
});
That’s it. Open Limelight and you’ll see every state change as it happens.

Zustand

Limelight works with Zustand out of the box. Pass your store hooks directly:
import { create } from "zustand";

// Your existing store - no changes needed
const useUserStore = create((set) => ({
  user: null,
  isLoading: false,
  login: (user) => set({ user, isLoading: false }),
  logout: () => set({ user: null }),
}));

// Connect to Limelight
Limelight.connect({
  stores: {
    user: useUserStore,
  },
});

Vanilla Stores

If you’re using Zustand’s vanilla stores (created with createStore instead of create), they work the same way:
import { createStore } from "zustand/vanilla";

const userStore = createStore((set) => ({
  user: null,
  login: (user) => set({ user }),
}));

Limelight.connect({
  stores: {
    user: userStore,
  },
});

Action Names

Limelight automatically infers action names from your code. When you call a function like login() that internally calls set(), Limelight captures “login” as the action name.
const useUserStore = create((set) => ({
  user: null,
  // Limelight will show this as "login" in the timeline
  login: (user) => set({ user }),
  // This will show as "logout"
  logout: () => set({ user: null }),
}));
Action names are inferred from the call stack. If Limelight can’t determine the name, it falls back to “set”.

Redux

Connect your Redux store the same way:
import { configureStore } from "@reduxjs/toolkit";
import userReducer from "./slices/user";
import cartReducer from "./slices/cart";

const store = configureStore({
  reducer: {
    user: userReducer,
    cart: cartReducer,
  },
});

Limelight.connect({
  stores: {
    redux: store,
  },
});
Limelight automatically captures Redux action types and payloads:
// Dispatching this action...
dispatch(setUser({ name: "John", email: "[email protected]" }));

// ...shows up in Limelight as:
Action: "user/setUser"
Payload: { name: "John", email: "[email protected]" }

Multiple Stores

You can connect as many stores as you need. Mix Zustand and Redux in the same app:
import { useAuthStore } from "./stores/auth";
import { useCartStore } from "./stores/cart";
import { useUIStore } from "./stores/ui";
import { legacyReduxStore } from "./redux/store";

Limelight.connect({
  stores: {
    auth: useAuthStore,
    cart: useCartStore,
    ui: useUIStore,
    legacy: legacyReduxStore,
  },
});
Use descriptive names for your stores. These names appear in the Limelight UI and help you quickly identify which store an action belongs to.

Configuration Options

Disable State Inspection

If you want to connect stores but temporarily disable state inspection:
Limelight.connect({
  stores: {
    user: useUserStore,
  },
  enableStateInspector: false, // Stores are registered but not tracked
});

Filter Sensitive Data

Use the beforeSend hook to filter or modify state before it’s sent to Limelight:
Limelight.connect({
  stores: {
    user: useUserStore,
  },
  beforeSend: (event) => {
    // Filter out state updates from specific stores
    if (event.phase === "STATE:UPDATE" && event.data.storeId === "sensitive") {
      return null; // Don't send this event
    }

    // Redact sensitive fields
    if (event.phase === "STATE:UPDATE" && event.data.storeId === "user") {
      const state = { ...event.data.state };
      if (state.password) state.password = "[REDACTED]";
      if (state.token) state.token = "[REDACTED]";
      event.data.state = state;
    }

    return event;
  },
});

Throttle High-Frequency Updates

For stores that update very frequently (e.g., mouse position, animations), you may want to filter updates:
let lastMouseUpdate = 0;

Limelight.connect({
  stores: {
    mouse: useMouseStore,
    ui: useUIStore,
  },
  beforeSend: (event) => {
    // Throttle mouse store updates to once per second
    if (event.phase === "STATE:UPDATE" && event.data.storeId === "mouse") {
      const now = Date.now();
      if (now - lastMouseUpdate < 1000) {
        return null;
      }
      lastMouseUpdate = now;
    }
    return event;
  },
});

What Gets Captured

For each state change, Limelight captures:
FieldDescription
Action TypeThe name of the function that triggered the change (Zustand) or the action type (Redux)
PayloadThe data passed to the action (Redux) or the partial state passed to set() (Zustand)
StateThe full state after the change
DiffWhich keys changed and their before/after values
TimestampWhen the change occurred
Stack TraceWhere in your code the change originated

Supported Libraries

Zustand

Full support for create() and createStore(). Action names inferred automatically.

Redux

Full support for Redux Toolkit and vanilla Redux. Captures action types and payloads.
Support for Jotai, Recoil, and MobX is coming soon. Let us know which libraries you’d like to see supported.

Best Practices

The names you pass to stores appear throughout the Limelight UI. Use names that clearly describe what the store contains:
// ✅ Good
stores: {
  auth: useAuthStore,
  shoppingCart: useCartStore,
  userPreferences: usePreferencesStore,
}

// ❌ Avoid
stores: {
  store1: useAuthStore,
  s2: useCartStore,
  data: usePreferencesStore,
}
Limelight infers action names from your function names. Use clear, descriptive names:
// ✅ Good - shows as "addItem" in Limelight
const useCartStore = create((set) => ({
  items: [],
  addItem: (item) => set((s) => ({ items: [...s.items, item] })),
}));

// ❌ Avoid - shows as "set" in Limelight
const useCartStore = create((set) => ({
  items: [],
  update: (item) => set((s) => ({ items: [...s.items, item] })),
}));
Stores that update many times per second can flood the timeline. Use beforeSend to throttle or filter these updates.
Never send passwords, tokens, or PII to Limelight. Use beforeSend to redact sensitive fields before they leave the device.

Troubleshooting

  1. Make sure Limelight.connect() is called after your store is created
  2. Verify the store is passed correctly to the stores object
  3. Check that enableStateInspector is not set to false
  4. Look for errors in the console starting with [Limelight]
Limelight infers action names from the call stack. If your bundler minifies function names in development, the names may not be captured correctly. Solutions: - Ensure your development build doesn’t minify function names - Use named functions instead of arrow functions for actions
  1. Verify the WebSocket connection is established (check for “Connected” status) 2. Make sure you’re not filtering out the events with beforeSend 3. Check that the state is actually changing (Limelight only captures changes)
Limelight is designed to have minimal performance impact:
  • State is captured synchronously but sent asynchronously
  • Diffs are computed on the desktop app, not in your app
  • The SDK automatically disables itself in production builds
If you notice performance issues with high-frequency stores, use beforeSend to throttle updates.

Next Steps

``` ````