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

@kookee/react

v1.4.1

Published

Official Kookee React SDK - hooks, content rendering and an embeddable AI support chat

Readme

@kookee/react

Official React SDK for Kookee — hooks for your blog, changelog, help center and feedback board, plus an embeddable AI support chat.

Features

  • Hooks for every module — blog, pages, help center, changelog, announcements, feedback and custom entry types
  • Headless — hooks return data, you own the markup
  • Server renderinginitialData hands framework-loader results to any hook
  • No query-layer dependency — the root export has no runtime dependencies
  • Tree-shakeable — importing one hook costs ~2 kB, not the whole library
  • Chat widget — opinionated, styled, on its own subpath so it costs nothing unless used

Installation

npm install @kookee/react @kookee/sdk

@kookee/sdk 1.1.0 or later is a peer dependency, as are react and react-dom (18 or 19).

Quick Start

Wrap your app once:

import { KookeeProvider } from '@kookee/react';

<KookeeProvider config={{ projectId: 'your-project-id' }} locale="en">
  <App />
</KookeeProvider>;

Then read data anywhere:

import { useBlogPosts } from '@kookee/react';

function Blog() {
  const { data, isLoading, error, loadMore, hasMore } = useBlogPosts({ limit: 10 });

  if (isLoading) return <Spinner />;
  if (error) return <Error error={error} />;

  return (
    <>
      {data.map((post) => (
        <article key={post.id}>{post.title}</article>
      ))}
      {hasMore && <button onClick={loadMore}>Load more</button>}
    </>
  );
}

projectId or a public apiKey

Use projectId in browser code where you can: it is safe to expose and grants read-only access to published content. An apiKey in a client bundle ships to every visitor, so if you use one in the browser (the script-tag widget below does), restrict it to your domains with the project's allowed origins.

Provider

<KookeeProvider config={{ projectId, baseUrl }} locale="en">

| Prop | Type | Description | | --- | --- | --- | | config | KookeeConfig | projectId (or apiKey), optional baseUrl | | locale | string | Default locale for every hook; per-call params override it |

The client is created once and shared. useKookee() returns it directly — the escape hatch for anything the hooks do not cover:

const kookee = useKookee();
await kookee.entries.react(postId, { reactionType: 'like', action: 'add' });

Hooks

Every hook returns { data, isLoading, error, refetch }. List hooks add { loadMore, hasMore, isLoadingMore, total }.

| Module | Hooks | | --- | --- | | Blog | useBlogPosts · useBlogPost · useBlogPostById · useBlogTags | | Pages | usePages · usePage · usePageById | | Help | useHelpArticles · useHelpArticle · useHelpArticleById · useHelpCategories · useHelpSearch | | Changelog | useChangelog · useChangelogEntry · useChangelogEntryById | | Announcements | useAnnouncements · useAnnouncement | | Feedback | useFeedbackPosts · useFeedbackPost · useFeedbackColumns · useFeedbackTopContributors · useFeedbackComments | | Custom types | useEntries · useEntry · useEntryById · useEntryTags · useEntryCategories | | Shared | useComments · useReact · useConfig · useConfigList · useTranslations · useTranslationsById |

Detail hooks stay idle until their argument resolves

Pass undefined while a route param is still loading and the hook makes no request:

const { slug } = useParams();
const { data: post, isLoading } = useBlogPost(slug); // idle while slug is undefined

Custom entry types

useEntries is how you read entry types you defined yourself. type is required:

const { data } = useEntries({ type: 'recipe', limit: 20 });
const { data: recipe } = useEntry(slug, { type: 'recipe' });

Search

useHelpSearch debounces internally, so it is safe to wire straight to an input:

const [query, setQuery] = useState('');
const { data: results, isLoading } = useHelpSearch(query);

Reactions

useReact is a mutation, not a query:

const { react, isPending } = useReact(post.id);

<button disabled={isPending} onClick={() => react({ reactionType: 'like', action: 'add' })}>
  👍 {post.reactions.like ?? 0}
</button>;

react() resolves with the updated reactions map, so you can use its return value instead of refetching.

It does not update the entry in place — the hook holding that entry keeps its old counts until you use the returned value or call its refetch().

Rendering content

import { EntryContent } from '@kookee/react';

<EntryContent entry={post} className="prose" />;

EntryContent accepts detail entries only. List responses do not include contentHtml, so passing a list item is a compile error rather than a blank page:

const { data: posts } = useBlogPosts();
<EntryContent entry={posts[0]} />; // ✗ Type error — list items have no body

const { data: post } = useBlogPost(slug);
<EntryContent entry={post} />; // ✓

It renders the HTML your Kookee project produced, as-is, and has no DOM dependencies — the same markup on the server and the client. Content that originates outside Kookee should be sanitised before it reaches the CMS.

The container always carries the kookee-entry-content class (your className is merged after it) — the class the SDK stylesheets are scoped to. For syntax-highlighted code blocks and file attachment chips, add the SDK's content stylesheet:

import '@kookee/sdk/styles/content.css';

In a Tailwind v4 app, import it into a cascade layer instead, so your prose-code: utilities keep winning over the stylesheet's defaults (an unlayered import would beat any layered utility regardless of specificity):

@import '@kookee/sdk/styles/content.css' layer(components);

The flip side of the layered import: prose's own pre styles and prose-code: utilities also reach elements inside code blocks, and the layered stylesheet can't defend against them. If you render entry content inside prose containers, add this unlayered guard once:

.kookee-entry-content .kookee-code-block pre {
  margin: 0;
  border-radius: 0;
}

.kookee-entry-content .kookee-code-block pre code {
  background: none;
  padding: 0;
  border: none;
  border-radius: 0;
  font-size: 0.875rem;
}

If your app has no typography system of its own (no Tailwind prose), also add @kookee/sdk/styles/typography.css for baseline headings, lists, tables and images — scoped to the same class, inheriting your page's font and colors.

The content stylesheet is themeable through --kookee-code-* and --kookee-file-chip-* custom properties — set them on :root to match your design system (required for dark mode, whose chip fallbacks are light-theme colors). See the @kookee/sdk README for the full list.

Reading custom fields

Changelog type, version, link and any field you defined yourself live in entry.fields, not as top-level properties. Read them with the SDK's helpers:

import { fieldOptionKey, fieldStringValue } from '@kookee/sdk';

const type = fieldOptionKey(entry.fields, 'changelogType'); // 'feature' | 'fix' | ...
const version = fieldStringValue(entry.fields, 'version'); // '2.1.0'

Both return undefined when the field is absent, so entry types without them need no special-casing.

Server rendering

Hooks fetch in an effect, which does not run on the server. Fetch in your framework's loader and pass the result as initialData — the hook renders it immediately and skips its first request:

// TanStack Start
export const Route = createFileRoute('/blog')({
  loader: () => kookee.blog.list({ limit: 10 }),
  component: Blog,
});

function Blog() {
  const loaderData = Route.useLoaderData();
  const { data, loadMore } = useBlogPosts({ limit: 10 }, { initialData: loaderData });
  // Server-renders from loaderData; loadMore still works on the client.
}

The same pattern applies to Next.js server components and any other loader. initialData covers the parameters it was fetched with — changing them fetches normally.

Caching

There is none. Two components calling useBlogPosts() make two requests. This keeps the package dependency-free and predictable. If you want caching, deduplication or background refetching, use useKookee() with your own query layer:

const kookee = useKookee();
const { data } = useQuery({
  queryKey: ['blog', 'list'],
  queryFn: ({ signal }) => kookee.blog.list({ limit: 10 }, signal),
});

Requests are cancelled when inputs change or a component unmounts, and out-of-order responses are discarded, so you will not see a slow response overwrite a newer one.

Errors

error is the SDK's KookeeApiError for API failures, carrying code and status:

import { KookeeApiError } from '@kookee/sdk';

if (error instanceof KookeeApiError && error.status === 404) return <NotFound />;

Network and CORS failures reject as TypeError, so check the type before reading status.

Chat widget

The AI support chat lives on its own subpath, so its markdown renderer and syntax highlighter are only bundled if you use it:

import { KookeeChatWidget } from '@kookee/react/chat';
import '@kookee/react/chat.css';

<KookeeChatWidget clientOptions={{ projectId: 'your-project-id' }} locale="en" />;

| Export | Description | | --- | --- | | KookeeChatWidget | Floating button and panel, ready to drop in | | KookeeChat | The chat surface alone, for your own container | | KookeeChatProvider / useKookeeChat | Bring your own UI |

The widget works standalone. Inside a KookeeProvider it reuses that client instead of creating a second one.

<KookeeChatWidget
  clientOptions={{ projectId }}
  locale="en"
  onSourceClick={(source) => navigate(`/help/${source.slug}`)}
/>

onSourceClick receives clicks on both the source chips under an answer and the inline article citations in the answer text. Without it, citations render as inert link-styled text.

The conversation is kept in sessionStorage (keyed by project, scoped to the browser tab), so minimizing the panel or navigating to another page of the site brings the same chat back. The header's New chat button, or clearMessages() from useKookeeChat, discards it and starts a fresh session. KookeeChat (inline) shows the same button in a header when you pass title.

Controlling the widget

Pass hideLauncher to drop the floating button and open the panel from your own UI. Open state follows the usual controlled/uncontrolled convention: open + onOpenChange to control it, or defaultOpen to only set the initial value (read once on mount). The panel's own close button reports through onOpenChange in both modes. Do not switch between the two modes after mount. With hideLauncher and neither open nor defaultOpen, the panel can never open.

const [open, setOpen] = useState(false);

<button onClick={() => setOpen(true)}>Ask AI</button>
<KookeeChatWidget
  clientOptions={{ projectId }}
  hideLauncher
  open={open}
  onOpenChange={setOpen}
/>

launcherLabel="Need help?" shows a pill next to the launcher, fading in shortly after the page loads. Clicking it opens the chat; its X dismisses it permanently for that browser (localStorage), and it stays hidden while the panel is open.

zIndex and offset override the fixed-position layout. offset.x is the distance from the side edge chosen by position (right for bottom-right, left for bottom-left), offset.y from the bottom; numbers are pixels, strings are passed through as CSS lengths.

<KookeeChatWidget
  clientOptions={{ projectId }}
  zIndex={50}
  offset={{ x: 16, y: 'calc(20px + env(safe-area-inset-bottom))' }}
/>

The same values can be set from a stylesheet instead, via --kookee-widget-z, --kookee-widget-offset-x and --kookee-widget-offset-y on .kookee-widget.

Script tag (no React)

The same widget ships as a single <script> for sites that do not use React — plain HTML, Shopify, WordPress, Vue, Angular. React and every dependency are bundled, and the widget renders inside a shadow root so your page's CSS and the widget's never touch.

<script async src="https://kookee.dev/chat/latest.js" data-api-key="YOUR_API_KEY"></script>

The key is meant to be public; restrict it to your domains with the project's allowed origins. Optional attributes: data-base-url, data-locale, data-title, data-greeting, data-placeholder, data-launcher-label, data-position (bottom-right | bottom-left), data-theme (light | dark | auto), data-primary-color.

For suggestions, callbacks or a custom launcher, omit data-api-key and initialize from code. Every KookeeChatWidget prop except clientOptions and open is accepted:

<script src="https://kookee.dev/chat/latest.js"></script>
<script>
  KookeeChat.init({
    apiKey: 'YOUR_API_KEY',
    suggestions: ['Pricing', 'Refunds'],
    hideLauncher: true,
    onSourceClick: (source) => { location.href = '/help/' + source.slug; },
  });
  document.querySelector('#ask-ai').addEventListener('click', () => KookeeChat.open());
</script>

| Method | Description | | --- | --- | | init(config) | Mounts the widget. Throws if already initialized — call destroy() first | | open() / close() / toggle() | Open state | | sendMessage(text) | Sends a message as the user | | clearMessages() | Clears the conversation and starts a new session | | on(event, cb) | 'open', 'close', 'sourceClick'; returns an unsubscribe function | | destroy() | Unmounts and removes the widget; listeners are kept |

Calls before init() log a warning and do nothing; on() works at any time. With async on the tag, call the API from the script's onload or after the page's load event. Citations are clickable only when onSourceClick is given or a sourceClick listener is registered. Auto-init from data-* needs a classic external script (document.currentScript is null for type="module" and for tag managers that inline the source) — use init() there.

TypeScript projects that call the KookeeChat global from code can download https://kookee.dev/chat/latest.d.ts into their types folder (it declares the global with no imports). With this package installed, the equivalent is:

import type { KookeeChatApi } from '@kookee/react/chat';
declare global {
  var KookeeChat: KookeeChatApi;
}

The script is not in the npm package. It is built from src/widget/ with the rest of this package (pnpm build writes dist/chat.global.js and dist/chat-global.d.ts).

Advanced: the generic hooks

The named hooks are thin wrappers over three generics, exported for anything not covered above:

import { useKookeeList, useKookeeQuery, useKookeeCollection, useKookee } from '@kookee/react';

const kookee = useKookee();
const { data } = useKookeeList(
  (page, signal) => kookee.blog.list({ limit: 10, page }, signal),
  'blog|10',
);
  • useKookeeList — paginated responses, with loadMore
  • useKookeeQuery — a single value
  • useKookeeCollection — methods returning a bare array

The second argument is a cache key. It must reflect everything the callback closes over — the callback is not in the dependency list, so a value missing from the key will silently go stale. Pass null to keep the hook idle.

TypeScript

Types come from @kookee/sdk:

import type { BlogEntryListItem, BlogEntryDetail, PaginatedResponse } from '@kookee/sdk';

License

MIT