JSPM

@creative-locator/react

0.2.0
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 6
  • Score
    100M100P100Q53825F
  • License GPL-2.0-or-later

React bindings for Creative Locator. Drop-in <DealerLocator /> component + mount-once hooks (useDealerLocator / useDealerLocatorState / useDealerEvents) + Next.js 15 App Router Server Component entry resolved via the react-server exports condition.

Package Exports

  • @creative-locator/react
  • @creative-locator/react/client
  • @creative-locator/react/package.json
  • @creative-locator/react/server

Readme

@creative-locator/react

React component bindings for Creative Locator.

A thin wrapper around @creative-locator/headless that gives React applications a one-component install: drop a <DealerLocator /> into your JSX, pass an apiUrl, render. The component derives the host container from a ref so you never have to manage document.querySelector calls or worry about hydration timing.

Install

pnpm add @creative-locator/core @creative-locator/headless @creative-locator/react leaflet leaflet.markercluster

react and react-dom are peer dependencies (^18 || ^19). The package does not bundle React.

Usage

import { DealerLocator } from '@creative-locator/react';
import '@creative-locator/core/styles/style.scss';

export function LocatorPage() {
    return (
        <DealerLocator
            apiUrl="https://example.com/wp-json/creative-locator/v1"
            style={{ height: '600px' }}
        />
    );
}

The component renders a single <div> as the host element. Sizing is the host application's responsibility — apply height via the style prop, a CSS class, or a parent container. Without a non-zero height, the locator's map will not be visible.

Props

DealerLocator accepts every option from createHeadlessLocator except container (which is derived from the component's internal ref), plus two layout-shaped props for the wrapping <div>:

Prop Type Default Notes
apiUrl string Required. Creative Locator REST base URL.
config Partial<LocatorConfig> {} Merged over defaultLocatorConfig.
apiHeaders Record<string, string> Auth / nonce headers.
fetch typeof globalThis.fetch global Override for SSR or test contexts.
storage 'browser' | 'memory' | Storage auto Auto-detects SSR.
enableGoogleAnalytics boolean false GA bridge off by default in headless.
licenseTier 'free' | 'pro' 'free' Gates Pro layouts client-side.
adapters Partial<PlatformAdapters> Per-slot adapter overrides (advanced).
className string Applied to the wrapping <div>.
style React.CSSProperties Applied to the wrapping <div>.

See @creative-locator/headless for the canonical option reference.

Mount-once contract

Like most map libraries, the locator is expensive to initialize. The component intentionally ignores prop changes after the initial mount — re-rendering with a new apiUrl or config does NOT recreate the locator. If you need to swap configuration at runtime, remount the component via React's key prop:

<DealerLocator key={apiUrl} apiUrl={apiUrl} />

This is the same trade-off Leaflet wrappers (react-leaflet etc.) make for the same reason.

Hooks (#994 PR A)

Three React hooks expose the underlying DealerLocator for templates that want reactive access to locator state and typed event subscription. The component above is the simplest entry; hooks are the idiomatic shape for richer integrations. See docs/adr/ADR-010-framework-adapter-composables.md for the design.

import { useRef } from 'react';
import { useDealerLocator, useDealerLocatorState, useDealerEvents } from '@creative-locator/react';

export function LocatorPage() {
    const root = useRef<HTMLDivElement | null>(null);
    const { locator } = useDealerLocator(root, { apiUrl: '...' });
    const { allDealers, currentRadius } = useDealerLocatorState(locator);
    useDealerEvents(locator, 'marker:click', ({ location }) => {
        console.log('clicked', location.id);
    });

    return (
        <>
            <div ref={root} style={{ height: 600 }} />
            <p>
                {allDealers.length} dealers within {currentRadius} mi.
            </p>
        </>
    );
}

useDealerLocatorState uses useSyncExternalStore so concurrent-mode React reads a consistent snapshot. The snapshot identity is cached per locator — destructured fields keep referential stability across renders when state hasn't moved.

Next.js 15 App Router (#994 PR B)

The package ships a react-server exports condition. Inside a Server Component, import { DealerLocator } from '@creative-locator/react' resolves to the Server Component entry — an async function that renders the static shell HTML server-side, optionally fetches and inlines the initial dealer payload, and hands off interactivity to a Client Component child on hydration.

// app/locator/page.tsx — runs on the server
import { DealerLocator } from '@creative-locator/react';
import '@creative-locator/core/styles/style.scss';

export default function LocatorPage() {
    return (
        <DealerLocator
            apiUrl="https://example.com/wp-json/creative-locator/v1"
            style={{ height: 600 }}
        />
    );
}

The Server Component issues a fetch(${apiUrl}/dealers?limit=20) itself by default (Next 15's fetch cache + dedupe makes this effectively free) and emits the result as an inline <script type="application/json"> payload that the client-side HydrationOrchestrator.tryHydrateFromInlinePayload() picks up. The user sees dealer markers on first paint, before any REST round-trip resolves on the client.

Pass initialPayload to skip the SC's own fetch when you've already fetched the data upstream:

export default async function LocatorPage() {
    const res = await fetch('https://example.com/wp-json/creative-locator/v1/dealers?limit=50');
    const json = await res.json();
    return (
        <DealerLocator
            apiUrl="https://example.com/wp-json/creative-locator/v1"
            initialPayload={{ dealers: json.dealers, total: json.total }}
            fetchInitialPayload={false}
        />
    );
}

Server Component props

Prop Type Default Notes
apiUrl string Required. Creative Locator REST base URL.
initialPayload InlinePayload Pre-fetched { dealers, total, cached_at? }. Skips the SC's own fetch.
fetchInitialPayload boolean true Set false to skip server-side data resolution (client-only data flow).
ssrCap number 20 Server-fetch limit. Mirrors WP crloc_ssr_bootstrap_cap.
bootstrapId string ${instanceId}__bootstrap DOM id of the inline payload <script>. Consumer-passed config.ssrBootstrapId wins if set.
instanceId string crloc-locator-1 Wrapper element id. Multi-locator pages need a unique value per instance.
shellOptions RenderShellOptions Pass-through to renderLocatorShell (mapPanelInert, labels).
config Partial<Config> Same as the SPA component.
apiHeaders Record<string, …> Serializable headers only — no functions.

Non-serializable overrides (fetch, adapters) are not exposed here because they can't cross the RSC → Client boundary. Compose your own Client Component if you need them.

Subpath imports (escape hatch)

The react-server condition does the right thing automatically. For cases where you need to bypass the conditional resolution:

  • @creative-locator/react/server — always the Server Component.
  • @creative-locator/react/client — always the legacy SPA component + hooks.

SPA-only consumers

If you're not on the App Router (e.g. still on Pages Router, CRA, or Vite SPA), the package's default condition resolves to the legacy client-only <DealerLocator /> — the same drop-in wrapper that's been here since #515. Nothing changes for those consumers.

Astro

Use the @astrojs/react integration to render the component as a client island:

---
// src/pages/locator.astro
import { DealerLocator } from '@creative-locator/react';
import '@creative-locator/core/styles/style.scss';
---

<DealerLocator
    client:only="react"
    apiUrl={import.meta.env.PUBLIC_LOCATOR_API_URL}
    style={{ height: '600px' }}
/>

client:only="react" skips SSR entirely (Leaflet needs window).

License

GPL-2.0-or-later