Next.js
WriteTrack uses browser APIs and must run client-side only. Here’s how to integrate it with Next.js.
App Router (Next.js 13+)
Section titled “App Router (Next.js 13+)”Add the 'use client' directive to components using WriteTrack:
'use client';
import { useRef, useEffect } from 'react';import { WriteTrack } from 'writetrack';
export function ResponseForm() { const textareaRef = useRef<HTMLTextAreaElement>(null); const trackerRef = useRef<WriteTrack | null>(null);
useEffect(() => { if (textareaRef.current) { trackerRef.current = new WriteTrack({ target: textareaRef.current }); trackerRef.current.start(); }
return () => { trackerRef.current?.stop(); }; }, []);
const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (trackerRef.current) { const data = trackerRef.current.getData(); const events = trackerRef.current.getRawEvents(); console.log(`Captured ${events.length} events`, data.quality); } };
return ( <form onSubmit={handleSubmit}> <textarea ref={textareaRef} rows={10} /> <button type="submit">Submit</button> </form> );}Using the React Hook
Section titled “Using the React Hook”The writetrack/react hook handles client-side concerns automatically:
'use client';
import { useRef } from 'react';import { useWriteTrack } from 'writetrack/react';
export function ResponseForm() { const textareaRef = useRef<HTMLTextAreaElement>(null); const { isTracking, tracker } = useWriteTrack(textareaRef);
const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (tracker) { const data = tracker.getData(); console.log('Session:', data.quality); } };
return ( <form onSubmit={handleSubmit}> <textarea ref={textareaRef} rows={10} /> <button type="submit">Submit</button> </form> );}Pages Router
Section titled “Pages Router”For the Pages Router, either use the 'use client' approach above, or use dynamic imports:
import dynamic from 'next/dynamic';
const ResponseForm = dynamic(() => import('../components/ResponseForm'), { ssr: false,});
export default function Page() { return <ResponseForm />;}Server Actions
Section titled “Server Actions”Send captured data to Server Actions:
'use client';
import { useRef } from 'react';import { useWriteTrack } from 'writetrack/react';import { submitResponse } from './actions';
export function ResponseForm() { const textareaRef = useRef<HTMLTextAreaElement>(null); const { tracker } = useWriteTrack(textareaRef);
async function handleSubmit(formData: FormData) { if (tracker) { formData.set('keystrokeCount', String(tracker.getKeystrokeCount())); }
await submitResponse(formData); }
return ( <form action={handleSubmit}> <textarea ref={textareaRef} name="content" rows={10} /> <button type="submit">Submit</button> </form> );}'use server';
export async function submitResponse(formData: FormData) { const content = formData.get('content') as string; const keystrokeCount = formData.get('keystrokeCount') as string;
// Store or process the submission await db.responses.create({ content, keystrokeCount: Number(keystrokeCount), });}WASM Analysis
Section titled “WASM Analysis”WriteTrack resolves its WASM binary with new URL('./writetrack.wasm', import.meta.url).
Webpack emits that asset automatically; Turbopack does not treat .wasm as an
emitted static asset, so copy the file to public/ and pass wasmUrl. This
works under both bundlers, in development and in production, so it is the
setup we recommend for every Next.js app:
cp node_modules/writetrack/dist/writetrack.wasm public/writetrack.wasmconst { tracker } = useWriteTrack(textareaRef, { wasmUrl: '/writetrack.wasm',});Add the copy to a predev and prebuild script so it is refreshed after
dependency updates.
If getAnalysis() returns null, check the browser console for the failure
reason. See Troubleshooting for other
bundlers.
Visualization components in the App Router
Section titled “Visualization components in the App Router”The writetrack/viz web components (scorecard, sparkline, badges) are browser-only, and Server Components reject dynamic(…, { ssr: false }) directly. Add one Client Component boundary and dynamic-import your visuals inside it:
'use client';
import dynamic from 'next/dynamic';
export const SessionVisuals = dynamic(() => import('./SessionVisuals'), { ssr: false,});'use client';
import { useEffect, useRef } from 'react';import { WtScorecard } from 'writetrack/viz';
export default function SessionVisuals({ report }: { report: unknown }) { const ref = useRef<HTMLElement>(null);
useEffect(() => { WtScorecard.register(); (ref.current as HTMLElement & { setData(d: unknown): void })?.setData( report ); }, [report]);
return <wt-scorecard ref={ref} />;}Server Components can then render <SessionVisuals report={report} /> freely.
API Routes
Section titled “API Routes”Alternatively, send data to API routes:
const handleSubmit = async () => { if (!tracker) return;
const data = tracker.getData();
await fetch('/api/submit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: textareaRef.current?.value, typingData: data, }), });};