Skip to content
playhtml

React API

The full type surface of @playhtml/react. For a gentler introduction, see Using React. For concept-by-concept usage, each page under Data and Capabilities has a React tab alongside the vanilla form.

Initializes the playhtml client for a React subtree. There must be exactly one PlayProvider per React root.

interface PlayProviderProps {
  initOptions?: InitOptions;
  pathname?: string;
  children: React.ReactNode;
}

Everything in initOptions maps one-to-one onto the vanilla playhtml.init() argument — see the init options reference.

pathname is optional and only needed for client-side-navigation frameworks (React Router, Next.js, etc.) where the browser Navigation API isn’t available. Pass it from your router and playhtml will rebuild rooms + rescan the DOM on pathname changes. See navigation for details.

import { PlayProvider } from "@playhtml/react";

<PlayProvider initOptions={{ cursors: { enabled: true } }}>
  <App />
</PlayProvider>;

HOC that returns a component with live, shared data plus a setData callback. The config controls how the element is wired into playhtml; the render function is a regular functional component that receives playhtml’s state as its first argument and your own props as its second.

withSharedState<T, V, P>(
  config: WithSharedStateConfig<T, V> | ((props: P) => WithSharedStateConfig<T, V>),
  render: (
    playhtmlProps: ReactElementEventHandlerData<T, V>,
    componentProps: P,
  ) => React.ReactNode,
): React.ComponentType<P>;
interface WithSharedStateConfig<T, V> {
  defaultData: T;
  myDefaultAwareness?: V;
  id?: string;
  tagInfo?: TagType[];
}
  • defaultData — required. The initial value of data. Survives reload.
  • myDefaultAwareness — optional. Initial value for this user’s ephemeral per-user field. Does not persist.
  • id — optional. Stable id for the element. If omitted, playhtml derives one from the rendered DOM; see Dynamic elements for why stable ids matter.
  • tagInfo — optional. Marks the element as one of the built-in capabilities (e.g. [TagType.CanToggle]). See Capabilities.
interface ReactElementEventHandlerData<T, V> {
  data: T;
  setData: (data: T | ((draft: T) => void)) => void;
  awareness: V[];
  myAwareness?: V;
  setMyAwareness: (data: V) => void;
  ref: React.RefObject<HTMLElement>;
}

setData accepts either a replacement value or a mutator function. See Data essentials for the merge semantics.

Pass a callback instead of a config object when defaultData needs to derive from props:

export const Reaction = withSharedState(
  ({ reaction: { count } }) => ({ defaultData: { count } }),
  ({ data, setData }, props) => /* … */,
);

Component form of withSharedState. Useful when you want JSX children (render-prop style) instead of wrapping a component, or when you need ref access to a specific element.

interface CanPlayElementProps<T, V> {
  id?: string;
  defaultData: T;
  myDefaultAwareness?: V;
  tagInfo?: TagType[];
  standalone?: boolean;
  children: (props: ReactElementEventHandlerData<T, V>) => React.ReactNode;
}
  • id — required if the top-level child is a React Fragment. Otherwise defaults to the child’s id, or a hash of the child’s content.
  • standalone — when true, this element doesn’t inherit defaults from any built-in capability (it’s a pure can-play element).
<CanPlayElement
  tagInfo={[TagType.CanToggle]}
  id="my-lamp"
  defaultData={{ on: false }}
>
  {({ data, setData }) => (
    <button onClick={() => setData({ on: !data.on })}>
      {data.on ? "on" : "off"}
    </button>
  )}
</CanPlayElement>

Typed wrapper around CanPlayElement for draggable elements. Accepts the same dataSource, shared, and standalone props, plus three bounds props for constraining the drag area.

interface CanMoveElementProps {
  bounds?: string;
  boundsMinVisible?: number;
  boundsMinVisiblePx?: number;
  dataSource?: string;
  shared?: boolean | string;
  standalone?: boolean;
  children: React.ReactElement | ((data: MoveEventData) => React.ReactElement);
}
  • bounds — id or CSS selector of the container to keep the element inside. "arena", "#arena", and ".grid" all work.
  • boundsMinVisible — fraction (0–1) of the element that must stay inside bounds on every edge. Default 0.25. Use 1 to pin the element fully inside, 0 to drop the fraction constraint entirely.
  • boundsMinVisiblePx — absolute pixel floor on the keep-visible slice. Default 60. Useful when an image has transparent padding around its paint — a pure fraction of the layout bbox might otherwise let the visible pixels clip into invisible border.

The effective keep-visible slice on each axis is max(boundsMinVisible × size, boundsMinVisiblePx). Set both knobs to 0 to opt fully out of the keep-visible guarantee. See can-move in the capabilities reference for the interaction details.

import { CanMoveElement } from "@playhtml/react";

<div id="fridge" style={{ position: "relative", height: 400 }}>
  <CanMoveElement bounds="fridge">
    <div id="magnet-a">🍎</div>
  </CanMoveElement>
  <CanMoveElement bounds="fridge" boundsMinVisible={0.5} boundsMinVisiblePx={0}>
    <div id="magnet-b">🥐</div>
  </CanMoveElement>
</div>;

Access the playhtml context from any descendant of PlayProvider.

interface PlayContextValue {
  hasSynced: boolean;
  cursors: CursorsView;
  configureCursors: (opts: Partial<CursorOptions>) => void;
  getMyPlayerIdentity: () => PlayerIdentity;
  registerPlayEventListener: (type: string, handler: PlayEvent) => string;
  removePlayEventListener: (type: string, id: string) => void;
  dispatchPlayEvent: (msg: { type: string; eventPayload?: unknown }) => void;
}

Boolean that flips to true once the initial state from the server has landed. Useful for gating effects that should run exactly once per synced session:

const { hasSynced } = usePlayContext();
useEffect(() => {
  if (hasSynced) setData({ count: data.count + 1 });
}, [hasSynced]);

A reactive view of the cursor system. Components using this re-render when colors or identities change.

const { cursors } = usePlayContext();
// cursors.allColors: string[]
// cursors.color: string
// cursors.name: string

See Cursors for the full cursor configuration surface.

const {
  registerPlayEventListener,
  removePlayEventListener,
  dispatchPlayEvent,
} = usePlayContext();

Usually you’ll wrap these in a hook to bind a listener to the component’s lifecycle — see Events for the useConfetti pattern.

Re-exported from @playhtml/common. Use these as tagInfo entries when you want a built-in capability (can-move, can-toggle, etc.) wired into your component.

import { TagType } from "@playhtml/common";

TagType.CanPlay;
TagType.CanMove;
TagType.CanToggle;
TagType.CanGrow;
TagType.CanSpin;
TagType.CanHover;
TagType.CanDuplicate;
TagType.CanMirror;

The repo has a collection of runnable React examples at packages/react/examples. Live versions are visible at playhtml.fun/experiments/one/ and playhtml.fun/experiments/two/.

A few things still in flux in the React package:

  • Per-key persistence config. Currently persistence is a whole-store choice: setMyAwareness for ephemeral, setData for persistent, no local-only mode. A future persistenceOptions object might let you configure per-key (none / local / global).
  • awareness splitting. awareness currently includes the local user; it may split into myAwareness + othersAwareness for clarity.
  • Hook ergonomics. A pure-hook interface (useSharedState({ id, defaultData })) is being evaluated as an alternative to the HOC form. The blocker is that hooks have no natural place to pin a stable id.