📚 Documentation: taktlytics.com/docs/wrappers/react
Idiomatic React wrapper for Takt privacy-friendly analytics.
A thin, SSR-safe React layer over @vskstudio/takt-core. It never changes the wire payload or the privacy guarantees. It just makes Takt feel native in a React app.
<Takt>component: drop it once near the root; it boots analytics in a mount effect and provides the instance to the tree.useTakt()hook: grab the live instance anywhere; returns a never-throwing no-op before mount or during SSR (it warns once in the console rather than failing silently).useTaktEvent()hook &<TaktEvent>component: declarative click tracking.<takt-analytics>custom element: framework-agnostic, React-free embed for non-React pages.
pnpm add @vskstudio/takt-react @vskstudio/takt-coreBoth are peer dependencies: react (^18 || ^19) and @vskstudio/takt-core (>=0.12.3).
Mount <Takt> once near your root. It fires an initial pageview, wires SPA navigation, and provides the instance to every descendant:
import { Takt } from '@vskstudio/takt-react'
export function App() {
return (
<Takt domain="example.com" outbound files={['pdf', 'zip']}>
<Routes />
</Takt>
)
}Then track custom events from any descendant:
import { useTakt } from '@vskstudio/takt-react'
export function SignupButton() {
const takt = useTakt()
return (
<button
onClick={() =>
takt.track('Signup', {
props: { plan: 'pro' },
revenue: { amount: '29.00', currency: 'EUR' },
})
}
>
Sign up
</button>
)
}useTakt() always returns a usable instance: before <Takt> mounts (or during SSR) it hands back a never-throwing no-op, so your handlers never crash. That no-op is not silent. The first track()/pageview() call on it logs a single console.warn so a mis-ordered provider does not go unnoticed.
| Prop | Type | Default | Description |
|---|---|---|---|
domain |
string |
location.hostname |
Site identifier sent with every event. |
endpoint |
string |
https://taktlytics.com/api/event |
Ingestion endpoint. Pass /api/event for a same-origin first-party proxy. |
scriptOrigin |
string |
n/a | First-party origin to derive the endpoint from ({origin}/api/event): your Takt domain or a custom domain to dodge ad-blockers (endpoint wins over it). |
outbound |
boolean |
false |
Auto-track outbound link clicks. |
files |
boolean | string[] |
false |
Auto-track file downloads; pass extensions to restrict. |
spa |
boolean |
true |
Track SPA navigations (pushState/replaceState + popstate). |
track404 |
boolean |
false |
Report a 404 event on error pages ([data-takt-404] / <meta name="takt:404"> marker, or a 404 HTTP status). |
respectDnt |
boolean |
true |
Suppress events when the browser's Do Not Track is enabled. |
excludeLocalhost |
boolean |
true |
Suppress events on localhost and private IP ranges. |
enabled |
boolean |
true |
Master on/off switch: set to false to disable all tracking at runtime. |
sampleRate |
number |
1 |
Fraction of sessions to track (0 to 1). |
trackQuery |
boolean |
false |
Keep the full query string and hash on URLs. Wins over queryParams. |
queryParams |
string[] |
n/a | Allowlist applied when trackQuery is off: keep only these query params, drop the rest. |
exclude |
string[] |
n/a | Path prefixes never tracked, e.g. ['/app', '/account']. Segment-bounded, case-insensitive, trailing slash ignored, matched on the percent-decoded path (/%61pp is excluded by /app). |
scrubUrl |
(url: string) => string |
n/a | Transform the URL before it is sent (page, referrer, and the url prop of outbound-link and file-download events). Component prop only. Cannot be set as a custom-element attribute. Must be a developer-controlled function; never build it from user input. |
redactRoutes |
string[] |
n/a | Sensitive route patterns: a matching path is sent as the pattern, e.g. ['/verify/[token]', '/reset/:code']. See Route redaction. |
redactUndecodablePaths |
boolean |
true |
When a route is configured and a path cannot be decoded, send it as /[redacted]. false decodes it leniently instead, as takt-core-php does. |
routeTemplates |
boolean |
false |
Send every page as its route template instead of the real path. Needs routeTemplate. |
routeTemplate |
() => string | null |
n/a | Returns the current route template, e.g. reactRouterTemplate(router). Used when routeTemplates is on. Component prop only. |
tagged |
boolean |
false |
Auto-track clicks on [data-takt-event] elements; data-takt-prop-* attributes become event props. |
debug |
boolean |
false |
Log each payload to the console before sending. |
Config props are read once when
<Takt>mounts. Changing them afterwards has no effect, so remount the component to reconfigure. The one exception isrouteTemplate:<Takt>always calls the function from the latest render.
<Takt> also unwires everything it enabled on unmount (SPA, outbound, files, 404, tagged), and de-duplicates the initial pageview across React StrictMode's double mount.
Two equivalent ways to track a click without writing a handler.
useTaktEvent() returns an { onClick } you spread onto any element:
import { useTaktEvent } from '@vskstudio/takt-react'
export function BuyButton() {
const onBuy = useTaktEvent({ name: 'Buy', revenue: { amount: '9.00', currency: 'EUR' } })
return <button {...onBuy}>Buy</button>
}<TaktEvent> wraps a single child and composes its existing onClick:
import { TaktEvent } from '@vskstudio/takt-react'
export function SignupCta({ onClick }: { onClick: () => void }) {
return (
<TaktEvent name="Signup" props={{ plan: 'pro' }}>
<button onClick={onClick}>Sign up</button>
</TaktEvent>
)
}Both take core's TrackOptions (props, revenue) plus a name, and resolve the active instance at click time, so they work inside <Takt> or with an init()-driven core setup, falling back to core's default instance otherwise.
<TaktEvent> expects exactly one React element child: it forwards its own ref onto that child (composing with any ref the child already carries), and in development it warns and renders the children untouched if the child is not a valid element.
For non-React pages, import the side-effecting ./element entry to register <takt-analytics>. It bundles core and pulls in no React runtime:
import '@vskstudio/takt-react/element'<takt-analytics domain="example.com" outbound files></takt-analytics>| Attribute | Kind | Maps to | Notes |
|---|---|---|---|
domain |
value | domain |
Defaults to location.hostname. |
endpoint |
value | endpoint |
Defaults to https://taktlytics.com/api/event. |
script-origin |
value | scriptOrigin |
endpoint wins over it. |
sample-rate |
value | sampleRate |
Parsed as a float; ignored if not finite. |
query-params |
value | queryParams |
Comma-separated list, e.g. query-params="utm_source,ref". |
exclude |
value | exclude |
Comma-separated list of path prefixes. |
redact-routes |
value | redactRoutes |
Comma-separated list of route patterns, e.g. redact-routes="/verify/[token],/reset/:code". |
respect-dnt |
boolean | respectDnt |
On by default; only "false"/"0" disables it. |
exclude-localhost |
boolean | excludeLocalhost |
On by default; only "false"/"0" disables it. |
spa |
boolean | spa |
On by default; only "false"/"0" disables it. |
enabled |
boolean | enabled |
Only read when the attribute is present; "false"/"0" disables tracking. |
track-query |
boolean | trackQuery |
Only read when the attribute is present. |
outbound |
presence | outbound |
Active as soon as the attribute exists. |
redact-undecodable-paths |
boolean | redactUndecodablePaths |
On by default; only "false"/"0" disables it. |
files |
presence | files |
Active as soon as the attribute exists. |
file-extensions |
value | files (extensions) |
Comma-separated list, e.g. file-extensions="pdf,zip". Read only when files is present. |
track-404 |
presence | track404 |
Reports a 404 event on error pages. |
tagged |
presence | tagged |
Autocapture of [data-takt-event] clicks. |
debug |
boolean | debug |
Only read when the attribute is present; logs each payload. |
The element fires a pageview on connectedCallback and disposes every listener it added on disconnectedCallback. Some <Takt> props have no attribute equivalent: scrubUrl and routeTemplate (functions), and routeTemplates (the element has no router to resolve a template from).
Thin wrappers over Takt's server-rendered widgets. <TaktBadge> renders an <img> (the badge SVG); <TaktEmbed> renders an <iframe> (the embed dashboard). Both forward standard element attributes (className, style, …).
import { TaktBadge, TaktEmbed } from '@vskstudio/takt-react'
export function Footer() {
return (
<>
<TaktBadge domain="example.com" variant="d" glyph="dash" />
<TaktEmbed domain="example.com" theme="dark" />
</>
)
}<TaktBadge> takes domain (required), variant (a | b | d), glyph (unplug | dash | off | eyeoff), lang (fr | en) and host. <TaktEmbed> takes domain (required), theme (light | dark | auto), lang and host.
The badge alt defaults to "takt" and ships loading="lazy" decoding="async"; the embed defaults to width={404} height={264} title="takt" loading="lazy" and a zero border. All of those are overridable. The embed <iframe> is hardened: it ships sandbox="allow-scripts allow-same-origin" and a fixed referrerPolicy="strict-origin-when-cross-origin", both applied after your props so a consumer cannot weaken them. The optional host prop must be an absolute http(s) URL (validated by core, which reduces it to its origin); src is wrapper-controlled and cannot be overridden.
For dashboards you build yourself, createStats is re-exported from core. Every method takes the domain first, then the params: the domain is optional once createStats is bound to one:
import { createStats, PublicApiError } from '@vskstudio/takt-react'
const stats = createStats({ domain: 'example.com' })
try {
const summary = await stats.summary(undefined, { period: '7d' })
const rows = await stats.breakdown('page', undefined, { period: '7d' })
const live = await stats.realtime()
} catch (err) {
if (err instanceof PublicApiError) console.error(err.status, err.message)
}Query strings are stripped by default, but path segments are sent as they are: /verify/abc123 leaks the token. List the sensitive routes in redactRoutes and 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 brace syntax of takt-core-php: {param}, {param?} and {param<regex>}. The rule covers the page URL, same-origin referrers, outbound and download links, and 404 paths.
<Takt redactRoutes={['/verify/[token]', '/reset/:code', '/invoices/:id']}>
<App />
</Takt>Matching is case-insensitive, decodes percent-encoding repeatedly (up to five passes), normalizes dot segments and repeated slashes, and ignores matrix parameters, so /VERIFY/abc123 matches /verify/[token]. A {param<regex>} constraint must use the restricted grammar of takt-core: literals, ., \d, \w, \s and their negations, character classes, *, +, ?, {n,m}, top-level alternation, at most 100 characters. A route whose constraint is outside that grammar, or whose matching would cost too much, is never sent as is: every path it matches becomes /[redacted]. A path that cannot be decoded is masked the same way when a route is configured. Set redactUndecodablePaths={false} (redact-undecodable-paths="false" on the element) to decode it leniently instead. When a path is masked, its query string and fragment are dropped too.
This is the recommended setup for Next.js: the App Router exposes no route template on the client, so 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 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. When the resolver returns nothing, the page is not sent with its real path: a route covered by redactRoutes is sent as its pattern, and any other route as /[redacted], without query or hash. In this mode every file download is sent as /[redacted] on any host, and every referrer is reduced to its origin. It reads router.state.matches only, so it adds no dependency on react-router.
From the main entry:
- Components:
Takt,TaktEvent,TaktBadge,TaktEmbed - Hooks:
useTakt,useTaktEvent - Router helper:
reactRouterTemplate - Re-exported from core:
badgeUrl,embedUrl,normalizeHost,createStats,PublicApiError,optOut,optIn,isOptedOut - Types:
TaktProps,TaktEventParams,ReactRouterLike,TaktBadgeProps,TaktEmbedProps,TaktInstance, plusConfig,TrackOptions,RouteTemplateResolver,BadgeOptions,EmbedOptions,BadgeVariant,BadgeGlyph,EmbedTheme,WidgetLang,StatsClient,StatsClientOptions,StatsParams,StatsPeriod,StatsDimension,StatsMetrics,StatsSummary,StatsPoint,StatsTimeseries,StatsBreakdownRow,StatsBreakdown,StatsRealtimere-exported from core
From @vskstudio/takt-react/element: defineTaktElement(). Importing the subpath already calls it. The named export is there for explicit or repeated registration (it is idempotent).
The main entry ships with a built-in 'use client' banner, and <Takt> boots inside a mount effect, so nothing touches window/document on the server. In the Next.js App Router, render <Takt> from a client component (or the App Router root) and the rest of the API works unchanged. Importing @vskstudio/takt-react/element on the server is a no-op: registration is guarded behind a customElements check.
All privacy behavior lives in @vskstudio/takt-core: Do Not Track support, localhost exclusion, opt-in/opt-out consent, and a frozen wire payload. This wrapper never alters any of it.
Consent works before <Takt> has mounted: useTakt().optOut(), useTakt().optIn() and useTakt().isOptedOut() go straight to the stored choice, and so 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 = () => {
blocked ? optIn() : optOut()
setBlocked(isOptedOut())
}
return <button onClick={toggle}>{blocked ? 'Enable analytics' : 'Disable analytics'}</button>
}