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, IAsyncDisposableMembers#
ComponentToken#
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>#
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#
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)#
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)#
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:
- cancels
ComponentToken, so in-flight executions observe cancellation while they still can; - runs the derived
Dispose()hook, then theDisposeAsync()hook — overrides still see their state attached; - 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.