Private BetaProposeFlow is currently in private beta.Join the waitlist

React Integration

Build approval workflows in React with pre-built components and hooks from the @proposeflow/react package.

Installation

npm install @proposeflow/react @proposeflow/sdk

Provider Setup

Wrap your app with the ProposeFlowProvider to enable hooks and components.

app/providers.tsx
'use client';

import { ProposeFlowProvider } from '@proposeflow/react';

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <ProposeFlowProvider
      apiKey={process.env.NEXT_PUBLIC_PROPOSEFLOW_API_KEY!}
    >
      {children}
    </ProposeFlowProvider>
  );
}

Note: For production, use a server-side API route to proxy requests instead of exposing your API key to the client.

useProposal Hook

Fetch and manage a single proposal with loading states.

components/ProposalReview.tsx
import { useProposal } from '@proposeflow/react';

export function ProposalReview({ proposalId }: { proposalId: string }) {
  const { proposal, isLoading, error, approve, reject } = useProposal(proposalId);

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!proposal) return <div>Proposal not found</div>;

  return (
    <div>
      <h2>{proposal.generatedObject.title}</h2>
      <p>{proposal.generatedObject.content}</p>

      <div className="flex gap-2">
        <button onClick={() => approve()}>Approve</button>
        <button onClick={() => reject('Needs revision')}>Reject</button>
      </div>
    </div>
  );
}

useGenerate Hook

Generate new proposals with automatic state management.

components/GenerateForm.tsx
import { useGenerate } from '@proposeflow/react';
import { useState } from 'react';

export function GenerateForm() {
  const [prompt, setPrompt] = useState('');
  const { generate, isGenerating, proposal, error } = useGenerate('blogPost');

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    await generate({ prompt });
  };

  return (
    <form onSubmit={handleSubmit}>
      <textarea
        value={prompt}
        onChange={(e) => setPrompt(e.target.value)}
        placeholder="Describe your blog post..."
      />

      <button type="submit" disabled={isGenerating}>
        {isGenerating ? 'Generating...' : 'Generate'}
      </button>

      {proposal && (
        <div>
          <h3>Generated: {proposal.generatedObject.title}</h3>
          <p>{proposal.generatedObject.summary}</p>
        </div>
      )}

      {error && <p className="error">{error.message}</p>}
    </form>
  );
}

ProposalCard Component

A pre-built component for displaying proposals with approval actions.

components/ReviewPage.tsx
import { ProposalCard } from '@proposeflow/react';

export function ReviewPage({ proposalId }: { proposalId: string }) {
  return (
    <ProposalCard
      proposalId={proposalId}
      onApprove={(proposal) => {
        console.log('Approved:', proposal.generatedObject);
        // Navigate or update UI
      }}
      onReject={(proposal, reason) => {
        console.log('Rejected:', reason);
        // Triggers regeneration with the reason as feedback
      }}
      allowEdit={true}  // Let users modify before approving
      renderData={(data) => (
        <div>
          <h2>{data.title}</h2>
          <p>{data.content}</p>
          <div>{data.tags.join(', ')}</div>
        </div>
      )}
    />
  );
}

ProposalList Component

components/PendingProposals.tsx
import { ProposalList } from '@proposeflow/react';

export function PendingProposals() {
  return (
    <ProposalList
      status="pending"
      schemaId="blogPost"
      renderItem={(proposal) => (
        <div key={proposal.id} className="proposal-item">
          <h3>{proposal.generatedObject.title}</h3>
          <span>{proposal.createdAt.toLocaleDateString()}</span>
          <a href={`/review/${proposal.id}`}>Review</a>
        </div>
      )}
      emptyState={<p>No pending proposals</p>}
    />
  );
}

Server Components

For Next.js App Router, use createProposeFlow to create a typed client with automatic schema registration.

lib/proposeflow.ts
import { createProposeFlow } from '@proposeflow/react';
import { z } from 'zod';

const blogPostSchema = z.object({
  title: z.string(),
  content: z.string(),
  tags: z.array(z.string()),
});

export const pf = createProposeFlow({
  schemas: {
    blogPost: blogPostSchema,
  },
  schemaSync: 'hash',           // Content-based schema versioning
  autoRegisterSchemas: true,    // Auto-register on first generate()
});

Then use the client in your Server Components and Server Actions:

app/proposals/page.tsx
import { pf } from '@/lib/proposeflow';

export default async function ProposalsPage({
  searchParams,
}: {
  searchParams: { cursor?: string };
}) {
  const { data: proposals, nextCursor } = await pf.listProposals({
    status: 'pending', // or 'approved' | 'rejected' | 'regenerating'
    cursor: searchParams.cursor,
    limit: 20,
  });

  return (
    <div>
      <h1>Pending proposals</h1>
      <ul>
        {proposals.map((proposal) => (
          <li key={proposal.id}>{proposal.id}</li>
        ))}
      </ul>
      {nextCursor ? <a href={`?cursor=${nextCursor}`}>Next</a> : null}
    </div>
  );
}

Styling

Components are unstyled by default and can be customized with CSS or Tailwind.

Tailwind example
<ProposalCard
  proposalId={id}
  className="rounded-lg border p-6 shadow-sm"
  approveButtonClassName="bg-green-600 text-white px-4 py-2 rounded"
  rejectButtonClassName="bg-red-600 text-white px-4 py-2 rounded"
/>