New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Regression: elements are re-rendered even if they didn't change, when there's a list below them (worked in v0.19.3) #3262
Comments
If you have a dynamically length list of children, and you want to make sure that the correct elements are "identified", use keyed lists. This is how you'd guarantee the previous behaviour, instead of relying on an implementation detail: #[function_component(Video)]
pub fn video() -> Html {
let number = use_state_eq(|| None::<i64>);
let time_update = {
let number = number.clone();
Callback::from(move |event| {
// number_from_video() returns some number
// based on the video's current position
number.set(number_from_video(event));
})
};
let number_vec = {
if let Some(num) = *number {
vec![html! {
<button key="button" class="my-button">
{format!("Number: {num}")}
</button>
}]
} else {
Vec::new()
}
};
let number_opt = number.map(|num| html! {
<button key="number" class="my-button">
{format!("Number: {num}")}
</button>
});
html! {
<>
// The video element gets reconciled with the existing video element and is not replaced
<video key="video" muted=true controls=true type={VIDEO_TYPE} src={VIDEO_URL}
ontimeupdate={time_update}>
</video>
// if let Some(num) = *number {
// <button key="case1" class="my-button">
// {format!("Number: {num}")}
// </button>
// }
{ for number_vec } // Case 2: Works with keys
{ for number_opt } // Case 3: Works with keys
</>
}
} |
I believe this is actually a regression where the vectors are wrongly flattened by html! {
<>
// The video element gets reconciled with the existing video element and is not replaced
<video muted=true controls=true type={VIDEO_TYPE} src={VIDEO_URL}
ontimeupdate={time_update}>
</video>
// if let Some(num) = *number {
// <button class="my-button">
// {format!("Number: {num}")}
// </button>
// }
<> // Html macro should expand for expression into a fragment / VList
{ for number_vec } // Case 2: Works without keys
</>
<> // Html macro should expand for expression into a fragment / VList
{ for number_opt } // Case 3: Works without keys
</>
</>
} |
I'm aware of keyed lists, but I tought they are only used to identify elements from the same list. I assumed whether or not using keys in It does fix the issue though (as long as all of the elements in the same fragment have keys (i.e. adding a key just to the My understanding is that yew would diff each of the rendered elements from the component with the last rendered version to check whether an update was needed. Am I misunderstanding something here? Because the Or is futursolo right and it's indeed a bug? @WorldSEnder |
While mostly correct, the point is that it's not clear which nodes should be diffed with which previous ones especially if the lists differ by length. As an aside and only considering the situation without keys, if both lists of children have the same length, nodes are currently diffed by position, i.e. the two lists are zipped and each pair reconciled. I don't think this part will change, even if it's fyi technically not guaranteed by the API. But you shouldn't be forced to use keys for layouts that don't have conditional children. If list lengths differ, yew currently reserves the right to do "whatever it wants" when pairing up nodes from the two lists. Perhaps its best explained by example. Since nested children are not relevant, I'll omit those. In your original post the diffing would have to consider
While my visual aligment is what you may expect, it's not so clear behind the scenes. What ends up happening is that the As an aside, Case 1 also spuriously works because of an implementation detail. It turns out that the
This is similar to how futursolo's approach works. By nesting the conditionally rendered children in further fragments, they end up always consuming one spot in the list that contains the Keyed lists solve the problem of not being able to identify children between renders. My solution would look like
By using the special key attribute, it's made clear to yew which nodes to reconcile with which others, no matter the lists changing lengths. Node pairs with the same key from old and new vdom are identified and reconciled. That's also why the key must be unique in each list, but not globally, since it only matters in this step. Relevant PR that touched it: #2330 Addendum: for the "flattening" of the following - at least I'm also not immediately sure why it works/doesn't work
I thought you'd have to use the |
I will take some time to explain in detail during the weekend, however my opinion is that I do not think for expression (or anything else) should make keys to be reconciled at a higher level hence make an originally keyless-safe part of a document fragment to be key-required. I do wish to quickly point out the following:
In my opinion, conditional rendering should work without keys by design. Background: In React, conditional rendering (e.g.: |
Thank you for the detailed explanation, it's very useful. Intuitively though, I would assume yew that when I use
should turn into <>
<video />
<></>
<a />
</> when <>
<video />
<>
<button />
<button />
<button />
</>
<a />
</> so that
What I'm trying to say is that my intuition is that yew would know that I'm expanding a collection, and would take that into account when deciding what should get reconciled with what. Easier said than done, I guess 😅 Anyway, I have a question about keyed lists: at this moment I have no easy way to give unique keys to all of my dynamic elements. Is having a keyed list in which some elements have the same key a bad idea? In my current use-case it's enough that the <video key="video" />
<button key="dont_care" />
<button key="dont_care" />
<button key="dont_care" /> |
EDIT: You need to force it, and jsx syntax might differ here. You can definitely reproduce the problem in react if you want it, I just haven't found a way with jsx syntax.
|
Yes. In "worst case" where you don't any identifying info, you could use the index in your list as a key via |
A falsy value for The JSX expression is more equivalent to:
That's why declarative syntax is safe without keys where arrays and iterators isn't as in a JSX expression / HTML macro, the declared block can always indicate there is an expression regardless of whether it is rendered. |
I just ran into this and while it makes sense that yew reserves the right to do whatever it wants when pairing the nodes it feels unintuitive to patch child nodes in reverse order: yew/packages/yew/src/dom_bundle/blist.rs Line 172 in 2c4ea60
Especially for something like: #[function_component]
fn BuggyBlist() -> Html {
let state = use_state(|| false);
let toggle = {
let state = state.clone();
Callback::from(move |_| state.set(!*state))
};
let header = html! {
<>
<input
name="A"
type="text"
/>
<input
name="B"
type="text"
/>
</>
};
if *state {
html! {
<>
<button onclick={toggle}> {"Toggle"} </button>
{ header }
// <></>
</>
}
} else {
html! {
<>
<button onclick={toggle}> {"Toggle"} </button>
{ header }
<div>
{ "end element" }
</div>
</>
}
}
} We're adding and removing the end element but this manifests as re-rendering all of the previous input fields. |
Sorry about the delay. I finally have some time to sit down and explain my opinion on this issue. In short, I do not think expanding My main concern of the current However, this is not the only concern around this extending behaviour. So I will discuss this in detail: 1. An extending for-expression is hard to spot, and hard to maintain.Expanding Whilst we can make a distinction between The above, which increased the likelihood of users using Suppose that a developer has created the following page: html! {
<div class="page-container gutter-left gutter-right">
<main class="main">
<header class="header header-stand gutter-bottom flex-container">
<div class="header-container">
<h1 class="header-page-title h1-standout highlight gutter-bottom">{"Title"}</h1>
<nav>
<a href="/" class="nav-link current home nav-item-flex">
<span class="link-text">{"Home"}</span>
</a>
<a href="/" class="nav-link nav-item-flex">
<span class="link-text">{"Articles"}</span>
</a>
<a href="/" class="nav-link nav-item-flex">
<span class="link-text">{"Home"}</span>
</a>
<a href="/" class="nav-link nav-item-flex">
<span class="link-text">{"Log In"}</span>
</a>
<a href="/" class="nav-link nav-item-flex">
<span class="link-text">{"Register"}</span>
</a>
</nav>
</div>
</header>
<div class="main-content max-width-xl">
<div class="main-content-container gutter-top gutter-buttom">
{article_1}
</div>
</div>
<div class="main-content max-width-xl">
<div class="main-content-container gutter-top gutter-buttom">
{article_2}
</div>
</div>
<div class="main-content max-width-xl">
<div class="main-content-container gutter-top gutter-buttom">
{article_3}
</div>
</div>
<div class="main-content max-width-xl">
<div class="main-content-container gutter-top gutter-buttom">
{article_4}
</div>
</div>
<div class="main-content max-width-xl">
<div class="main-content-container gutter-top gutter-buttom">
{article_5}
</div>
</div>
<form class="form-contact-us">
<div class="form-field-container">
<input
name="email"
placeholder="E-Mail Address"
type="email"
class="content-field-input input-text-field gutter-top gutter-bottom styled-rounded"
/>
</div>
<div class="form-field-container">
<input
name="phone"
placeholder="Phone Number"
type="text"
class="content-field-input input-text-field gutter-top gutter-bottom styled-rounded"
/>
</div>
<div class="form-field-container">
<input
name="phone"
placeholder="Phone Number"
type="text"
class="content-field-input input-text-field gutter-top gutter-bottom styled-rounded"
/>
</div>
<div class="form-field-container full-width">
<textarea
name="message"
placeholder="Message"
class="content-field-input input-textarea gutter-top gutter-bottom styled-rounded"
></textarea>
</div>
{for random_3_questions}
<div class="form-field-container">
<input
type="submit"
value="Submit"
class="content-field-input button gutter-top styled-rounded"
/>
</div>
</form>
{for seasonal_contents}
<div class="footer-paginator">
<div class="paginator-arrow paginator-prev">
<i class="arrow-left"></i>
<span>{"Previous Page"}</span>
</div>
{paginator_nos}
<div class="paginator-arrow paginator-next">
<i class="arrow-right"></i>
<span>{"Next Page"}</span>
</div>
</div>
<footer class="page-footer">
<div class="footer-container">
<div class="footer-line">
<h5>
<span class="font-secondary color-secondary">{"©"}</span>
<span class="font-primary color-secondary">{"My Website"}</span>
</h5>
</div>
</div>
</footer>
</main>
<aside class="page-sidebar gutter-right styled-rounded">
{sidebar_contents}
</aside>
</div>
} By including The developer of this page, is finding that the contents are not updating as expected that contents are getting resetted from time to time, and is trying to debug this page. This creates some questions for the debugging process:
In addition, this is not only a concern of when one is working on their own code, but also when reviewing other people's code when adding a 2. What key should a
|
I was also thinking about the ergonomics of this a bit. I always thought (before reading the source) that it would be possible to partially key child elements. Being able to just key the important elements without keying everything would make it easier to spot check that the essential elements will always be reused regardless of surrounding changes or surrounding lists. Is there a reason this isn't supported? It seems like the keyed and unkeyed implementations are just less generic partially keyed implementations. |
Do we need to make the |
After trying to implement expansion of |
Closing this as Yew 0.21 has been released with the fix |
I believe this has not been fixed.
|
I've just discovered that a vector of use yew::prelude::*;
#[function_component]
fn App() -> Html {
let x = vec![html!{"A"}, html!{"B"}];
html! { <div>{x}</div> }
}
fn main() {
yew::Renderer::<App>::new().render();
} So then the only issue here is not documenting the difference between |
Problem
Elements are re-rendered even if they didn't change, when there's a list below them. It works correctly in yew
0.19.3
, but no longer in yew0.20.0
.This is especially problematic when the re-rendered element is e.g. a
<video>
since when it gets re-rendered it stops and resets to the beginning. This is currently blocking me from updating0.19.3
->0.20.0
Steps To Reproduce
Run/see the full repro in this repo: https://github.com/andoalon/yew-bug (just run
trunk serve --open
)In short, it boils down to this:
Expected behavior
Elements are only re-rendered if they changed.
Observed behavior with
<video>
It resets to the beginning and stops when the list below it changes. On a different note, I noticed that the
muted
attribute of<video>
seems to not be working. Not sure if I misused it 🤔Environment:
v0.20.0
(previously was usingv0.19.3
where the issue was not present)1.69.0
(stable)wasm32-unknown-unknown
(not sure if relevant)trunk
(not sure if relevant)Questionnaire
The text was updated successfully, but these errors were encountered: