npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@qoh/core-react

v1.0.0

Published

Queen of hearts Core React API

Readme

@qoh/core-react

React client library for headless.li — the Semantic Layer for AI Agents.

Build CMS-driven websites with zero hallucinations at a fraction of the (token) cost.


Stop fighting GraphQL. Start shipping.

headless.li is a middleware platform that connects GraphQL-based headless CMS systems to your React components — without manually writing any GraphQL queries. Works with:

Drupal · WordPress · AEM · Sitecore · Contentstack · Hygraph · Strapi · Payload · Sanity · Directus (and any other GraphQL-based CMS)

Register your components once against CMS type names, call service.query() with a query name and slug, and pass the result to <QueenofheartsRenderComponent />. The library resolves the right component for each content block automatically based on __typename.

Installation

npm install @qoh/core-react

Requires React 17, 18, or 19 as a peer dependency.

Quick start

1. Initialize the service

Call QueenofheartsService.init() once at application startup (e.g. in your root layout or _app.tsx) with your headless.li API token.

import { QueenofheartsService } from '@qoh/core-react';

QueenofheartsService.init('your-headlessli-api-token');

You can get a free token at headless.li

2. Register your components

Map each CMS content type (__typename) to the React component that should render it.

import { registerComponent } from '@qoh/core-react';
import HeroSection from './components/HeroSection';
import TextBlock from './components/TextBlock';

registerComponent(HeroSection, 'HeroRecord');
registerComponent(TextBlock, 'TextBlockRecord');

3. Fetch page data

Use QueenofheartsService.getInstance().query() to retrieve CMS content. The service automatically sends the registered component list to the API so only the fields your components actually use are returned.

const service = QueenofheartsService.getInstance();

const page = await service.query('page', {
  variables: {
    filter: [{ name: 'slug', operator: Filter.eq, value: 'home' }],
  },
});

4. Render the content

Pass the block array from the CMS response to <QueenofheartsRenderComponent />. It iterates the array and renders the correct registered component for each block based on __typename.

import { QueenofheartsRenderComponent } from '@qoh/core-react';

export default function Page({ page }) {
  return <QueenofheartsRenderComponent data={page.sections} />;
}

Component registration

Eager components

registerComponent(MyComponent, 'MyRecord');

registerComponent accepts an optional Zod schema as the third argument (see Zod schemas).

By default, a component is registered only once. Pass true as the fourth argument to overwrite an existing registration:

registerComponent(MyComponent, 'MyRecord', schema, true);

Lazy components

For code-splitting, register a dynamic import loader instead of the component directly:

import { registerLazyComponent } from '@qoh/core-react';

registerLazyComponent(() => import('./components/HeavyChart'), 'ChartRecord');

The loader is called the first time that component type is encountered and the resolved module cached in the registry. QueenofheartsRenderComponent wraps rendering in <Suspense> automatically.

Pre-loading lazy components

If you know which component types a page will need before rendering, you can pre-load them:

import { fetchDynamicComponents } from '@qoh/core-react';

await fetchDynamicComponents(pageData);

This walks pageData recursively, collects all __typename values, and triggers the loaders for any registered lazy components that have not been loaded yet.


Zod schemas — selective field fetching

When you register a component with a Zod schema, the library derives a ComponentFieldMap from the schema and sends it to the headless.li API. The API then restricts each component's query to only the fields that schema describes, reducing payload size.

import { z } from 'zod';
import { registerComponent } from '@qoh/core-react';

const HeroSchema = z.object({
  __typename: z.string(),
  headline: z.string(),
  subline: z.string().optional(),
  image: z.object({
    url: z.string(),
    alt: z.string().nullable(),
  }),
});

registerComponent(HeroSection, 'HeroRecord', HeroSchema);

For union/polymorphic fields (an array of blocks of different types), use .loose() on the object — this signals to the library that child component resolution should be delegated:

const PageSchema = z.object({
  __typename: z.string(),
  sections: z.array(z.object({ __typename: z.string() }).loose()),
});

Without a schema the library requests all fields ({ __all: true }).


Querying content

service.query(queryName, options?)

Fetches CMS records by query name. The query name corresponds to a root-level query in your CMS GraphQL schema (e.g. page, blogPost, product).

const result = await service.query('page', {
  variables: {
    filter: [{ name: 'slug', operator: Filter.eq, value: 'about' }],
    locale: 'en',
  },
  depth: 3,
});

Options:

| Option | Type | Description | |---|---|---| | variables.filter | { name, operator, value }[] | Filter conditions | | variables.locale | string | Locale code | | variables | Record<string, unknown> | Any additional CMS-specific variables | | ignoreProperties | string[] | CMS fields to exclude from the response | | depth | number | Relation nesting depth | | fieldArgs | Record<string, unknown> | Extra per-field arguments |

Filter operators (via the Filter enum):

import { Filter } from '@qoh/core-react';

Filter.eq      // field equals value
Filter.neq     // field does not equal value
Filter.in      // field is one of (value is comma-separated)
Filter.notIn   // field is not one of

service.queryGraphql(graphqlQuery)

Sends a raw GraphQL query string to the headless.li proxy. Use this for one-off queries that don't map to registered components.

const result = await service.queryGraphql(`
  query {
    allProducts {
      id
      title
    }
  }
`);

Passing shared props to all components

Use contextProps to inject shared data (router, i18n, feature flags, etc.) into every component rendered by <QueenofheartsRenderComponent />. Each component receives the contextProps object merged with its CMS data.

<QueenofheartsRenderComponent
  data={page.sections}
  contextProps={{ locale: 'en', router }}
/>

Error handling

API errors throw an ApiError instance:

import { ApiError } from '@qoh/core-react';

try {
  const page = await service.query('page', options);
} catch (e) {
  if (e instanceof ApiError) {
    console.error(e.status, e.body.summary, e.body.details);
  }
}

Browser devtools

When the <body> element has the class qoh-inject-ids, the service enters debug mode. It injects a __qohId attribute into every CMS block object and listens for custom window events emitted by the headless.li browser devtools extension to support component highlighting and data inspection.


API reference

| Export | Kind | Description | |---|---|---| | QueenofheartsService | class | Singleton service. Call .init() once, then .getInstance() everywhere. | | QueenofheartsRenderComponent | React component | Renders a CMS data array or object by dispatching to registered components. | | registerComponent | function | Registers an eager React component against a CMS type name. | | registerLazyComponent | function | Registers a lazy-loaded component with a dynamic import loader. | | fetchDynamicComponents | function | Pre-loads lazy components found in a data tree. | | getAllregisteredComponents | function | Returns the current component registry. | | unregisterComponent | function | Removes a component from the registry by type name. | | zodToComponentFields | function | Converts a Zod schema to a ComponentFieldMap. | | Filter | enum | Filter operators: eq, neq, in, notIn. | | ApiError | class | Error class thrown on non-2xx API responses. | | componentField | Zod schema | Zod schema for a generic polymorphic block array field. |