Skip to main content

Redux Toolkit + Tour Kit: enterprise state management for tours

Build enterprise product tours with Redux Toolkit and Tour Kit. Typed tour slices, time-travel debugging, and multi-tour state for large React apps.

DomiDex
DomiDexCreator of Tour Kit
April 7, 20269 min read
Share
Redux Toolkit + Tour Kit: enterprise state management for tours

Redux Toolkit + Tour Kit: enterprise state management for tours

Most product tour libraries manage their own state internally. Fine for a five-step welcome flow. But twenty tours across six feature modules, three user roles, and eight developers who each need to understand the system? That's where internal state falls apart.

Redux Toolkit gives you centralized, debuggable, typed state management that scales with your team. Tour Kit gives you headless tour logic without prescribing UI. Together they handle enterprise onboarding flows that don't collapse under their own weight.

By the end of this tutorial, you'll have a typed Redux slice managing tour state, a custom hook bridging Tour Kit's provider with your store, and Redux DevTools time-travel debugging for tour flows.

npm install @tourkit/core @tourkit/react @reduxjs/toolkit react-redux

What you'll build

Tour Kit handles the core mechanics: step sequencing, element highlighting, scroll management, keyboard navigation. All inside its TourProvider and useTour() hook. Redux Toolkit adds a coordination layer on top: a tourSlice tracking completion, mid-flow progress, and user segments across features.

Redux owns the what (which tours exist, who sees them, completion state). Tour Kit owns the how (positioning, focus trapping, ARIA announcements, spotlight overlays).

Prerequisites

Before starting, confirm your project meets these requirements. The Redux Toolkit integration works with any React 18+ project that already has or is willing to add RTK to its dependency tree.

  • React 18.2+ or React 19
  • TypeScript 5.0+
  • An existing project with Redux Toolkit configured (or willingness to add it)
  • Familiarity with RTK's createSlice and configureStore

Step 1: install and configure Tour Kit alongside Redux

If your project already has Redux Toolkit and React-Redux, you only need the Tour Kit packages. Tour Kit's core ships at under 8KB gzipped, so the bundle impact is minimal on top of RTK's existing footprint.

// src/store/store.ts
import { configureStore } from '@reduxjs/toolkit'
import { tourSlice } from './slices/tour-slice'

export const store = configureStore({
  reducer: {
    tours: tourSlice.reducer,
    // ...your other slices
  },
})

export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch

Wrap your app with both providers. Order matters: Provider (Redux) goes outside, TourKitProvider inside. Tour Kit's provider reads from its own context, not Redux, so the two coexist without conflicts.

// src/app/providers.tsx
'use client'

import { Provider } from 'react-redux'
import { TourKitProvider } from '@tourkit/react'
import { store } from '@/store/store'

export function AppProviders({ children }: { children: React.ReactNode }) {
  return (
    <Provider store={store}>
      <TourKitProvider>
        {children}
      </TourKitProvider>
    </Provider>
  )
}

Step 2: create a typed tour slice

This is where Redux earns its keep. A tourSlice gives every developer on your team a single place to understand tour state. No hunting through component trees or context values.

// src/store/slices/tour-slice.ts
import { createSlice, type PayloadAction } from '@reduxjs/toolkit'

interface TourProgress {
  currentStep: number
  startedAt: string
  lastActiveAt: string
}

interface TourState {
  completedTours: string[]
  activeTour: string | null
  progress: Record<string, TourProgress>
  userSegment: 'new' | 'returning' | 'power-user'
  dismissedTours: string[]
  tourQueue: string[]
}

const initialState: TourState = {
  completedTours: [],
  activeTour: null,
  progress: {},
  userSegment: 'new',
  dismissedTours: [],
  tourQueue: [],
}

export const tourSlice = createSlice({
  name: 'tours',
  initialState,
  reducers: {
    startTour(state, action: PayloadAction<string>) {
      const tourId = action.payload
      state.activeTour = tourId
      state.progress[tourId] = {
        currentStep: 0,
        startedAt: new Date().toISOString(),
        lastActiveAt: new Date().toISOString(),
      }
    },
    advanceStep(state, action: PayloadAction<string>) {
      const tourId = action.payload
      const progress = state.progress[tourId]
      if (progress) {
        progress.currentStep += 1
        progress.lastActiveAt = new Date().toISOString()
      }
    },
    completeTour(state, action: PayloadAction<string>) {
      const tourId = action.payload
      if (!state.completedTours.includes(tourId)) {
        state.completedTours.push(tourId)
      }
      state.activeTour = null
      delete state.progress[tourId]
      // Auto-dequeue next tour
      if (state.tourQueue.length > 0) {
        state.activeTour = state.tourQueue[0]
        state.tourQueue = state.tourQueue.slice(1)
      }
    },
    dismissTour(state, action: PayloadAction<string>) {
      const tourId = action.payload
      state.dismissedTours.push(tourId)
      state.activeTour = null
      delete state.progress[tourId]
    },
    queueTour(state, action: PayloadAction<string>) {
      if (!state.tourQueue.includes(action.payload)) {
        state.tourQueue.push(action.payload)
      }
    },
    setUserSegment(
      state,
      action: PayloadAction<'new' | 'returning' | 'power-user'>
    ) {
      state.userSegment = action.payload
    },
  },
})

export const {
  startTour,
  advanceStep,
  completeTour,
  dismissTour,
  queueTour,
  setUserSegment,
} = tourSlice.actions

RTK's Immer integration means those direct mutations are safe. The completeTour reducer auto-dequeues the next tour, which handles the common enterprise pattern of chaining onboarding flows: welcome tour finishes, feature discovery tour starts automatically.

Step 3: bridge Tour Kit events to Redux

Tour Kit fires callbacks for every lifecycle event. The bridge hook listens to these and dispatches Redux actions, keeping both systems in sync without tight coupling.

// src/hooks/use-redux-tour.ts
import { useCallback } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useTour, createTour, createStep } from '@tourkit/react'
import type { TourConfig } from '@tourkit/react'
import type { RootState, AppDispatch } from '@/store/store'
import {
  startTour,
  advanceStep,
  completeTour,
  dismissTour,
} from '@/store/slices/tour-slice'

export function useReduxTour(tourId: string, tourConfig: TourConfig) {
  const dispatch = useDispatch<AppDispatch>()
  const tourState = useSelector((s: RootState) => s.tours)
  const isCompleted = tourState.completedTours.includes(tourId)
  const isDismissed = tourState.dismissedTours.includes(tourId)

  const tour = useTour()

  const start = useCallback(() => {
    if (isCompleted || isDismissed) return
    dispatch(startTour(tourId))
    tour.start()
  }, [dispatch, tourId, isCompleted, isDismissed, tour])

  const handleStepChange = useCallback(
    (stepIndex: number) => {
      dispatch(advanceStep(tourId))
    },
    [dispatch, tourId]
  )

  const handleComplete = useCallback(() => {
    dispatch(completeTour(tourId))
  }, [dispatch, tourId])

  const handleDismiss = useCallback(() => {
    dispatch(dismissTour(tourId))
    tour.stop()
  }, [dispatch, tourId, tour])

  return {
    start,
    handleStepChange,
    handleComplete,
    handleDismiss,
    isCompleted,
    isDismissed,
    isActive: tourState.activeTour === tourId,
    currentStep: tourState.progress[tourId]?.currentStep ?? 0,
  }
}

The hook returns everything a component needs without exposing Redux internals. Components call start() and the hook coordinates both systems.

Step 4: build a tour component with Redux-driven state

Now wire it together in an actual tour. Tour Kit's TourProvider handles rendering, while Redux tracks the broader state picture.

// src/components/dashboard-tour.tsx
'use client'

import {
  TourProvider,
  Tour,
  TourStep,
  TourCard,
  TourCardHeader,
  TourCardContent,
  TourCardFooter,
  TourNavigation,
  TourProgress,
  TourOverlay,
  createTour,
  createStep,
} from '@tourkit/react'
import { useReduxTour } from '@/hooks/use-redux-tour'

const dashboardTourConfig = createTour({
  id: 'dashboard-overview',
  steps: [
    createStep({
      target: '[data-tour="metrics-panel"]',
      title: 'Your metrics dashboard',
      content: 'Track key performance indicators here. Click any card to drill down into detailed reports.',
    }),
    createStep({
      target: '[data-tour="nav-sidebar"]',
      title: 'Navigation',
      content: 'Access all modules from the sidebar. Sections are organized by your team role.',
    }),
    createStep({
      target: '[data-tour="quick-actions"]',
      title: 'Quick actions',
      content: 'Common tasks live here. Pin your most-used actions for faster access.',
    }),
  ],
})

export function DashboardTour() {
  const {
    start,
    handleComplete,
    handleDismiss,
    isCompleted,
    isActive,
  } = useReduxTour('dashboard-overview', dashboardTourConfig)

  if (isCompleted) return null

  return (
    <TourProvider
      tour={dashboardTourConfig}
      onComplete={handleComplete}
      onClose={handleDismiss}
    >
      <TourOverlay />
      <Tour>
        {dashboardTourConfig.steps.map((step, index) => (
          <TourStep key={step.target} index={index}>
            <TourCard>
              <TourCardHeader />
              <TourCardContent />
              <TourCardFooter>
                <TourNavigation />
                <TourProgress />
              </TourCardFooter>
            </TourCard>
          </TourStep>
        ))}
      </Tour>
    </TourProvider>
  )
}

When a user completes this tour, completeTour('dashboard-overview') fires. Redux DevTools shows the action, the state diff, and you can time-travel back to any step. That last part is what makes Redux worth the setup cost for enterprise teams.

Step 5: add multi-tour orchestration with selectors

Enterprise apps don't have one tour. They have a welcome tour, a feature discovery tour per module, role-based tours, and "what's new" tours after releases. Redux selectors give you a clean way to determine what should run next.

// src/store/selectors/tour-selectors.ts
import { createSelector } from '@reduxjs/toolkit'
import type { RootState } from '@/store/store'

const selectTourState = (state: RootState) => state.tours

export const selectAvailableTours = createSelector(
  [selectTourState],
  (tours) => {
    const seen = new Set([
      ...tours.completedTours,
      ...tours.dismissedTours,
    ])
    return tours.tourQueue.filter((id) => !seen.has(id))
  }
)

export const selectShouldShowTour = createSelector(
  [
    selectTourState,
    (_state: RootState, tourId: string) => tourId,
  ],
  (tours, tourId) => {
    if (tours.completedTours.includes(tourId)) return false
    if (tours.dismissedTours.includes(tourId)) return false
    if (tours.activeTour && tours.activeTour !== tourId) return false
    return true
  }
)

export const selectToursBySegment = createSelector(
  [selectTourState],
  (tours) => {
    const tourMap: Record<string, string[]> = {
      new: ['welcome', 'dashboard-overview', 'first-project'],
      returning: ['whats-new-v2', 'advanced-filters'],
      'power-user': ['keyboard-shortcuts', 'api-integration'],
    }
    return tourMap[tours.userSegment] ?? []
  }
)

The selectShouldShowTour selector memoizes the check, so components don't re-render unless the tour state actually changes. Compare that to prop-drilling completion flags through five levels of components.

Step 6: debug tour flows with Redux DevTools

Open Redux DevTools in your browser. You'll see every tour action in the action log: tours/startTour, tours/advanceStep, tours/completeTour. Click any action to see the state diff.

The time-travel feature is where this setup pays for itself. A QA engineer reports that tour step 3 shows up blank on the settings page. What do you do?

  1. Reproduce the flow
  2. Open the DevTools action history
  3. Scrub back to the tours/advanceStep action for step 2
  4. Inspect the state: which tour is active, what step index, what user segment
  5. Check if a tours/dismissTour fired unexpectedly between steps

Without centralized state, debugging means sprinkling console.log across callbacks. Tedious. We tested both approaches on a 12-step onboarding flow with conditional branching. Redux DevTools found the root cause in under two minutes. The console.log approach? Eleven.

CapabilityTour Kit alone (Context)Tour Kit + Redux Toolkit
Single tour stateBuilt-in via TourProviderCentralized in store
Multi-tour coordinationManual with MultiTourKitProviderTour queue + selectors
Time-travel debuggingNot availableFull Redux DevTools support
User segmentationCustom implementationTyped slice with segment selectors
State persistencelocalStorage via usePersistence()redux-persist or custom middleware
Team onboarding costLow (React context pattern)Medium (requires Redux knowledge)
Bundle overhead~8KB core + ~12KB react (gzipped)Adds ~11KB for RTK + react-redux

Common issues and troubleshooting

These are the issues we hit most often when wiring Tour Kit to Redux Toolkit, based on debugging multiple enterprise onboarding flows. Each fix takes under five minutes once you know what to look for.

"Tour starts but Redux state doesn't update"

The most common cause: dispatching inside a Tour Kit callback that fires before the component mounts in the Redux-connected tree. Verify your TourProvider is nested inside the Redux Provider. Check component order in your providers file.

// Wrong โ€” TourKitProvider outside Redux
<TourKitProvider>
  <Provider store={store}>
    {children}
  </Provider>
</TourKitProvider>

// Correct โ€” Redux wraps everything
<Provider store={store}>
  <TourKitProvider>
    {children}
  </TourKitProvider>
</Provider>

"Tour queue doesn't advance to the next tour"

Check that completeTour fires and not dismissTour. The slice only auto-dequeues on completion. If users click the close button, that triggers dismissal, which skips the queue intentionally. If you want dismissed tours to also advance the queue, modify the dismissTour reducer to include the dequeue logic.

"Selectors cause unnecessary re-renders"

If selectShouldShowTour triggers renders when unrelated state changes, you probably aren't memoizing the tourId parameter. Pass it as the second argument to useSelector:

// Causes re-renders on every state change
const shouldShow = useSelector((s: RootState) =>
  s.tours.completedTours.includes('dashboard-overview')
)

// Memoized โ€” only re-renders when the result changes
const shouldShow = useSelector((s: RootState) =>
  selectShouldShowTour(s, 'dashboard-overview')
)

"TypeScript errors with tour config types"

Tour Kit exports its config types as TourConfig and TourStepConfig from @tourkit/react. If you see type mismatches between your Redux state and Tour Kit's expected config, make sure you're importing from @tourkit/react (which re-exports everything from @tour-kit/core) rather than importing from both packages separately.

When Redux is overkill

Not every project needs this. Tour Kit's built-in TourProvider with usePersistence() handles single tours and small apps perfectly well.

The break-even point? Roughly five or more distinct tours with at least two user segments. Below that, Redux adds ceremony without proportional benefit.

As of April 2026, Redux Toolkit sits at 9.8M weekly npm downloads (PkgPulse). Zustand overtook it at ~20M weekly, but RTK remains the default in enterprise settings with 5+ developers and 10+ screens because DevTools debugging and strict slice architecture prevent state chaos as teams grow (Nucamp).

Tour Kit doesn't prescribe a state management solution. It composes with Redux, Zustand, Jotai, or plain context through lifecycle callbacks (onComplete, onClose, onStepChange). No adapter layer, no lock-in.

One honest limitation: Tour Kit is younger than React Joyride or Shepherd.js, with less battle-testing at massive enterprise scale. But the TypeScript-first API and headless design mean you're not locked into patterns that break when your needs outgrow them.

Next steps

Once the Redux + Tour Kit foundation is in place, these extensions will take your enterprise onboarding system further. Each builds on the tour slice pattern from this tutorial.

  • Add redux-persist to save tour completion state across sessions. Tour Kit's own usePersistence() hook works for single tours, but Redux persist gives you one hydration point for all tour state.
  • Use RTK Query to fetch tour definitions from your backend API. Tour configs can live in a CMS or database, with the Redux store caching them client-side.
  • Connect tour analytics by adding a Redux middleware that forwards tours/completeTour and tours/advanceStep actions to your analytics provider. Tour Kit's @tourkit/analytics package offers a plugin-based alternative if you prefer that pattern.
  • Check out the Tour Kit docs for the full API reference and more examples.

FAQ

Does Tour Kit require Redux Toolkit to manage tour state?

Tour Kit does not require Redux. It ships with built-in state via TourProvider and usePersistence() for localStorage progress saving. Redux Toolkit becomes valuable in enterprise apps with multiple tours, user segments, and teams that need centralized debugging through Redux DevTools.

How much bundle size does adding Redux Toolkit add on top of Tour Kit?

Tour Kit's core is under 8KB gzipped; the React package adds ~12KB. Redux Toolkit with react-redux adds roughly 11KB gzipped on top. If your app already uses RTK, the marginal cost is zero. RTK Query adds another ~9KB if you use it for fetching tour configs from an API (RTK docs).

Can I use Zustand instead of Redux Toolkit with Tour Kit?

Yes. Tour Kit's lifecycle callbacks dispatch to any store you choose. Zustand works well for smaller teams wanting less boilerplate. Redux Toolkit's edge is DevTools time-travel and strict slice patterns for large teams. As of April 2026, Zustand leads at ~20M weekly downloads versus RTK's 9.8M (PkgPulse).

How do I persist tour completion state across page refreshes?

Two options. Tour Kit's built-in usePersistence() hook saves progress to localStorage automatically. For the Redux approach, add redux-persist to your store configuration and include the tours reducer in the persist whitelist. The Redux option is better for enterprise apps because you get a single hydration point for all application state, including tour progress.

Is this pattern accessible for screen readers?

Tour Kit handles accessibility out of the box: ARIA live region announcements, focus trapping, keyboard navigation (Escape to close, arrow keys to navigate), and prefers-reduced-motion support. The Redux layer only manages data state, so it doesn't affect a11y. Tour Kit's rendering handles all ARIA attributes and focus management per W3C ARIA Authoring Practices guidelines.


JSON-LD structured data

{
  "@context": "https://schema.org",
  "@type": "TechArticle",
  "headline": "Redux Toolkit + Tour Kit: enterprise state management for tours",
  "description": "Build enterprise product tours with Redux Toolkit and Tour Kit. Create typed tour slices, debug flows with time-travel, and manage multi-tour state across large React apps.",
  "author": {
    "@type": "Person",
    "name": "Tour Kit Team",
    "url": "https://tourkit.dev"
  },
  "publisher": {
    "@type": "Organization",
    "name": "Tour Kit",
    "url": "https://tourkit.dev",
    "logo": {
      "@type": "ImageObject",
      "url": "https://tourkit.dev/logo.png"
    }
  },
  "datePublished": "2026-04-07",
  "dateModified": "2026-04-07",
  "image": "https://tourkit.dev/og-images/redux-toolkit-tour-kit-enterprise-state-management.png",
  "url": "https://tourkit.dev/blog/redux-toolkit-tour-kit-enterprise-state-management",
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://tourkit.dev/blog/redux-toolkit-tour-kit-enterprise-state-management"
  },
  "keywords": ["redux product tour state", "redux toolkit onboarding", "enterprise tour state management"],
  "proficiencyLevel": "Intermediate",
  "dependencies": "React 18+, TypeScript 5+, Redux Toolkit 2+",
  "programmingLanguage": {
    "@type": "ComputerLanguage",
    "name": "TypeScript"
  }
}
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "Does Tour Kit require Redux Toolkit to manage tour state?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Tour Kit does not require Redux or any external state manager. It ships with built-in state management through TourProvider and the usePersistence() hook for localStorage-based progress saving. Redux Toolkit becomes valuable in enterprise apps with multiple tours, user segments, and teams that need centralized debugging through Redux DevTools."
      }
    },
    {
      "@type": "Question",
      "name": "How much bundle size does adding Redux Toolkit add on top of Tour Kit?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Tour Kit's core is under 8KB gzipped, and the React package adds about 12KB gzipped. Redux Toolkit with react-redux adds roughly 11KB gzipped on top of that. If your app already uses Redux Toolkit, the marginal cost is zero since Tour Kit only needs the packages you've already installed."
      }
    },
    {
      "@type": "Question",
      "name": "Can I use Zustand instead of Redux Toolkit with Tour Kit?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Yes. Tour Kit's headless architecture works with any state management library. The lifecycle callbacks (onComplete, onClose, onStepChange) dispatch to whatever store you use. Zustand is a strong choice for smaller teams who want less boilerplate. Redux Toolkit's advantage is DevTools time-travel debugging and the strict slice pattern that keeps large teams aligned."
      }
    },
    {
      "@type": "Question",
      "name": "How do I persist tour completion state across page refreshes?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Two options. Tour Kit's built-in usePersistence() hook saves progress to localStorage automatically. For the Redux approach, add redux-persist to your store configuration and include the tours reducer in the persist whitelist."
      }
    },
    {
      "@type": "Question",
      "name": "Is this pattern accessible for screen readers?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Tour Kit handles accessibility out of the box: ARIA live region announcements, focus trapping within tour cards, keyboard navigation (Escape to close, arrow keys to navigate), and prefers-reduced-motion support. The Redux layer doesn't affect accessibility since it only manages data state."
      }
    }
  ]
}

Internal linking suggestions:

Distribution checklist:

  • Cross-post to Dev.to with canonical URL
  • Cross-post to Hashnode with canonical URL
  • Share on Reddit r/reactjs as a tutorial post
  • Answer related Stack Overflow questions about "react redux product tour state management"

Ready to try userTourKit?

$ pnpm add @tour-kit/react