
Security News
NIST Officially Stops Enriching Most CVEs as Vulnerability Volume Skyrockets
NIST will stop enriching most CVEs under a new risk-based model, narrowing the NVD's scope as vulnerability submissions continue to surge.
Compose React components by wiring named parameters — type-safe, no prop drilling, no hooks
▄▄▄▄
██▀▀▀ ██
▄███▄██ ██▄████ ▄█████▄ ███████ ███████
██▀ ▀██ ██▀ ▀ ▄▄▄██ ██ ██
██ ██ ██ ▄██▀▀▀██ ██ ██
▀██▄▄███ ██ ██▄▄▄███ ██ ██▄▄▄
▄▀▀▀ ██ ▀▀ ▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀
▀████▀▀
The smallest API imaginable.
Compose React components by wiring outputs into inputs.
compose({ into, from, key }) feeds from's output into into's input named key. The remaining unsatisfied inputs bubble up as the composed component's props. The result is always a standard React component.
No prop drilling. No Context. No useState. No useEffect. No manual subscriptions.
npm install graftjs
React components are functions with named parameters (props). When you build a UI, you're really building a graph of data dependencies between those functions. But React forces you to wire that graph imperatively — passing props down, lifting state up, wrapping in Context providers, sprinkling hooks everywhere.
Graft lets you describe the wiring directly. You say what feeds into what, and the library builds the component for you. The unsatisfied inputs become the new component's props. This is graph programming applied to React.
Have you ever chased a stale closure bug through a useEffect dependency array? Or watched a parent re-render cascade through child components that didn't even use the state that changed? Or needed to add a parameter deep in a component tree and had to refactor every intermediate component just to thread it through?
Graft eliminates all three by design.
It takes named inputs and produces an output. That's it.
import { z } from "zod/v4";
import { component, View } from "graftjs";
const Greeting = component({
input: z.object({ name: z.string() }),
output: View,
run: ({ name }) => <h1>Hello, {name}</h1>,
});
The output doesn't have to be JSX. A component that returns data is just a transform:
const FormatPrice = component({
input: z.object({ price: z.number() }),
output: z.string(),
run: ({ price }) =>
new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(price),
});
compose({ into, from, key }) feeds from's output into into's input named key. The satisfied input disappears. Unsatisfied inputs bubble up as the new component's props.
import { compose } from "graftjs";
const PriceDisplay = component({
input: z.object({ displayPrice: z.string() }),
output: View,
run: ({ displayPrice }) => <span>{displayPrice}</span>,
});
// FormatPrice needs { price }, PriceDisplay needs { displayPrice }
// After compose: the result needs only { price }
const FormattedPrice = compose({
into: PriceDisplay,
from: FormatPrice,
key: "displayPrice",
});
Wire multiple inputs at once:
const Card = component({
input: z.object({ title: z.string(), body: z.string() }),
output: View,
run: ({ title, body }) => <div><h2>{title}</h2><p>{body}</p></div>,
});
const App = compose({
into: Card,
from: { title: TitleSource, body: BodySource },
});
When all inputs are satisfied (or you want the remaining ones as props), toReact gives you a standard React.FC.
import { toReact } from "graftjs";
const App = toReact(FormattedPrice);
// TypeScript knows this needs { price: number }
<App price={42000} />
In React you'd use useEffect + useState for a WebSocket, a timer, or a browser API. In graft, that's an emitter — a component that pushes values over time. Everything downstream re-runs automatically.
import { emitter } from "graftjs";
const PriceFeed = emitter({
output: z.number(),
run: (emit) => {
const ws = new WebSocket("wss://stream.binance.com:9443/ws/btcusdt@trade");
ws.onmessage = (e) => emit(Number(JSON.parse(e.data).p));
return () => ws.close(); // cleanup on unmount
},
});
Wire it into the graph and you have a live-updating UI with no hooks:
const LivePrice = compose({ into: FormatPrice, from: PriceFeed, key: "price" });
const App = toReact(compose({ into: PriceDisplay, from: LivePrice, key: "displayPrice" }));
// No props needed — everything is wired internally
<App />
Returns a [Component, setter] tuple. The component emits the current value. The setter can be called from anywhere — event handlers, callbacks, outside the graph.
import { state } from "graftjs";
const [CurrentUser, setCurrentUser] = state({
schema: z.string(),
initial: "anonymous",
});
const App = toReact(
compose({ into: Greeting, from: CurrentUser, key: "name" }),
);
// Renders: Hello, anonymous
setCurrentUser("Alice");
// Re-renders: Hello, Alice
In React, everything is "a" by default. Each render creates a counter, a form, a piece of state. Multiple instances are the norm — you get isolation for free via hooks.
Graft defaults to "the". state() creates the cell. emitter() creates the stream. There is exactly one, and every subscriber sees the same value. Definiteness is the default.
instantiate() is how you say "a" — it's the explicit opt-in to indefinite instances. Each subscription gets its own independent copy of the subgraph, with its own state cells and emitter subscriptions.
import { instantiate } from "graftjs";
const TextField = () => {
const [Value, setValue] = state({ schema: z.string(), initial: "" });
const Input = component({
input: z.object({ label: z.string(), text: z.string() }),
output: View,
run: ({ label, text }) => (
<label>
{label}
<input value={text} onChange={(e) => setValue(e.target.value)} />
</label>
),
});
return compose({ into: Input, from: Value, key: "text" });
};
// Two independent text fields — typing in one doesn't affect the other
const NameField = instantiate(TextField);
const EmailField = instantiate(TextField);
A live crypto price card. Price streams over WebSocket, coin name is fetched async, both feed into a card layout.
graph BT
App["<App coinId="bitcoin" />"] -- coinId --> CoinName
CoinName -- name --> Header
Header -- header --> PriceCard
PriceFeed -- price --> FormatPrice
FormatPrice -- displayPrice --> PriceCard
PriceCard -- View --> Output((" "))
style Output fill:none,stroke:none
import { z } from "zod/v4";
import { component, compose, emitter, toReact, View } from "graftjs";
const PriceFeed = emitter({
output: z.number(),
run: (emit) => {
const ws = new WebSocket("wss://stream.binance.com:9443/ws/btcusdt@trade");
ws.onmessage = (e) => emit(Number(JSON.parse(e.data).p));
return () => ws.close();
},
});
const CoinName = component({
input: z.object({ coinId: z.string() }),
output: z.string(),
run: async ({ coinId }) => {
const res = await fetch(
`https://api.coingecko.com/api/v3/coins/${coinId}?localization=false&tickers=false&market_data=false&community_data=false&developer_data=false`,
);
return (await res.json()).name;
},
});
const FormatPrice = component({
input: z.object({ price: z.number() }),
output: z.string(),
run: ({ price }) =>
new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(price),
});
const Header = component({
input: z.object({ name: z.string() }),
output: View,
run: ({ name }) => <h1>{name}</h1>,
});
const PriceCard = component({
input: z.object({ header: View, displayPrice: z.string() }),
output: View,
run: ({ header, displayPrice }) => (
<div>
{header}
<span>{displayPrice}</span>
</div>
),
});
const LivePrice = compose({ into: FormatPrice, from: PriceFeed, key: "price" });
const NamedHeader = compose({ into: Header, from: CoinName, key: "name" });
const App = toReact(
compose({ into: PriceCard, from: { displayPrice: LivePrice, header: NamedHeader } }),
);
// One prop left — everything else is wired internally
<App coinId="bitcoin" />
compose() edges. If emitter A feeds component X and emitter B feeds component Y, A changing has zero effect on Y. This isn't an optimization — graft simply doesn't have a mechanism to cascade re-renders.compose() it directly. No touching the components in between.compose boundary validates with zod. A type mismatch gives you a clear ZodError at the boundary where it happened — not a silent undefined propagating through your tree.run async and loading states propagate automatically. Errors short-circuit downstream. No useEffect, no isLoading boilerplate.run() directly with plain objects, no render harness needed.The idea comes from graph programming. Graft drastically reduces the tokens needed to construct something, and drastically reduces the number of possible bugs. It's a runtime library, not a compiler plugin. ~400 lines of code, zero dependencies beyond React and zod.
When a component is async, graft handles the in-between time with two sentinels that flow through the graph like regular data.
GraftLoading — emitted when a value isn't available yet. Async components emit it immediately, then the resolved value. Emitters emit it until their first emit() call. compose short-circuits on loading — downstream run functions aren't called. toReact renders null.
GraftError — wraps a caught error from an async rejection. Like loading, it short-circuits through compose. toReact renders null.
import { GraftLoading, isGraftError } from "graftjs";
const AsyncData = component({
input: z.object({ id: z.string() }),
output: z.number(),
run: async ({ id }) => {
const res = await fetch(`/api/data/${id}`);
if (!res.ok) throw new Error("fetch failed");
return (await res.json()).value;
},
});
// subscribe() lets you observe the full lifecycle:
AsyncData.subscribe({ id: "123" }, (value) => {
if (value === GraftLoading) {
// Still loading...
} else if (isGraftError(value)) {
console.error("Error:", value.error);
} else {
console.log("Got value:", value);
}
});
state() never produces sentinels — it always has a value (the initial value).
compose deduplicates emissions from from using reference equality (===). If from emits the same value twice in a row, into's run isn't called and nothing propagates downstream. This means an emitter spamming the same primitive is a no-op, and calling a state setter with the current value is free.
npm install graftjs
Requires React 18+ as a peer dependency. Uses zod v4 (zod/v4 import) for schemas.
MIT
FAQs
Compose React components by wiring named parameters — type-safe, no prop drilling, no hooks
We found that graftjs demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
NIST will stop enriching most CVEs under a new risk-based model, narrowing the NVD's scope as vulnerability submissions continue to surge.

Company News
/Security News
Socket is an initial recipient of OpenAI's Cybersecurity Grant Program, which commits $10M in API credits to defenders securing open source software.

Security News
Socket CEO Feross Aboukhadijeh joins 10 Minutes or Less, a podcast by Ali Rohde, to discuss the recent surge in open source supply chain attacks.