Retree - v1.0.0
    Preparing search index...

    Class ConvexPaginatedQueryNode<Query>

    Reactive paginated query node that subscribes to a Convex paginated query and exposes the loaded pages as Retree state.

    Use this directly or through ConvexNode.paginatedQuery for live paginated lists. New pages update state, result, and error, which can emit Retree changes and re-render React subscribers.

    Dispose the node when its owner is torn down. Use "skip" when the query should be temporarily disabled.

    const messages = new ConvexPaginatedQueryNode(client, api.messages.list, {
    args: { channelId: "general" },
    initialNumItems: 20,
    });

    Type Parameters

    Hierarchy (View Summary)

    Index

    Constructors

    Properties

    Convex client used by this node.

    error: Error | null = null

    Latest subscription error.

    Runtime options for this Retree node.

    Retree ignores this field for reactivity so options do not emit or become part of the tree.

    Latest structured query result.

    RETREE_LINKED_KEYS_SYMBOL: Set<string | symbol>
    RETREE_SELECT_GETTERS_SYMBOL: Map<
        string
        | symbol,
        IReactiveSelectGetter<ReactiveNode, unknown>,
    >

    Latest paginated query state emitted by Convex.

    Accessors

    • get dependencies(): never[]

      Dependencies to listen for changes to.

      Returns never[]

      When any IReactiveDependency criteria is met, a change will be emitted for this ReactiveNode instance.

      Keep this getter deterministic. Do not start subscriptions, perform network work, or mutate state here. Use ReactiveNode.onObserved, ReactiveNode.onUnobserved, and ReactiveNode.onChanged for lifecycle work.

      The returned array may change length or ordering while the node is observed. Retree treats added, removed, or reordered entries as invalidation and refreshes subscriptions. Use null when you want an inactive slot to keep its position, but it is not required for correctness.

      class ProjectSummary extends ReactiveNode {
      public tasks: { done: boolean }[] = [];

      get doneCount() {
      return this.tasks.filter((task) => task.done).length;
      }

      get dependencies() {
      return [this.dependency(this.tasks, [this.doneCount])];
      }
      }

    Methods

    • Creates a new IReactiveDependency instance.

      Type Parameters

      • TNode extends object = object

      Parameters

      • node: OptionalNode<TNode>

        the node to listen to "nodeChanged" events for.

      • Optionalcomparisons: any[]

        Optional. Values to compare between updates to node.

      Returns IReactiveDependency<TNode>

      dependency object.

      Use this inside the ReactiveNode.dependencies getter or an @select dependency selector when one slot needs explicit comparison cells. If node is a Retree-managed object, it is observed with nodeChanged. If node is a primitive or unproxied value, Retree treats it as a comparison-only dependency.

      Comparison cells should be deterministic. If their length/order changes, Retree treats that as invalidation and emits for this node. If no comparisons are provided, every nodeChanged event from the dependency emits for this node.

      get dependencies() {
      return [
      this.authStore,
      this.authStore.session?.userId,
      this.dependency(this.selectedProject ?? null, [this.projectId]),
      ];
      }
    • Creates a new IReactiveDependency instance.

      Type Parameters

      • TValue

      Parameters

      Returns IReactiveDependency

      dependency object.

      Use this inside the ReactiveNode.dependencies getter or an @select dependency selector when one slot needs explicit comparison cells. If node is a Retree-managed object, it is observed with nodeChanged. If node is a primitive or unproxied value, Retree treats it as a comparison-only dependency.

      Comparison cells should be deterministic. If their length/order changes, Retree treats that as invalidation and emits for this node. If no comparisons are provided, every nodeChanged event from the dependency emits for this node.

      get dependencies() {
      return [
      this.authStore,
      this.authStore.session?.userId,
      this.dependency(this.selectedProject ?? null, [this.projectId]),
      ];
      }
    • Stop the active Convex paginated query subscription.

      Returns void

      Call this when the owner of the paginated query node is torn down. Disposing stops future Convex updates; it does not clear state, result, or error.

      public dispose() {
      this.messages.dispose();
      }
    • Create a reactive pointer to an existing Retree-managed node.

      Type Parameters

      • TNode extends object

      Parameters

      • node: TNode

        Existing Retree-managed node to point at.

      Returns RetreeLink<TNode>

      A Retree-managed RetreeLink whose current points at node.

      This is a convenience wrapper around Retree.link. Use it when a ReactiveNode method needs to return or store a pointer to a node owned elsewhere without reparenting that node.

      Do not use link when ownership should move; use Retree.move or ReactiveNode.moveTo. Do not use it when two locations need independent state; use Retree.clone.

      class EditorState extends ReactiveNode {
      public selected = null as RetreeLink<Task> | null;

      get dependencies() {
      return [];
      }

      public select(task: Task) {
      this.selected = this.link(task);
      }
      }
    • Request more items for the active paginated query.

      Parameters

      • numItems: number

        Number of additional items to load.

      Returns boolean

      Whether Convex started a load-more request.

      Use this from UI actions such as "Load more" buttons or infinite scroll. It returns false when there is no active paginated state to extend. Passing a non-positive number throws.

      const requested = messages.loadMore(20);
      if (!requested) {
      console.log("No active page to extend");
      }
    • Memoize the result of fn, scoped to this ReactiveNode instance.

      Type Parameters

      • T

      Parameters

      • fn: () => T
      • Optionalcomparisons: unknown[]

      Returns T

      Two forms:

      • Keyless (inside a getter): this.memo(fn, deps?) — derives the cache key from the active getter's property name. Throws if called outside a getter or more than once in the same getter.
      • Explicit key: this.memo(key, fn, deps?) — works anywhere; required when stacking multiple memo cells in one getter, or memoizing inside a method.

      Cache semantics for comparisons:

      • Omitted/undefined: run fn under automatic dependency trapping and recompute when one of the trapped reads changes.
      • []: compute once and cache forever for this instance.
      • [a, b, ...]: recompute when any cell shallow-changes using Object.is. Tree-node cells are compared by their latest reproxy identity, so passing this.list correctly invalidates when list mutates.

      memo is a cache, not a subscription. It does not emit nodeChanged or trigger React renders by itself. Pair it with dependencies, Retree.select, or useSelect when you also need notification behavior.

      class ListFilter extends ReactiveNode {
      list: Card[] = [];
      searchText = "";
      // Keyless form
      get filteredList() {
      return this.memo(
      () => this.list.filter((c) => c.text === this.searchText),
      [this.list, this.searchText]
      );
      }
      // Explicit-key form (e.g. when stacking two memos in one getter)
      get pair() {
      const a = this.memo("a", () => expensiveA(), [this.list]);
      const b = this.memo("b", () => expensiveB(), [this.searchText]);
      return { a, b };
      }
      get dependencies() { return [this.dependency(this.list)]; }
      }
    • Memoize the result of fn, scoped to this ReactiveNode instance.

      Type Parameters

      • T

      Parameters

      • key: string
      • fn: () => T
      • Optionalcomparisons: unknown[]

      Returns T

      Two forms:

      • Keyless (inside a getter): this.memo(fn, deps?) — derives the cache key from the active getter's property name. Throws if called outside a getter or more than once in the same getter.
      • Explicit key: this.memo(key, fn, deps?) — works anywhere; required when stacking multiple memo cells in one getter, or memoizing inside a method.

      Cache semantics for comparisons:

      • Omitted/undefined: run fn under automatic dependency trapping and recompute when one of the trapped reads changes.
      • []: compute once and cache forever for this instance.
      • [a, b, ...]: recompute when any cell shallow-changes using Object.is. Tree-node cells are compared by their latest reproxy identity, so passing this.list correctly invalidates when list mutates.

      memo is a cache, not a subscription. It does not emit nodeChanged or trigger React renders by itself. Pair it with dependencies, Retree.select, or useSelect when you also need notification behavior.

      class ListFilter extends ReactiveNode {
      list: Card[] = [];
      searchText = "";
      // Keyless form
      get filteredList() {
      return this.memo(
      () => this.list.filter((c) => c.text === this.searchText),
      [this.list, this.searchText]
      );
      }
      // Explicit-key form (e.g. when stacking two memos in one getter)
      get pair() {
      const a = this.memo("a", () => expensiveA(), [this.list]);
      const b = this.memo("b", () => expensiveB(), [this.searchText]);
      return { a, b };
      }
      get dependencies() { return [this.dependency(this.list)]; }
      }
    • Move this node to a new structural parent.

      Type Parameters

      Parameters

      • destination: ConvexPaginatedQueryNode<Query> extends TValue ? TValue[] : never

        Retree-managed destination collection or object.

      • Optionalkey: number

        Optional array insertion index, map key, or object property key.

      Returns this

      The latest reproxy for this node after it moves.

      This is a convenience wrapper around Retree.move. Use it from instance methods when a node should transfer ownership to another Retree-managed array, map, set, or object.

      Do not call moveTo on a root node; roots have no parent to remove from. Do not manually remove the node from its current parent before moving.

      class Task extends ReactiveNode {
      public title = "";

      get dependencies() {
      return [];
      }

      public complete(done: Task[]) {
      this.moveTo(done); // same as Retree.move(this, done)
      }
      }
    • Move this node to a new structural parent.

      Type Parameters

      Parameters

      Returns this

      The latest reproxy for this node after it moves.

      This is a convenience wrapper around Retree.move. Use it from instance methods when a node should transfer ownership to another Retree-managed array, map, set, or object.

      Do not call moveTo on a root node; roots have no parent to remove from. Do not manually remove the node from its current parent before moving.

      class Task extends ReactiveNode {
      public title = "";

      get dependencies() {
      return [];
      }

      public complete(done: Task[]) {
      this.moveTo(done); // same as Retree.move(this, done)
      }
      }
    • Move this node to a new structural parent.

      Type Parameters

      Parameters

      Returns this

      The latest reproxy for this node after it moves.

      This is a convenience wrapper around Retree.move. Use it from instance methods when a node should transfer ownership to another Retree-managed array, map, set, or object.

      Do not call moveTo on a root node; roots have no parent to remove from. Do not manually remove the node from its current parent before moving.

      class Task extends ReactiveNode {
      public title = "";

      get dependencies() {
      return [];
      }

      public complete(done: Task[]) {
      this.moveTo(done); // same as Retree.move(this, done)
      }
      }
    • Move this node to a new structural parent.

      Type Parameters

      • TDestination extends object = object

      Parameters

      Returns this

      The latest reproxy for this node after it moves.

      This is a convenience wrapper around Retree.move. Use it from instance methods when a node should transfer ownership to another Retree-managed array, map, set, or object.

      Do not call moveTo on a root node; roots have no parent to remove from. Do not manually remove the node from its current parent before moving.

      class Task extends ReactiveNode {
      public title = "";

      get dependencies() {
      return [];
      }

      public complete(done: Task[]) {
      this.moveTo(done); // same as Retree.move(this, done)
      }
      }
    • Create a typed mutation function bound to this node's Convex client.

      Type Parameters

      Parameters

      • mutation: Mutation

        Convex mutation function reference.

      Returns RetreeConvexMutation<Mutation>

      A typed mutation function with optional optimistic update support.

      The returned function runs the Convex mutation. It does not update Retree state by itself. Pass withOptimisticUpdate when the mutation should immediately update a ConvexQueryNode; otherwise wait for the subscribed query to emit a server value.

      const toggle = this.mutation(api.tasks.toggleCompleted);
      return toggle(
      { taskId },
      {
      withOptimisticUpdate: (ctx) => {
      this.tasks.optimisticUpdate({
      ctx,
      apply(tasks) {
      const task = tasks.find((item) => item._id === taskId);
      if (task) task.isCompleted = !task.isCompleted;
      },
      });
      },
      }
      );
    • Runs after this ReactiveNode receives a fresh reproxy.

      Returns void

      Override this when a node needs to synchronize derived state only after a real Retree change. Retree runs this before nodeChanged / treeChanged listeners flush. If no transaction is already active, Retree starts one so state updates made here are batched with the reproxy that triggered the effect.

      Use this for small synchronization writes that should happen only after Retree has confirmed a real change. Avoid writing unconditionally here; guard against loops by checking whether the derived value actually changed.

      class SearchState extends ReactiveNode {
      public query = "";
      public normalizedQuery = "";

      get dependencies() {
      return [];
      }

      protected onChanged() {
      const next = this.query.trim().toLowerCase();
      if (this.normalizedQuery !== next) {
      this.normalizedQuery = next;
      }
      }
      }
    • Runs when this ReactiveNode gets its first active nodeChanged or treeChanged observer.

      Returns void

      Override this for work that requires the proxied instance, such as starting external subscriptions that write back into Retree state.

      Keep setup idempotent. Retree calls this when the first active nodeChanged or treeChanged listener starts observing the node, not when the node is constructed.

      class LiveValue extends ReactiveNode {
      public value = "";
      @ignore private unsubscribe: (() => void) | null = null;

      get dependencies() {
      return [];
      }

      protected onObserved() {
      this.unsubscribe = subscribe((value) => {
      this.value = value; // ✅ emits through Retree
      });
      }
      }
    • Runs when this ReactiveNode loses its last active nodeChanged or treeChanged observer.

      Returns void

      Use this to clean up resources created in ReactiveNode.onObserved. Do not rely on it as a destructor for unobserved nodes; it only runs after observation had started.

      protected onUnobserved() {
      this.unsubscribe?.();
      this.unsubscribe = null;
      }
    • Prepare lazy Retree child proxies below this ReactiveNode.

      Parameters

      Returns void

      Retree lazily proxies plain object and array fields on ReactiveNodes. Call this when an app wants to pay that first-touch cost during a controlled phase, such as while showing a loading spinner. This walks only own data properties, so computed getters like dependencies are not evaluated or cached as child nodes. Fields marked with @ignore are skipped.

      Do not call this for every render. Call it once during setup, loading, or before a known interaction that will traverse a large subtree.

      class LargeNode extends ReactiveNode {
      public sections = [{ title: "Intro", cards: [] }];

      get dependencies() {
      return [];
      }
      }

      const node = Retree.root(new LargeNode());
      node.prepareTree({ depth: 1 });
    • Update the query arguments and resubscribe when the shallow argument comparison changes. Pass "skip" to disable the subscription.

      Parameters

      Returns void

      Updating args can emit because state, result, and error may change. Passing "skip" disables the active subscription and sets result.status to "skipped".

      messages.updateArgs({ channelId: "random" }); // ✅ may emit
      messages.updateArgs("skip"); // ✅ emits skipped state and unsubscribes