replaceEqualDeep<T> function
Structural sharing
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
Listis walked element by element. The result is a new list holding the kept instances, of the incoming list's element type, orpreviousitself when every element was kept. - An InfiniteData is walked as its two lists,
pagesandpageParams, each shared on its own; the whole ispreviouswhen both come back unchanged. - A
Mapor aSetis 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,Float32Listand 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;
}