Skip to content

Angular ​

@rayfold/angular connects components to the Rayfold client's cache. When a command changes a book, every component showing that book updates with the new value. Nothing is fetched again, and components showing other books do not recompute. It needs Angular 19 or newer, and no zone.

sh
npm install @rayfold/angular @rayfold/client

The client keeps a normalised cache and pushes changes to it. A signal invalidates on push and computes on read, so the binding is thin: these are the client's own subscriptions, exposed as signals.

Provide the client ​

ts
import { bootstrapApplication } from "@angular/platform-browser";
import { RayfoldClient, createFetchTransport } from "@rayfold/client";
import { provideRayfold } from "@rayfold/angular";

const client = new RayfoldClient({
  transport: createFetchTransport({ url: "/rayfold", headers: () => ({ authorization: `Bearer ${token()}` }) }),
});

bootstrapApplication(App, { providers: [provideRayfold(client)] });

Read with injectQuery ​

ts
@Component({
  selector: "book-card",
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    @if (book.loading()) { <p>Loading…</p> }
    @else if (book.error()) { <p>Could not load it.</p> }
    @else { <h2>{{ book.data()?.title }}</h2><p>{{ book.data()?.stock }} in stock</p> }
  `,
})
export class BookCard {
  readonly id = input.required<string>();
  readonly book = injectQuery<Book>("book", () => ({ id: this.id() }), { shape: "{ id title stock }" });
}

The arguments are a function, so they can read signals. When id() changes, the query re-runs and the previous subscription ends. Pass a plain object when nothing about the call changes.

The request goes out the moment the query is injected, before the first change detection. loading() is true immediately, so a template never paints an empty state it should not.

A query that fails after it answered keeps its data() beside error(), so a screen can show what it had and say what went wrong; the next answer clears the error.

refetch() goes back to the server and ignores the cache. enabled: false sends nothing, for a query that has to wait for an id. Like the arguments, it may be a function, and then the query starts by itself once the id arrives:

ts
readonly book = injectQuery<Book>("book", () => ({ id: this.id() }), { enabled: () => this.id() !== "" });

Change with injectCommand ​

ts
export class RestockButton {
  readonly id = input.required<string>();
  readonly restock = injectCommand<Book>("restock");
  // error() is unknown: anything can be thrown, so the template gets a message rather than the value
  readonly problem = computed(() => {
    const e = this.restock.error();
    return e instanceof Error ? e.message : e ? "Could not restock." : "";
  });

  run(id: string) {
    this.restock.run({ id, qty: 5 });   // returns a promise; the outcome also lands in the signals
  }
}
html
<button (click)="run(id())" [disabled]="restock.running()">Restock</button>
@if (problem()) { <p>{{ problem() }}</p> }

data() is the result of the latest run, and a run that fails clears it and sets error(). Each run gets a fresh idempotency key unless you pass one, so a retry is replayed, not run twice. The command's patches reach the cache, and every query showing that book updates, including queries in components that know nothing about this button.

Follow other people's changes with injectLive ​

ts
readonly book = injectLive<Book>("book", () => ({ id: this.id() }), { shape: "{ id title stock }" });

Same signals, except the server pushes every change to the result, whoever made it. The subscription ends when the component is destroyed or the arguments change. Give the client a WebSocket transport to share one connection between many live queries (Live updates).

Tests and server rendering ​

Since 0.2.2

This came with 0.2.2; in 0.2.1 the application can count as stable before the first answer.

A query or a live query keeps the application unstable until its first answer arrives, and a command keeps it unstable while it runs. await fixture.whenStable() in a test waits for the data instead of capturing "Loading...", and so does server rendering. Testing shows a test without a network.

Why signals rather than RxJS ​

The client is a cache that changes, not a stream of requests. An Observable models "the next value arrives". What happens in the cache is "this value is now stale; recompute when someone reads it", and that is what a signal models. If you need an observable at the edge of an existing RxJS codebase, toObservable from @angular/core/rxjs-interop converts any of these signals.

Next ​

Released under the Apache-2.0 license.