DejaComponentBase

Base component that re-renders automatically when the Query<T> and Mutation<T> instances it owns change state. Declare them as fields or properties; the base attaches to each one at initialisation and detaches on dispose.

public abstract class DejaComponentBase : ComponentBase, IAsyncDisposable

Members#

ComponentToken#

protected CancellationToken ComponentToken { get; }

A token cancelled when this component is disposed. The component's queries and mutations already use it — pass it explicitly only to work Deja doesn't run for you, such as a direct API call from an event handler. There is nothing to dispose, and reading it after disposal returns an already-cancelled token rather than throwing, so a late continuation observes cancellation instead of an ObjectDisposedException.

Observe<TState>#

protected TState Observe<TState>(TState state) where TState : IDejaObservable

Attaches state to this component: re-renders on every change, hands it the registered DejaClient and this component's ComponentToken, and detaches on dispose.

Rarely called directly. Discovery runs this for every Query<T> / Mutation<T> held in a field or property at initialisation, which covers the ordinary component. Call it by hand only for state discovery can't see — created after initialisation (an event handler, a per-row list), or in a component whose OnInitialized override skips base.OnInitialized(). Unattached state still fetches; it just never re-renders the component, never reaches the shared cache, and is never cancelled on dispose.

Returns state so it can be used inline; attaching the same instance twice is a no-op. Throws InvalidOperationException when another component already owns the state — give this component its own instance, or pass the data down as a [Parameter]. See Component base for worked examples.

OnInitialized#

protected override void OnInitialized()

Attaches the declared state. An override in a derived component must call base.OnInitialized() — before its own state is used, conventionally on the first line — or nothing is attached and the component never re-renders on state changes.

Dispose (hook)#

protected virtual void Dispose()

Override to add synchronous cleanup; runs during disposal, before the DisposeAsync override and before the base detaches from observed state. Call base.Dispose() from an override.

DisposeAsync (hook)#

protected virtual ValueTask DisposeAsync()

Override to add asynchronous cleanup; runs after the Dispose override, before the base detaches. Call base.DisposeAsync() from an override. Do not declare @implements IAsyncDisposable — the base already implements it, and this override is its extension point.

Disposal order#

When Blazor removes the component, the base:

  1. cancels ComponentToken, so in-flight executions observe cancellation while they still can;
  2. runs the derived Dispose() hook, then the DisposeAsync() hook — overrides still see their state attached;
  3. detaches from all observed state and disposes the queries the component created — cancelling their fetches — even when derived cleanup throws.

Guard rail: redeclared disposal throws#

A derived component declaring its own Dispose or DisposeAsync (typically via @implements IDisposable / @implements IAsyncDisposable) is rejected in the constructor with an InvalidOperationException. A redeclared DisposeAsync would replace Deja's disposal entirely — the component would silently leak its attachments and never dispose its queries; a redeclared Dispose would never be called by anything, because Blazor disposes only through IAsyncDisposable once it is present. Explicit interface re-implementations are also caught; Dispose(bool)-style helpers are allowed. The exception message names the fix: protected override the hooks.

Guard rail: skipped base.OnInitialized()#

Skipping base.OnInitialized() compiles and renders once — then the component silently stops reacting, because nothing was attached. Deja detects this on first render and reports it once as a console error (via stderr, surfaced as console.error on WebAssembly), naming the component and the fix.

Discovery details#

  • Fields and properties are scanned once at initialisation, walking the type hierarchy.
  • [Parameter], [CascadingParameter] and [Inject] members are skipped — parameters are owned by the parent that passed them; attaching would both steal the parent's listener slot (throwing) and dispose state the component doesn't own.
  • State assigned after initialisation is never picked up by the scan — nothing rescans on later renders — so it must be passed to Observe().
  • The scan is trimming-annotated, so it survives published, trimmed WebAssembly builds.