Next.js / React
The pattern for mounting Playgent inside a React component without breaking hydration or leaking iframes on unmount.
The SDK is browser-only — Playgent.init touches window and the DOM. In a React app you mount it from a client component, load the script via next/script (or a manual loader), and clean up with player.destroy() on unmount.
App Router (Next.js 13+)
Create a client component that owns the player:
"use client";
import Script from "next/script";
import { useEffect, useRef } from "react";
type PlaygentInitOptions = {
containerId: string;
player: string;
game: string;
mode?: "pinned" | "daily" | "random";
content?: string;
onComplete?: (data: { score: number | null; won: boolean | null; timeMs: number }) => void;
};
declare global {
interface Window {
Playgent: { init: (opts: PlaygentInitOptions) => { destroy: () => void } };
}
}
export function PlaygentGame({ game, mode = "daily" }: { game: string; mode?: "pinned" | "daily" | "random" }) {
const ref = useRef<{ destroy: () => void } | null>(null);
const sdkReady = useRef(false);
function mount() {
if (ref.current || !sdkReady.current) return;
ref.current = window.Playgent.init({
containerId: "playgent-host",
player: "plyr_acme",
game,
mode,
onComplete: ({ score, won, timeMs }) => {
// forward to your analytics
},
});
}
useEffect(() => {
return () => {
ref.current?.destroy();
ref.current = null;
};
}, []);
return (
<>
<Script
src="https://static.playgent.com/player/v2.js"
strategy="afterInteractive"
onLoad={() => { sdkReady.current = true; mount(); }}
/>
<div id="playgent-host" />
</>
);
}
Use it from any server or client page:
import { PlaygentGame } from "@/components/PlaygentGame";
export default function GamesPage() {
return <PlaygentGame game="sudoku" mode="daily" />;
}
Pages Router
The same pattern works under pages/. Either use next/script as above, or load the SDK in _document.tsx:
// pages/_document.tsx
import { Html, Head, Main, NextScript } from "next/document";
export default function Document() {
return (
<Html>
<Head>
<script src="https://static.playgent.com/player/v2.js" async />
</Head>
<body><Main /><NextScript /></body>
</Html>
);
}
Then the component can skip the <Script> tag and just guard Playgent on window.
Multiple games on one page
Each player instance needs its own container. Give them unique IDs:
"use client";
import { useEffect, useId, useRef } from "react";
export function PlaygentEmbed({ game }: { game: string }) {
const id = useId().replace(/:/g, "_");
const containerId = `playgent-${id}`;
const ref = useRef<{ destroy: () => void } | null>(null);
useEffect(() => {
if (!window.Playgent) return;
ref.current = window.Playgent.init({
containerId,
player: "plyr_acme",
game,
mode: "daily",
});
return () => { ref.current?.destroy(); ref.current = null; };
}, [game, containerId]);
return <div id={containerId} />;
}
This component assumes the SDK is loaded in your layout / _document. Render it as many times as you want.
Identity from your auth provider
If you use Clerk, NextAuth, Supabase Auth, or anything else, pass the user into the player as soon as they're logged in:
"use client";
import { useUser } from "@clerk/nextjs"; // or your auth hook
import { useEffect, useRef } from "react";
export function PlaygentGame() {
const { user } = useUser();
const ref = useRef<any>(null);
useEffect(() => {
if (!window.Playgent) return;
ref.current = window.Playgent.init({
containerId: "playgent-host",
player: "plyr_acme",
game: "sudoku",
mode: "daily",
externalUserId: user?.id ?? null,
externalUsername: user?.username ?? null,
});
return () => { ref.current?.destroy(); ref.current = null; };
}, []);
useEffect(() => {
if (ref.current && user) {
ref.current.setIdentity({
externalUserId: user.id,
externalUsername: user.username,
});
}
}, [user]);
return <div id="playgent-host" />;
}
Next.js gotchas
StrictMode double-mount in development
React's StrictMode invokes effects twice in development to surface side-effect bugs. Without the cleanup function above, you'd end up with two iframes stacked on top of each other. The player.destroy() cleanup makes the second mount idempotent.
Server-side rendering
Playgent.init runs only in the browser. Don't call it from a server component or getServerSideProps. Anything that touches window.Playgent needs the "use client" directive and a useEffect (or Script onLoad) guard.
Hydration mismatch
Render the host <div> unconditionally (don't gate it behind typeof window !== "undefined") — the markup is identical on server and client. The init call goes inside useEffect, which never runs during SSR.
Module bundlers and the SDK
Don't import player/v2.js through your bundler. It's an IIFE designed to attach Playgent to window, and bundling it produces brittle results across versions. Always load it via a <script src=…> tag or next/script.
Origin allowlist
Add every domain your app is served on (production, preview deploys, localhost) to the player in Hub → Configuration → Players. Vercel preview URLs (*.vercel.app) work with a wildcard if you preview-deploy often.