Angular
injectStitch and injectStitchStream expose a stitch's lifecycle as both Angular signals and an RxJS observable over the framework-agnostic @stitchapi/query-core store — plus an optional TanStack Query adapter.
Use @stitchapi/angular when you call stitches from Angular components and want the
call's lifecycle — pending / success / error, cancellation, refetch, and
streaming re-renders — managed for you. injectStitch is the request/response
function; injectStitchStream re-renders as each delta chunk arrives, which is
the differentiator over plain request/response query libraries.
Every result is exposed two ways from one shared execution: fine-grained
signals (data(), isPending(), …) for templates and computed, and an RxJS
observable (state$) for the async pipe and operators. Both are a thin layer
over @stitchapi/query-core, the
framework-agnostic store that owns the reactive lifecycle.
Example
Install the bindings, the store, core, and Angular:
npm install @stitchapi/angular @stitchapi/query-core stitchapi @angular/corestitchapi, @angular/core, and rxjs are peer dependencies;
@tanstack/angular-query-experimental is an optional peer, needed only for the
queryOptions adapter below.
injectStitch — request / response
Call injectStitch in an injection context (a component field initializer). Pass
the input as a signal or getter (() => this.id()) so the query re-fetches
when it changes — Angular signal inputs are the natural source. Read the result's
signals in the template; the in-flight run is aborted when the component is
destroyed.
import { Component, input } from '@angular/core';
import { injectStitch } from '@stitchapi/angular';
import { stitch } from 'stitchapi';
const getUser = stitch({
baseUrl: 'https://api.example.com',
path: '/users/{id}',
});
@Component({
selector: 'app-profile',
template: `
@if (user.isPending()) {
<app-spinner />
} @else if (user.isError()) {
<button (click)="user.refetch()">Retry</button>
} @else {
<h1>{{ user.data()?.name }}</h1>
}
`,
})
export class ProfileComponent {
readonly id = input.required<string>();
// `id` is a signal — passing a getter re-fetches when it changes.
readonly user = injectStitch(getUser, () => ({
params: { id: this.id() },
}));
}injectStitchStream — live deltas
For a streaming stitch (an sse or stream surface),
injectStitchStream updates as each chunk arrives. chunks() is the running list,
data() is the accumulated array (mode: 'append', default) or the latest chunk
(mode: 'replace'), and status() is 'streaming' until the terminal result,
then 'success':
import { Component, input } from '@angular/core';
import { injectStitchStream } from '@stitchapi/angular';
import { sse } from 'stitchapi/sse';
const chat = sse({ url: 'https://api.example.com/chat' });
@Component({
selector: 'app-chat',
template: `
@for (c of tokens.chunks(); track $index) {
<span>{{ c }}</span>
}
@if (tokens.isStreaming()) {
<app-cursor />
}
`,
})
export class ChatComponent {
readonly prompt = input.required<string>();
readonly tokens = injectStitchStream(chat, () => ({
body: { prompt: this.prompt() },
}));
}Both functions return the same shape — data, error, status, chunks, and the
isPending / isError / isSuccess / isStreaming flags (each a Signal), the
state$ observable, plus refetch and cancel.
The observable: state$
Prefer RxJS? The same state is a multicast Observable<StitchQueryState<T>> on
state$, sharing the one query execution with the signals. Read it with the
async pipe:
import { AsyncPipe } from '@angular/common';
import { Component, input } from '@angular/core';
import { injectStitch } from '@stitchapi/angular';
@Component({
selector: 'app-profile',
imports: [AsyncPipe],
template: `
@if (state$ | async; as s) {
@if (s.isSuccess) {
<h1>{{ s.data?.name }}</h1>
}
}
`,
})
export class ProfileComponent {
readonly id = input.required<string>();
readonly state$ = injectStitch(getUser, () => ({
params: { id: this.id() },
})).state$;
}The shared store: @stitchapi/query-core
The bindings hold almost no logic. All of it — running the call under an
AbortController, publishing status transitions, folding delta chunks into
state, cancel() by aborting, refetch() by re-running — lives in
@stitchapi/query-core's createStitchQuery, a subscribe / getSnapshot
handle with no framework imports and no node:*, so it is browser- and
edge-safe. Angular bridges it to an observable and derives the signals from that:
import { } from '@stitchapi/query-core';
import { } from 'stitchapi';
import { } from 'zod';
const = ({
: 'https://api.example.com',
: '/users',
: .(.({ : .(), : .() })),
});
const = (, { : { : 'ada' } });
const = .(() => {
const = .();
if (.) .(.);
});Because the whole reactive lifecycle lives in the store and not the binding, the
React, Vue, and
Svelte bindings are the same few lines against their
own reactive primitive — Angular's are toSignal + toObservable.
Optional: TanStack Query
Already on TanStack Query?
queryOptions(stitch, input) returns a plain { queryKey, queryFn } object you
pass straight to injectQuery — it imports nothing from
@tanstack/angular-query-experimental, so it works even if you never install it:
import { Component, input } from '@angular/core';
import { queryOptions } from '@stitchapi/angular';
import { injectQuery } from '@tanstack/angular-query-experimental';
@Component({ selector: 'app-profile', template: `...` })
export class ProfileComponent {
readonly id = input.required<string>();
readonly user = injectQuery(() =>
queryOptions(getUser, { params: { id: this.id() } }),
);
}Reach for queryOptions when TanStack Query already owns your view state
and you want a stitch as the queryFn; reach for injectStitch /
injectStitchStream when you want StitchAPI to own the lifecycle directly —
especially for streaming, which injectQuery does not model. See the
TanStack Query guide for how the two
layers split.
See also
Svelte
stitchStore and stitchStreamStore — real Svelte stores wrapping a stitch call, over the framework-agnostic @stitchapi/query-core store. Works on Svelte 4 and 5, plus an optional TanStack Query adapter.
TanStack Query
A stitch is the queryFn. StitchAPI owns the call’s resilience; TanStack Query (or SWR) owns view state — they compose, they do not compete.