takt

React

React has a dedicated wrapper: @vskstudio/takt-react (provider, hooks, widgets, web component), built on the @vskstudio/takt-core core.

pnpm add @vskstudio/takt-react @vskstudio/takt-core
# or: npm install @vskstudio/takt-react @vskstudio/takt-core
# or: yarn add @vskstudio/takt-react @vskstudio/takt-core
# or: bun add @vskstudio/takt-react @vskstudio/takt-core

react (^18 || ^19) and @vskstudio/takt-core (>=0.10.0 since 0.8.0) are peer dependencies: the core is not installed automatically, add it explicitly.

The package is fully typed and SSR / RSC-safe (init only happens on mount, in the browser). The . entry ships the 'use client' directive, so it drops straight into the Next.js App Router. It exposes two entry points:

  • @vskstudio/takt-react: the <Takt> provider, the useTakt() and useTaktEvent() hooks, the <TaktEvent> component, the <TaktBadge> and <TaktEmbed> widgets, plus the core helpers it re-exports (badgeUrl(), embedUrl(), createStats(), PublicApiError, optOut(), optIn(), isOptedOut()), and the reactRouterTemplate() router helper.
  • @vskstudio/takt-react/element: the React-free <takt-analytics> web component for plain HTML.

<Takt> provider + useTakt()

Place <Takt> once near the root: it initializes the instance on mount, emits the initial pageview and wires up SPA navigation. useTakt() retrieves the shared instance in any descendant component to emit events.

// app/layout.tsx (or your root component)
import { Takt } from '@vskstudio/takt-react'

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <Takt domain="example.com" outbound files>
          {children}
        </Takt>
      </body>
    </html>
  )
}

<Takt> only renders a provider around its children: under the Next.js App Router, rendering the <html> and <body> tags remains the root layout’s job.

import { useTakt } from '@vskstudio/takt-react'

function SignupButton() {
  const takt = useTakt()
  return (
    <>
      <button onClick={() => takt.track('Signup', { props: { plan: 'pro' } })}>
        Sign up
      </button>
      <button
        onClick={() =>
          takt.track('Purchase', {
            props: { plan: 'pro' },
            revenue: { amount: '29', currency: 'EUR' }
          })
        }
      >
        Buy
      </button>
    </>
  )
}

<Takt> provider props:

PropTypeDefaultRole
domainstringlocation.hostnameSite identifier
endpointstringhttps://taktlytics.com/api/eventIngestion URL. Pass /api/event for a same-origin first-party proxy.
scriptOriginstringnoneFirst-party origin to derive the endpoint from (that origin followed by /api/event). endpoint wins over it.
outboundbooleanfalseTracks outbound links
filesboolean \| string[]falseTracks downloads (optional extension list)
spabooleantrueTracks client navigation (auto pageviews)
track404booleanfalseReports a 404 event on error pages ([data-takt-404] / <meta name="takt:404"> marker, or a 404 HTTP status).
taggedbooleanfalseAuto-tracks clicks on [data-takt-event] elements
respectDntbooleantrueRespects Do Not Track
excludeLocalhostbooleantrueIgnores localhost / private IPs
enabledbooleantrueMaster switch: false disables all tracking
sampleRatenumber1Fraction of sessions tracked (0 to 1)
trackQuerybooleanfalseKeeps the query string in URLs
queryParamsstring[]noneParameters kept when trackQuery is false (allowlist)
excludestring[]nonePath prefixes never tracked. Segment-bounded: /app covers /app and /app/… but not /application.
scrubUrl(url: string) => stringnoneTransforms every URL before it is sent (page, referrer, url of outbound links and downloads, path of 404s)
debugbooleanfalseLogs each payload to the console before sending
redactRoutesstring[]noneSensitive route patterns: a matching path is sent as the pattern, e.g. ['/verify/[token]', '/reset/:code'] (see Route redaction)
redactUndecodablePathsbooleantrueNot exposed by this wrapper: set it on the core SDK (init() / createTakt()), see Unreadable paths
routeTemplatesbooleanfalseSends every page as its route template; needs routeTemplate
routeTemplate() => string \| nullnoneReturns the current route template, e.g. () => reactRouterTemplate(router); read when routeTemplates is on. Component prop only

Config props are read once when <Takt> mounts. Changing them afterwards has no effect, so remount the component to reconfigure. The one exception is routeTemplate: <Takt> always calls the function from the latest render.

The provider forwards all of this configuration to the core: the advanced settings (enabled, sampleRate, trackQuery, queryParams, exclude, scrubUrl) are available as props, with no separate core instance, and so is debug since 0.7.0. Never call init() at module level in a file rendered on the server: the core touches history as it is instantiated.

scrubUrl is a function: it cannot cross the Next.js server → client boundary. Render <Takt> from a client component if you pass it. The same goes for routeTemplate.

useTakt() never throws: called outside a <Takt> or during SSR, it returns a no-op instance. Calls stay harmless, but the first track() / pageview() emits a console.warn reporting that <Takt> is not mounted.

Consent, however, works before mounting: useTakt().optOut(), useTakt().optIn() and useTakt().isOptedOut() act directly on the stored choice, as do the optOut, optIn and isOptedOut functions exported by the package. A consent banner can therefore render first, and the instance created later honours the choice.

import { useState } from 'react'
import { isOptedOut, optIn, optOut } from '@vskstudio/takt-react'

function AnalyticsToggle() {
  const [blocked, setBlocked] = useState(isOptedOut)
  const toggle = () => {
    if (blocked) optIn()
    else optOut()
    setBlocked(isOptedOut())
  }
  return <button onClick={toggle}>{blocked ? 'Enable analytics' : 'Disable analytics'}</button>
}

Route redaction

The query string is stripped by default, but the path is sent as is: /verify/abc123 leaks the token. List the sensitive routes in redactRoutes: a matching path is sent as the pattern, while every other page keeps its real path. Patterns accept [param], [[optional]], [...rest], (group), :param, :param?, * and **. The rule covers the page URL, same-site referrers, outbound and download links, and 404 paths.

<Takt domain="example.com" redactRoutes={['/verify/[token]', '/reset/:code', '/invoices/:id']}>
  {children}
</Takt>

This is the recommended setup for Next.js: the App Router exposes no route template on the client. List the dynamic routes that carry secrets, using the same [param] names as your app/ folders.

routeTemplates sends every page as its route template, so /users/42 becomes /users/:id. It suits fully private apps; on a public site it merges every article into one row. Takt needs to know the matched route, which it reads from routeTemplate. With a React Router data router (createBrowserRouter), pass reactRouterTemplate(router):

import { RouterProvider, createBrowserRouter } from 'react-router'
import { Takt, reactRouterTemplate } from '@vskstudio/takt-react'

const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    children: [
      { index: true, element: <Home /> },
      { path: 'users/:id', element: <User /> },
      { path: 'docs/*', element: <Docs /> }
    ]
  }
])

export function App() {
  return (
    <Takt domain="example.com" routeTemplates routeTemplate={() => reactRouterTemplate(router)}>
      <RouterProvider router={router} />
    </Takt>
  )
}

reactRouterTemplate joins the path of every matched route (/users/:id, /docs/*), skips index and layout routes without a path, and returns null when nothing matches. In that case redactRoutes still applies and the real path is sent otherwise. It reads router.state.matches only, so it adds no dependency on react-router. Details in Privacy.

useTaktEvent() hook + <TaktEvent>

For declarative click tracking, useTaktEvent() returns an { onClick } you spread onto any element. The instance is resolved at click time (no stale closure), with a fallback to the core’s track():

import { useTaktEvent } from '@vskstudio/takt-react'

function BuyButtons() {
  const signup = useTaktEvent({ name: 'Signup', props: { plan: 'pro' } })
  const purchase = useTaktEvent({
    name: 'Purchase',
    revenue: { amount: '29', currency: 'EUR' }
  })
  return (
    <>
      <button {...signup}>Sign up</button>
      <button {...purchase}>Buy</button>
    </>
  )
}

<TaktEvent> wraps a single child and composes its existing onClick, so you can annotate an element without touching its handler. It forwards refs to the child:

import { TaktEvent } from '@vskstudio/takt-react'

function Cta({ onClick }) {
  return (
    <TaktEvent name="Signup" props={{ plan: 'pro' }}>
      <button onClick={onClick}>Sign up</button>
    </TaktEvent>
  )
}

The child’s original onClick still fires; tracking runs alongside it.

<TaktBadge> and <TaktEmbed> widgets

To display a site’s public stats (see Widgets), the package ships two drop-in components:

import { TaktBadge, TaktEmbed } from '@vskstudio/takt-react'

function Footer() {
  return (
    <>
      <TaktBadge domain="example.com" variant="b" glyph="dash" lang="en" />
      <TaktEmbed domain="example.com" theme="dark" lang="en" />
    </>
  )
}

<TaktBadge> renders an <img> (loading="lazy", decoding="async"): variant is a, b or d, glyph is unplug, dash, off or eyeoff. <TaktEmbed> renders a 404×264 <iframe> by default, loading="lazy" and sandboxed: theme is light, dark or auto. Both accept lang (fr / en), a host prop to point at another Takt origin, and forward their remaining attributes to the rendered element.

<takt-analytics> web component

For plain HTML or a non-React framework, import the React-free (auto-registered) element:

<script type="module">
  import '@vskstudio/takt-react/element'
</script>

<takt-analytics domain="example.com" outbound files></takt-analytics>

Attributes mirror the props, in kebab-case:

  • Flags (presence = on, value ignored): outbound, files, tagged.
  • On by default, set ="false" (or ="0") to turn them off: spa, respect-dnt, exclude-localhost.
  • Values: domain, endpoint, script-origin, enabled, sample-rate, track-query, debug (only read when the attribute is present), plus query-params, exclude and redact-routes which take a comma-separated list (redact-routes="/verify/[token],/reset/:code").
<takt-analytics domain="example.com" spa="false" respect-dnt="false"></takt-analytics>

routeTemplates and routeTemplate have no attribute: the element has no router. files takes no extension list here, and scrubUrl has no equivalent: those are values only the <Takt> provider receives as props. 404 detection likewise goes through the track404 prop. The bundle is SSR-safe: importing it on the server is a no-op as long as customElements does not exist.