React Integration
Build approval workflows in React with pre-built components and hooks from the @proposeflow/react package.
Installation
npm install @proposeflow/react @proposeflow/sdkProvider 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"
/>