@pipe0/react
v0.1.7
Published
React component library for building forms and catalogs powered by pipe0 pipes and searches.
Downloads
2,663
Maintainers
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/reactImport 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 typedInput/Output/Provider/Tagvariants) —{ 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:
- Authenticates the incoming request using whatever auth your app uses (session cookie, JWT, etc.).
- Looks up that user's pipe0 connections / permissions.
- 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.
