New:Introducing Socket Scanning for VS Code Marketplace Extensions.Learn more →
Get Started

@mapled/react

Package Overview
Dependencies
Maintainers
1
Versions
6
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@mapled/react

React helpers for Mapled — published content in a React single-page app (Vite and the like): a provider and hooks over @mapled/client, with preview and publish notifications.

npmnpm
Version
0.1.0
Version published
Weekly downloads
21
-90.54%
Maintainers
1
Weekly downloads
 
Created
Source

@mapled/react

React helpers for Mapled, the hosted headless CMS for sites built with AI: published content in a React single-page app — Vite, Create React App, anything that renders in the browser. One provider, three hooks. Built on @mapled/client; a Next.js site uses @mapled/next instead.

Install

npm install @mapled/react

@mapled/client comes with it. Put the project's delivery key (Mapled: Integrations → Your site) in .env:

VITE_MAPLED_KEY=mk_live_…

It is bundled into the page, which is fine for this key and for no other: it reads published content only. A server key or a webhook signing secret never goes under a VITE_ name.

Wire it up

// src/mapled.ts
import { createClient } from "@mapled/client";
import { typedHooks } from "@mapled/react";
import type { MapledSchema } from "./mapled-types";

export const mapled = createClient<MapledSchema>({ key: import.meta.env.VITE_MAPLED_KEY });
export const { useSingle, useRecords, useRecord } = typedHooks<MapledSchema>();
// src/main.tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { MapledProvider } from "@mapled/react";
import { mapled } from "./mapled";
import { App } from "./App";

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <MapledProvider client={mapled}>
      <App />
    </MapledProvider>
  </StrictMode>,
);

mapled-types.ts is written by npx @mapled/cli types generate; with typedHooks<MapledSchema>() keys are checked and data has each collection's own type. Without types, import useSingle, useRecords and useRecord straight from @mapled/react.

Read content

Every hook answers { data, loading, error, refetch }.

// src/pages/Home.tsx
import Markdown from "react-markdown";
import { assetUrl } from "@mapled/client";
import { useSingle } from "../mapled";

export function Home() {
  const { data: home, loading, error } = useSingle("homepage");
  if (loading) return <p>Loading…</p>;
  if (error || !home) return <p>Nothing here yet.</p>;
  return (
    <main>
      <h1>{home.data.headline}</h1>
      {home.data.hero ? <img src={assetUrl(home.data.hero, { width: 1200 })} alt="" /> : null}
      <Markdown>{home.data.intro ?? ""}</Markdown>
    </main>
  );
}
// src/pages/Blog.tsx
import { Link } from "react-router-dom";
import { useRecords } from "../mapled";

export function Blog() {
  const { data, loading } = useRecords("posts", { sort: "-date", limit: 20, fields: ["title", "slug", "date"] });
  if (loading && !data) return <p>Loading…</p>;
  return (
    <ul>
      {data?.records.map((post) => (
        <li key={post.id}>
          <Link to={`/blog/${post.data.slug}`}>{post.data.title}</Link>
        </li>
      ))}
    </ul>
  );
}
// src/pages/Post.tsx
import Markdown from "react-markdown";
import { useParams } from "react-router-dom";
import { useRecord } from "../mapled";

export function Post() {
  const { slug } = useParams();
  const { data: post, loading } = useRecord("posts", slug ? { slug } : null);
  if (loading) return <p>Loading…</p>;
  if (!post) return <p>This post doesn't exist.</p>;
  return (
    <article>
      <h1>{post.data.title}</h1>
      <Markdown>{post.data.body}</Markdown>
    </article>
  );
}
  • useSingle(key, options?) — data is the single's record, or null while it is empty.
  • useRecords(collection, options?) — data is { records, total, release }; options are the client's: filter, sort, limit, offset, fields, expand.
  • useRecord(collection, { slug } | { id } | null, options?) — data is null when the record isn't published; null instead of a reference reads nothing (a router param that hasn't arrived).
  • Components that ask for the same thing share one request; an inline options object is the same read on every render.
  • Rich text values are Markdown — render them with a Markdown component, never as raw HTML.
  • useMapled() hands out the client for a read the hooks don't cover.

Preview

Nothing to add: when someone opens Preview in Mapled, the page opens with a one-time token in its address, the client trades it for a draft session, and the hooks show drafts in that tab. Say so on the page:

// src/PreviewBar.tsx
import { usePreview } from "@mapled/react";

export function PreviewBar() {
  const preview = usePreview();
  if (!preview.active) return null;
  return (
    <p role="status">
      You are looking at a preview. <button onClick={preview.exit}>Back to the published site</button>
    </p>
  );
}

Mapled has to know where the site lives and that it renders in the browser: the site manifest's framework is "react-spa" (npx @mapled/cli scan --write, bindings push — or your AI agent's push_site_manifest), and Production domain in Project settings holds the site's public address (an agent sets it with set_site_url). There is no webhook to configure and no route to mount.

Live updates

A publish shows when the page is loaded again. To follow it while the page is open:

// src/LiveApp.tsx
import { MapledProvider } from "@mapled/react";
import { mapled } from "./mapled";
import { App } from "./App";

export function LiveApp() {
  return (
    <MapledProvider client={mapled} live>
      <App />
    </MapledProvider>
  );
}

Each open tab then asks Mapled for the release pointer once a minute (live={{ interval: 30 }} for thirty seconds, never under fifteen) while it is visible, and the hooks read again — pinned to the new release — when it moves; what was on screen stays there until the new answer arrives. It is off by default because every check is a request of the delivery key's budget (1,200 a minute).

Forms

submitForm from @mapled/client posts a visitor's submission:

// src/ContactForm.tsx
import { useState, type FormEvent } from "react";
import { submitForm } from "@mapled/client";

const CONSENT = "I agree to be contacted about my request.";

export function ContactForm() {
  const [state, setState] = useState<"idle" | "sending" | "sent" | "failed">("idle");

  async function submit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const form = event.currentTarget;
    setState("sending");
    const answer = await submitForm("contact", form, {
      key: import.meta.env.VITE_MAPLED_KEY,
      consent: new FormData(form).get("consent") ? CONSENT : undefined,
    });
    setState(answer.ok ? "sent" : "failed");
  }

  if (state === "sent") return <p>Thank you — we'll be in touch.</p>;
  return (
    <form onSubmit={submit}>
      <input name="name" required />
      <input name="email" type="email" required />
      <textarea name="message" required />
      <input name="_gotcha" tabIndex={-1} autoComplete="off" aria-hidden="true" style={{ display: "none" }} />
      <label>
        <input name="consent" type="checkbox" required /> {CONSENT}
      </label>
      <button disabled={state === "sending"}>Send</button>
      {state === "failed" ? <p>That didn't go through. Try again in a minute.</p> : null}
    </form>
  );
}

Check the integration

npx @mapled/cli doctor

doctor knows a single-page app: it wants VITE_MAPLED_KEY and no webhook secret, reads ○ «Not needed» for the webhook and the routes, and fails an env file that puts a server key or a signing secret under a public prefix.

API

Export
MapledProviderclient, live? (true or { interval })
useSingle, useRecords, useRecord{ data, loading, error, refetch }
typedHooks<MapledSchema>()The same three hooks, typed by the generated schema
usePreview(){ active, exit }
useMapled()The provider's client

Keywords

mapled

FAQs

Package last updated on 19 Sep 2026

Related posts