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

@pipe0/react

v0.1.7

Published

React component library for building forms and catalogs powered by pipe0 pipes and searches.

Downloads

2,663

Readme

@pipe0/react

Prebuilt React components for rendering pipe0 pipe and search configuration forms. Drop a pipeId or searchId in and get a fully-typed, validated form with the right inputs, autocomplete, and connection pickers.

Install

npm install @pipe0/react

Import the stylesheet once in your app entry:

import "@pipe0/react/styles";

PipeForm

Renders a form for a specific pipe. The simplest usage is fully zero-config:

import { PipeForm } from "@pipe0/react";

<PipeForm
  pipeId="people:name:split@1"
  publicKey={PIPE0_PUBLIC_KEY}
  onSubmit={(payload) => {
    // payload is a fully-typed PipePayload for the given pipeId
    console.log(payload);
  }}
/>;

For layout control, use the compound API and build the form yourself:

import {
  PipeForm,
  PipeFormHeader,
  PipeFormTitle,
  PipeFormContent,
  PipeFormSubmitButton,
  usePipeForm,
} from "@pipe0/react";

function MyForm() {
  const pipeForm = usePipeForm({
    pipeId: "people:name:split@1",
    publicKey: PIPE0_PUBLIC_KEY,
    resolvers,
  });

  return (
    <PipeForm context={pipeForm} onSubmit={(payload) => submit(payload)}>
      <PipeFormHeader>
        <PipeFormTitle>Split name</PipeFormTitle>
      </PipeFormHeader>
      <PipeFormContent />
      <PipeFormSubmitButton>Run</PipeFormSubmitButton>
    </PipeForm>
  );
}

Customizing form rendering

Every compound part accepts a render prop that receives (props, state). props are wired with all the event handlers, refs, and a11y attributes that would have been spread on the default element. state exposes the component's iterables and computed values, so you never need to call hooks inside the callback.

// Inject a callout between the default sections and the error slots.
<PipeFormContent
  render={(props, { sections, fieldPaths, hasFieldLoaderError, form }) => (
    <div {...props}>
      {sections.map((section) => (
        <PipeFormSection key={section.key} section={section} />
      ))}
      <MyCallout />
      {hasFieldLoaderError && (
        <div role="alert">Failed to load options for one or more fields.</div>
      )}
      <FormLevelErrors control={form.control} fieldPaths={fieldPaths} />
    </div>
  )}
/>;

The default body of <PipeFormContent /> is exactly that snippet (without the callout); customizing the rendering means copying it and editing the parts that should change. Each level of the hierarchy works the same way:

<PipeFormSection
  section={section}
  render={(props, { groups, hasErrors }) => (
    <section {...props} data-errors={hasErrors}>
      {groups.map((group) => (
        <PipeFormGroup key={group.key} group={group} />
      ))}
    </section>
  )}
/>;

<PipeFormGroup
  group={group}
  render={(props, { fields, expanded }) => (
    <fieldset {...props}>
      {expanded && fields.map((field) => <PipeFormField key={field.path} field={field} />)}
    </fieldset>
  )}
/>;

Catalog components follow the same pattern. Each filter exposes the data a custom render needs as the second arg:

  • PipeCatalogSearchFilter{ value, setValue, isActive }
  • PipeCatalogCategoryFilter{ value, setValue, options, counts, totalCount, isActive }
  • PipeCatalogColumnFilter (and the typed Input/Output/Provider/Tag variants) — { value, setValue, options, isActive }
  • PipeCatalogList{ cards, isEmpty }
  • PipeCatalogActiveFilters{ activeFilters, isEmpty }
  • PipeCatalogCard{ selected, expanded, setExpanded }

Spreading {...props} always keeps the wired ref, click/keyboard handlers, className, and data-* attributes — the second arg is purely an escape hatch for rebuilding markup.

SearchForm

Same shape as PipeForm, but for searches (e.g. people:profiles:crustdata@1):

import { SearchForm } from "@pipe0/react";

<SearchForm
  searchId="people:profiles:crustdata@1"
  publicKey={PIPE0_PUBLIC_KEY}
  resolvers={resolvers}
  onSubmit={(payload) => runSearch(payload)}
/>;

The compound version uses useSearchForm plus SearchFormHeader, SearchFormContent, SearchFormSubmitButton, etc.

Resolvers

Some fields can't be statically rendered — they depend on the user's own data. For example: a Resend "audience" dropdown needs to list the audiences attached to that user's Resend connection, and a Crustdata "locations" filter needs autocomplete suggestions. These are handled by resolvers:

import type { FormResolvers } from "@pipe0/base";

const resolvers: FormResolvers = {
  // Called once on mount. Return the user's connected providers.
  // If omitted, connector-dependent fields are hidden.
  getConnections: async () => [{ public_id: "resend_abc", provider: "resend" }],

  // Called per dynamic field when its prerequisites are satisfied,
  // when a dependency changes, or when the user types in a dropdown.
  // `args.payload` contains the full form values — including `pipe_id`
  // (from PipeForm) or `search_id` (from SearchForm), and the selected
  // connector. Your backend can derive everything it needs from payload.
  getFieldContext: async (args) => {
    const path =
      "pipe_id" in args.payload
        ? "/api/pipe0/pipes/field-context"
        : "/api/pipe0/search/field-context";

    const res = await fetch(path, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({
        field_path: args.fieldPath,
        query: args.query,
        payload: args.payload,
      }),
    });
    return await res.json(); // FormStore — merged into form store by the SDK
  },
};

getFieldContext is used by both pipe and search forms. Discriminate on "pipe_id" in args.payload vs "search_id" in args.payload to decide which backend endpoint to call.

Why you proxy instead of calling pipe0 directly

publicKey identifies your pipe0 project in the browser but does not authorize resolver requests. Autocomplete and connection lookups hit pipe0's private API (POST /v1/pipes/field-context and /v1/search/field-context) which requires your pipe0 API key, and they need to know which end user is asking (so the right connections are returned). Shipping the API key to the browser would leak it, and pipe0 has no way to know who your user is.

So resolvers should call your backend, not pipe0's. Your backend:

  1. Authenticates the incoming request using whatever auth your app uses (session cookie, JWT, etc.).
  2. Looks up that user's pipe0 connections / permissions.
  3. Forwards the request to pipe0 using the server-side API key.

Minimal proxy example (Next.js route handler):

// app/api/pipe0/autocomplete/route.ts
import { Pipe0 } from "@pipe0/client";

const pipe0 = new Pipe0({ apiKey: process.env.PIPE0_API_KEY! });

export async function POST(req: Request) {
  const user = await requireUser(req); // your auth
  const body = await req.json();

  // Optionally scope connectionId to this user's connections
  const suggestions = await pipe0.pipes.autocomplete({
    pipe_id: body.pipeId,
    field_path: body.fieldPath,
    query: body.query,
    connection_id: body.connectionId,
    payload: body.payload,
  });

  return Response.json({ suggestions });
}

getConnections follows the same pattern: your backend owns the mapping from "logged-in user" → "pipe0 connection ids", and returns only that user's connections to the browser.