Docent
Guides

React Router and Remix

Mount Docent in a React Router or Remix app with useReactRouterAdapter, built from the useNavigate and useLocation hooks you already have.

React Router 7 and Remix 2 share one adapter, because they share the hooks it is built from.

The mount

import { useLocation, useNavigate } from 'react-router'
import { Docent } from '@usedocent/react'
import { useReactRouterAdapter } from '@usedocent/react/react-router'
import { PRESENCE_LOADERS } from '@usedocent/mascot/library/loaders'

export const Guide = ({ userId, children }: { userId: string; children: React.ReactNode }) => {
  const router = useReactRouterAdapter(useNavigate(), useLocation())

  return (
    <Docent
      appId={import.meta.env.VITE_DOCENT_APP_ID}
      publishableKey={import.meta.env.VITE_DOCENT_PUBLISHABLE_KEY}
      apiUrl="https://api.usedocent.com"
      user={{ id: userId }}
      router={router}
      loadPresence={(slug) => PRESENCE_LOADERS[slug]?.() ?? Promise.reject(new Error(slug))}
      loadAudio={(vendor) =>
        vendor === 'openai' ? import('@usedocent/sdk-audio-live') : import('@usedocent/sdk-audio')
      }
      voice="tap"
    >
      {children}
    </Docent>
  )
}

Render it inside the router, in a component the router renders, so the two hooks have a router above them. A root layout route is the usual place.

The adapter

useReactRouterAdapter(
  navigate: (path: string) => void,
  location: { pathname: string; search?: string },
): RouterAdapter

It takes the hook results rather than importing react-router, so it adds no dependency and works with whichever version you are on. The current path it reports is pathname plus search, which is what Docent's route matching expects.

Call it on every render with the live useLocation() result, as above. The adapter keeps the latest one in a ref and returns a stable object, so it neither goes stale nor remounts the guide.

Remix

Remix 2 re-exports the same hooks from @remix-run/react, so only the import line changes:

import { useLocation, useNavigate } from '@remix-run/react'

Everything else is identical. The component is browser-only, so mount it in a route component rather than anywhere that runs during the server render of a document.

Without the adapter

router is optional. Omitted, the SDK navigates with history.pushState and dispatches a popstate event. Pass the adapter whenever you have a router anyway: going through the router's own navigate is what keeps its loaders, transitions and scroll restoration in the loop. See Vite with React for what the fallback is for.

On this page