replaceEqualDeep<T> function

T replaceEqualDeep<T>(
  1. Object? previous,
  2. T next, [
  3. int depth = 0
])

Returns previous when next is deep-equal to it, and otherwise next with every deep-equal part swapped for the instance previous already held. This is the default structural sharing of every query, and the reason a refetch that brings back the same data does not rebuild anything: unchanged data keeps its instance, so identical checks and == short-circuits downstream stay cheap and true.

// Task has value equality.
final before = [Task(1, 'a'), Task(2, 'b')];
final after = replaceEqualDeep(before, [Task(1, 'a'), Task(2, 'c')]);
identical(after[0], before[0]); // true: kept
identical(after[1], before[1]); // false: changed

What is walked, and what is compared whole:

  • A List is walked element by element. The result is a new list holding the kept instances, of the incoming list's element type, or previous itself when every element was kept.
  • An InfiniteData is walked as its two lists, pages and pageParams, each shared on its own; the whole is previous when both come back unchanged.
  • A Map or a Set is compared deeply and kept whole when it is deep-equal, otherwise replaced whole: Dart cannot build a map or set of the caller's runtime type from inside a generic function.
  • A class that implements StructurallyShareable is asked to share itself.
  • Everything else is a leaf, compared with ==: a class with value equality is kept when equal, one without is replaced.
  • A typed-data list — Uint8List, Float32List and the rest — is a leaf too, never walked: a byte-by-byte walk of an image would cost more than the rebuild it saves.

A list that cannot grow — fixed-length or unmodifiable — comes back as itself when none of its elements was swapped for a cached instance, and otherwise as a fixed-length copy: add and remove throw on it, list[i] = x does not. An unmodifiable list of the caller's element type cannot be built from in here; a caller who needs the cache to hold one can seal it in a structuralSharing hook, which has the static types.

A set is compared as a multiset under this walk's own equality — never under the set's own policy, so a SplayTreeSet with a case-insensitive comparator still reports a member that changed case. A map is looked up by its own keys, so a map with a custom key equality is compared under that policy: two maps whose keys differ only in a way their comparator ignores are shared, and the older key representation is kept. Give such a map a custom structuralSharing hook, or noStructuralSharing(), when the key representation matters.

Sharing is best effort and never a type error: a previous that is not a T — <num>[] where a List<double> was asked for — is not returned as one, and a shared element that does not fit the incoming list's element type stays next's. Nesting deeper than 500 levels is not walked; past that depth next is returned as it is.

Implementation

T replaceEqualDeep<T>(Object? previous, T next, [int depth = 0]) {
  if (identical(previous, next)) {
    return next;
  }
  if (depth > 500) {
    return next;
  }

  if (previous is TypedData || next is TypedData) {
    return previous is T && previous == next ? previous : next;
  }

  if (previous is List && next is List) {
    final previousLength = previous.length;
    final nextLength = next.length;
    // `toList` on the incoming list keeps its runtime element type, which a
    // `List<Object?>` built here would not. Always a copy when not every
    // element is shared, as TanStack Query's is; the ported suite pins that.
    // The copy is growable only if `next` is.
    final growable = _isGrowable(next);
    final copy = next.toList(growable: growable);
    var swapped = false;
    var equalItems = 0;
    Set<Type>? refused;
    for (var i = 0; i < nextLength; i++) {
      final nextItem = next[i];
      if (i < previousLength) {
        final previousItem = previous[i];
        final shared =
            replaceEqualDeep<Object?>(previousItem, nextItem, depth + 1);
        if (identical(shared, nextItem)) {
          // Nothing to store: the copy already holds `next`'s element. It is
          // still an equal one when both sides held the very same instance.
          if (identical(nextItem, previousItem)) {
            equalItems++;
          }
          continue;
        }
        // The copy has `next`'s element type and `shared` may be `previous`'s
        // element: `<int>[1]` against `<double>[1.0]` finds `1 == 1.0` and
        // would store an `int` in a `List<double>`. A part that does not fit
        // stays `next`'s, and does not count as equal. Whether it fits is a
        // property of its runtime type alone, so a type the list refused once
        // is not tried again: `<int>` against `<double>` used to throw and
        // catch one `TypeError` per element, 2 µs apiece.
        final type = shared.runtimeType;
        if (refused != null && refused.contains(type)) {
          continue;
        }
        try {
          copy[i] = shared;
        } on TypeError {
          (refused ??= <Type>{}).add(type);
          continue;
        }
        swapped = true;
        if (identical(shared, previousItem)) {
          equalItems++;
        }
      }
    }
    if (previousLength == nextLength &&
        equalItems == previousLength &&
        previous is T) {
      return previous as T;
    }
    // A list that cannot grow may be one its writer sealed. With nothing
    // swapped in, the copy holds exactly `next`'s elements, so `next` itself
    // is the better answer: still sealed. With something swapped in, the
    // fixed-length copy keeps the cached instances and refuses `add`.
    if (!growable && !swapped) {
      return next;
    }
    // `next` is a `T`; its `toList()` usually is too, but a `T` narrower
    // than a plain list (a user's `ListBase`, say) is not.
    return copy is T ? copy as T : next;
  }

  if (previous is InfiniteData && next is InfiniteData) {
    // Same runtime type, or the shared lists could not be handed back into
    // `next`'s page types.
    if (previous.runtimeType != next.runtimeType) {
      return next;
    }
    final pages =
        replaceEqualDeep<List<Object?>>(previous.pages, next.pages, depth + 1);
    final pageParams = replaceEqualDeep<List<Object?>>(
        previous.pageParams, next.pageParams, depth + 1);
    if (identical(pages, previous.pages) &&
        identical(pageParams, previous.pageParams) &&
        previous is T) {
      return previous as T;
    }
    // Built on `previous`, whose `copyWith` keeps a list handed back
    // unchanged (identity included) and copies a changed one into an
    // unmodifiable list — the cache never holds a growable page list.
    return previous.copyWith(pages: pages, pageParams: pageParams) as T;
  }

  if (previous is Map && next is Map) {
    return previous is T && _mapsEqualDeep(previous, next, depth)
        ? previous as T
        : next;
  }

  if (previous is Set && next is Set) {
    return previous is T && _setsEqualDeep(previous, next, depth)
        ? previous as T
        : next;
  }

  // `is T` and not just `==`: `1 == 1.0` holds in Dart, and an `int` handed
  // back where a `double` was asked for would not be the caller's type.
  if (previous is T && previous == next) {
    return previous;
  }
  if (next is StructurallyShareable<Object?> &&
      previous != null &&
      previous.runtimeType == next.runtimeType) {
    Object? candidate;
    try {
      // The argument check is the class's own: `shareWith(T previous)` is
      // covariant in `T`, and the same runtime type is what makes it pass.
      final shared = next.shareWith(previous);
      if (shared is T) {
        candidate = shared;
      }
    } catch (_) {
      // Best effort: the incoming value stands.
    }
    // Not asserted. `candidate == next` fails every correct `shareWith` of a
    // class with identity equality, and `!identical(candidate, previous)`
    // the one of them that keeps the instance when nothing changed. With an
    // `==` that is not deep, the walk cannot tell a correct result from a
    // stale one; the contract is on [StructurallyShareable].
    if (candidate != null) {
      return candidate as T;
    }
  }
  return next;
}