-
Notifications
You must be signed in to change notification settings - Fork 10.4k
Description
This is inspired by, and would basically be equivalent to, the key
feature in React. It provides a way to tell the system how you want it to preserve child element/component instances when diffing a list.
Use case
Consider the existing situation where you don't have this feature:
<div>
@foreach (var flight in Flights)
{
<DetailsCard Flight="@flight" />
}
</div>
If you add a new flight into the middle of the Flights
list, what you'd want is that all the existing DetailsCard
instances are unaffected, and one new DetailsCard
gets created and put into the rendered output.
To visualize this, if Flights
previously contained [F0, F1, F2]
, then this is the before state:
- DetailsCard0, with Flight=F0
- DetailsCard1, with Flight=F1
- DetailsCard2, with Flight=F2
... and this is the desired after state, given we insert a new item FNew
at index 1:
- DetailsCard0, with Flight=F0
- DetailsCardNew, with Flight=FNew
- DetailsCard1, with Flight=F1
- DetailsCard2, with Flight=F2
However, the actual after state with the present diff algorithm is this:
- DetailsCard0, with Flight=F0
- DetailsCard1, with Flight=FNew
- DetailsCard2, with Flight=F1
- DetailsCardNew, with Flight=F2
The system has no way to know that DetailsCard2 or DetailsCard3 should preserve their associations with their older Flight instances, so it just re-associates them with whatever Flight matches their position in the list. As a result, DetailsCard1 and DetailsCard2 rebuild themselves completely using new data, which is wasteful and sometimes even leads to user-visible problems (e.g., input focus is unexpectedly lost).
Fixing this with 'key'
The developer should be able to make their app more efficient (and sometimes, better behaving) using key
like this:
<div>
@foreach (var flight in Flights)
{
<DetailsCard key="@flight" Flight="@flight" />
}
</div>
Here, key
is a new built-in intrinsic that you can use on any component, or any element. The result of this will be that you do get the desired after state shown above.
To achieve this, the diff algorithm:
- Notices if you've supplied a
key
, then from that point onwards within the current parent frame (in the above example, the<div>
), it changes the rules for retaining child elements/components - In this mode, previous child elements/components are reused if and only if their old frames have the same
key
value as the new frames. - That is, use of
key
is a equally a way of guaranteeing the non-preservation of children that don't match existing frames- As a further improvement, we might want to refine this by saying we can preserve any instances where both old and new don't have any
key
specified, like the system already does. Not sure whether that will complicate the implementation. It would improve efficiency in cases where you have a lot of top-level elements/components within a@foreach
block, and are only usingkey
for some of them.
- As a further improvement, we might want to refine this by saying we can preserve any instances where both old and new don't have any
- If the
key
value is anint
, we can use and compare it directly (and can store such values on theRenderTreeFrame
without additional boxing). For other types, we'll have to callGetHashCode
to get anint
. We can acceptnull
and give it some arbitrary int value (e.g.,0
). Alternatively, we could consider doing reference comparisons for other types, as the perf would be improved but it might surprise people that we don't respectGetHashCode
. - If the
key
values aren't unique within a given parent, this is an anomalous case, and preservation behavior is undefined. We'll probably preserve the first instance for each nonunique key value, and then create new instances for the rest. As long as the output is legal (like it already is) we don't have to define the rules more precisely. - If the
key
values match a certain old and new frame, but those frames aren't candidates for retention (e.g., they are different types of child component or element), this is a anomalous case, and the system will do whatever's easiest to implement while still being legal diff output. For example, we might consider that key value to be "used up" even though we don't retain the child (because it was incompatible).
Implementation note: Naively, this can be achieved either by a nested loop (for each new output frame, scan the old frames to find one with a matching key, and somehow track which ones were used already), or by a hash join (while processing, build a dictionary of the old frames by key, then iterate over the output frames to attach the corresponding children or create new children when there isn't a match). However we would want to avoid allocations, so might need some shared dictionary within the diff context.
To be clear, even though the example above uses key
on a component, you could equally use it on plain HTML elements, e.g.:
@foreach (var todo in TodoItems)
{
<input key="@todo.Id" bind="@todo.Text" />
}
This is valuable because it ensures you preserve things like focus, cursor position, visibility state of tooltips, etc., if the system injects new items into the list while the user is interacting with it.
Why can't the system do this automatically? Why does the developer have to specify key
explicitly?
Theoretically we could make this more automatic, using either of these techniques:
- Within a
@foreach (var x in ...) { ... }
block, we could implicitly put akey=@x
on the first top-level component/element in the block. Drawbacks:- Adding another top-level thing might break your app by changing retention behavior, which is surprising and weird
- It adds diff cost in all cases, even when there's no benefit
- Or, we make the default retention logic more sophisticated, so that even without any
key
, it tries to find the optimal old-new matches by diffing the attributes and picking matches to minimize edit distance.- I think it's obvious that we're not going to want to add this extra diff cost in all cases, especially given how it makes the behavior harder to predict. People would end up with really obscure, hard-to-repro bugs where the retention behavior switches around depending on user-entered data.