Work in progress — API is unstable and will change. Not ready for production use.
Write cloud infrastructure as React components. Deploy with Pulumi.
import React, { useState } from "react";
import * as pulumi from "@pulumi/pulumi";
import { pulumiToComponent, renderToPulumi, setPulumiSDK } from "@react-pulumi/core";
import * as aws from "@pulumi/aws";
setPulumiSDK(pulumi);
const [Instance] = pulumiToComponent(aws.ec2.Instance);
function App() {
const [replicas] = useState(2);
return Array.from({ length: replicas }, (_, i) => (
<Instance key={i} name={`web-${i}`} instanceType="t3.micro" ami="ami-0abcdef1234567890" />
));
}
renderToPulumi(App)();pulumi up # standard Pulumi CLI — useState persists to Pulumi.<stack>.yamlpulumiToComponentwraps Pulumi resource classes as React FCs that return[Component, Context]- React reconciler renders your JSX — resources are created at render time as side effects
- Context provides resource instances to descendants —
useContext(VcnCtx)reads the nearest ancestor - Pulumi engine diffs against cloud state and applies changes
- State persistence —
useStatevalues are saved toPulumi.<stack>.yamlconfig via a dynamic resource
React handles composition, conditional logic, loops, and component reuse. Pulumi handles the actual cloud diffing and deployment.
| Package | Description |
|---|---|
@react-pulumi/core |
React reconciler, resource tree, Pulumi bridge, renderToPulumi |
@react-pulumi/cli |
CLI commands: up, preview, destroy, viz |
@react-pulumi/viz |
Web dashboard with resource graph visualization |
- Node.js 20+
- Pulumi CLI
- pnpm
Create a new directory with these files:
package.json
{
"name": "my-infra",
"private": true,
"type": "module",
"dependencies": {
"@react-pulumi/core": "workspace:*",
"@pulumi/pulumi": "^3.0.0",
"@pulumi/random": "^4.0.0",
"react": "^19.0.0"
},
"devDependencies": {
"tsx": "^4.0.0"
}
}Pulumi.yaml
name: my-infra
runtime:
name: nodejs
options:
typescript: false
nodeargs: "--import tsx"
main: index.tsxtsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"jsxImportSource": "react",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}index.tsx
import React, { useState } from "react";
import * as pulumi from "@pulumi/pulumi";
import { pulumiToComponent, renderToPulumi, setPulumiSDK } from "@react-pulumi/core";
import * as random from "@pulumi/random";
setPulumiSDK(pulumi);
const [RandomPet] = pulumiToComponent(random.RandomPet);
const [RandomString] = pulumiToComponent(random.RandomString);
function App() {
const [petLength] = useState(3);
const [pwLength] = useState(16);
return (
<>
<RandomPet name="my-pet" length={petLength} />
<RandomString name="my-password" length={pwLength} special={true} />
</>
);
}
renderToPulumi(App)();pulumi login --local # or pulumi login for Pulumi Cloud
pulumi stack init dev
pulumi upOn first run, useState defaults are used. After deploy, state is persisted:
# Pulumi.dev.yaml (auto-generated)
config:
react-pulumi:state: '{"keys":["App:0","App:1"],"values":[3,16]}'Subsequent pulumi up runs read the persisted state — resources stay unchanged unless state changes.
Edit the config value directly in Pulumi.<stack>.yaml to change state between runs:
pulumi config set react-pulumi:state '{"keys":["App:0","App:1"],"values":[5,32]}'
pulumi up # petLength=5, pwLength=32pulumiToComponent wraps a Pulumi resource class as a React FC and returns [Component, Context]:
import * as aws from "@pulumi/aws";
import { useContext } from "react";
import { pulumiToComponent } from "@react-pulumi/core";
// Returns [Component, Context] — type token auto-extracted
const [Bucket, BucketCtx] = pulumiToComponent(aws.s3.Bucket);
const [BucketObject] = pulumiToComponent(aws.s3.BucketObject);
// Leaf resources — ignore Context
const [Instance] = pulumiToComponent(aws.ec2.Instance);Resources are created at render time. Descendants read ancestor instances via Context:
function BucketContents() {
const bucket = useContext(BucketCtx);
return <BucketObject name="index" bucket={bucket.id} objectKey="index.html" />;
}
<Bucket name="assets">
<BucketContents />
</Bucket>
// Or use render props:
<Bucket name="assets">
{(bucket) => <BucketObject name="index" bucket={bucket.id} objectKey="index.html" />}
</Bucket>function VPC({ name, cidr }: { name: string; cidr: string }) {
return (
<Vpc name={name} cidrBlock={cidr}>
<Subnet name={`${name}-public`} cidrBlock={cidr.replace(".0.0/16", ".0.0/20")} />
<Subnet name={`${name}-private`} cidrBlock={cidr.replace(".0.0/16", ".16.0/20")} />
<InternetGateway name={`${name}-igw`} />
</Vpc>
);
}function Database({ highAvailability }: { highAvailability: boolean }) {
return (
<>
<RdsInstance name="primary" instanceClass="db.t3.medium" />
{highAvailability && <RdsInstance name="replica" instanceClass="db.t3.medium" />}
</>
);
}function MultiRegionBuckets({ regions }: { regions: string[] }) {
return (
<>
{regions.map(region => (
<Bucket name={`data-${region}`} region={region} key={region} />
))}
</>
);
}function App() {
const [replicas] = useState(2);
const [instanceType] = useState("t3.micro");
return Array.from({ length: replicas }, (_, i) => (
<Instance key={i} name={`web-${i}`} instanceType={instanceType} />
));
}useState values persist to Pulumi.<stack>.yaml between pulumi up runs. Component structure changes (adding/removing/reordering hooks) trigger a warning and fall back to defaults.
Instead of renderToPulumi + pulumi up, you can use the react-pulumi CLI with a simpler export-based entry point:
// infra.tsx — just export a component, no setPulumiSDK needed
import { pulumiToComponent } from "@react-pulumi/core";
import * as random from "@pulumi/random";
const [RandomPet] = pulumiToComponent(random.RandomPet);
export default function App() {
return <RandomPet name="my-pet" length={3} />;
}react-pulumi up infra.tsx # deploy to 'dev' stack
react-pulumi up infra.tsx -s prod # deploy to 'prod' stack
react-pulumi preview infra.tsx # preview changes
react-pulumi destroy infra.tsx # tear downNote: the CLI approach does not support useState persistence. Use renderToPulumi for stateful components.
react-pulumi viz infra.tsx # launch dashboard on :3000The viz dashboard shows a real-time resource graph powered by React Flow, with deployment status tracking via Zustand.
- React reconciler + resource tree
- Pulumi bridge (
materializeTree— legacy host-component path) -
pulumiToComponentreturns[Component, Context]— render-time resource creation + Context - Cross-resource Output wiring via
useContext - Render props mode:
<Vcn>{(vcn) => <Subnet vcnId={vcn.id} />}</Vcn> - Provider scoping (
<AwsProvider>context propagation) -
<Group>for Pulumi ComponentResource -
renderToPulumi— standardpulumi upcompatibility - Persistent
useStateviaPulumi.<stack>.yaml -
react-pulumiCLI (up,preview,destroy,viz) - Viz dashboard (React Flow graph + Zustand store)
-
react-pulumi serve— daemon mode with re-render loop - Actions trigger
setState→ re-render → deploy -
useReducerpersistence -
useConfig()— read Pulumi stack config as a hook -
useStackOutput()— cross-stack references -
useEffect/useDeployEffect— post-deploy side effects -
useSignal()— webhook-driven state changes -
useCron()— time-based infrastructure -
useMetric()— metric-driven auto-scaling - Deploy queue with serialization + debounce
- Preview gate + auto-apply safety rails
See docs/plan-serve-mode.md for detailed design.
- React 19 + react-reconciler 0.31 — custom renderer
- Pulumi 3 — cloud infrastructure engine
- TypeScript — strict mode, ESM
- pnpm workspaces + Turborepo — monorepo tooling
- React Flow (
@xyflow/react) — graph visualization - Zustand — state management for viz
- Vitest — testing
MIT