Skip to content

Add for-loops to html! #3498

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

Merged
merged 16 commits into from
Jul 15, 2025
Merged

Conversation

its-the-shrimp
Copy link
Contributor

Description

  1. Adds the following syntax:
html! {
  for x in 0 .. 10 {
    <span>{x}</span>
  }
}
  1. Disallows top-level {for x}, i.e. the following is no longer valid:
html! { for x }

To disambiguate it and the new for loops, it'll be required to wrap the expression in braces like so:

html! { {for x} }

Checklist

  • I have reviewed my own code
  • I have added tests

@futursolo
Copy link
Member

Thanks for the pull request, however, I do not think an ordinary for-loop should be supported.

What would happen for the following expression?

html! {
  for x in 0..10 {
    <span>{break x}</span>
  }
}

@github-actions
Copy link

github-actions bot commented Oct 29, 2023

Benchmark - core

Yew Master

vnode           fastest       │ slowest       │ median        │ mean          │ samples │ iters
╰─ vnode_clone  2.402 ns      │ 3.65 ns       │ 2.407 ns      │ 2.431 ns      │ 100     │ 1000000000

Pull Request

vnode           fastest       │ slowest       │ median        │ mean          │ samples │ iters
╰─ vnode_clone  2.401 ns      │ 4.446 ns      │ 2.403 ns      │ 2.457 ns      │ 100     │ 1000000000

@github-actions
Copy link

github-actions bot commented Oct 29, 2023

Visit the preview URL for this PR (updated for commit c6c1ffe):

https://yew-rs--pr3498-add-html-for-6g530ixs.web.app

(expires Mon, 21 Jul 2025 20:43:18 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

@github-actions
Copy link

github-actions bot commented Oct 29, 2023

Benchmark - SSR

Yew Master

Benchmark Round Min (ms) Max (ms) Mean (ms) Standard Deviation
Baseline 10 290.874 310.262 292.921 6.094
Hello World 10 485.024 491.947 488.444 2.140
Function Router 10 1577.849 1595.337 1583.533 5.019
Concurrent Task 10 1005.750 1007.183 1006.472 0.526
Many Providers 10 1054.408 1094.138 1072.483 11.907

Pull Request

Benchmark Round Min (ms) Max (ms) Mean (ms) Standard Deviation
Baseline 10 310.447 311.225 310.722 0.223
Hello World 10 469.060 500.601 477.091 11.230
Function Router 10 1643.048 1651.667 1648.178 3.055
Concurrent Task 10 1006.070 1007.165 1006.710 0.320
Many Providers 10 1072.592 1107.600 1089.331 11.480

@github-actions
Copy link

github-actions bot commented Oct 29, 2023

Size Comparison

examples master (KB) pull request (KB) diff (KB) diff (%)
async_clock 99.914 99.914 0 0.000%
boids 168.757 168.778 +0.021 +0.013%
communication_child_to_parent 92.069 92.069 0 0.000%
communication_grandchild_with_grandparent 103.230 103.230 0 0.000%
communication_grandparent_to_grandchild 98.222 98.222 0 0.000%
communication_parent_to_child 87.926 87.926 0 0.000%
contexts 104.172 104.172 0 0.000%
counter 84.729 84.729 0 0.000%
counter_functional 85.062 85.079 +0.017 +0.020%
dyn_create_destroy_apps 87.614 87.655 +0.041 +0.047%
file_upload 98.909 98.909 0 0.000%
function_memory_game 170.597 170.597 0 0.000%
function_router 338.873 338.842 -0.031 -0.009%
function_todomvc 163.421 163.421 0 0.000%
futures 236.384 236.409 +0.025 +0.011%
game_of_life 104.850 104.850 0 0.000%
immutable 194.118 194.195 +0.077 +0.040%
inner_html 80.297 80.297 0 0.000%
js_callback 107.759 107.812 +0.054 +0.050%
keyed_list 195.972 196.013 +0.041 +0.021%
mount_point 83.604 83.604 0 0.000%
nested_list 112.979 112.979 0 0.000%
node_refs 90.975 90.975 0 0.000%
password_strength 1785.237 1785.237 0 0.000%
portals 92.886 92.921 +0.035 +0.038%
router 308.132 307.692 -0.439 -0.143%
suspense 111.856 111.856 0 0.000%
timer 88.877 88.896 +0.019 +0.021%
timer_functional 94.950 94.986 +0.036 +0.038%
todomvc 143.516 143.516 0 0.000%
two_apps 86.018 86.018 0 0.000%
web_worker_fib 135.138 135.159 +0.021 +0.016%
web_worker_prime 185.577 185.613 +0.036 +0.019%
webgl 83.154 83.154 0 0.000%

✅ None of the examples has changed their size significantly.

@its-the-shrimp
Copy link
Contributor Author

Fixed it, now an error is raised when trying to break out of a for loop or continue it

@futursolo
Copy link
Member

futursolo commented Oct 29, 2023

If we block break and continue, then code like the following will no longer work, which should be allowed.

#[function_component]
fn Comp() -> Html {
    html! {
        for i in 0..5 {
            <div>
                {{
                    loop {
                        let a = rand_number();
                        if a % 2 == 0 {
                            break a;
                        }
                    }
                }}
            </div>
        }
    }
}

I am fine with asking users to write the following:

let children = (0..5).map(
    html! { <span key={x}>{x}</span> }
);

html! { for x }

There are 2 reasons:

  1. As mentioned in Parse blocks in html! as Rust block expressions #3466, we want to discourage users to write rust code in html! as much as possible. I think this also extends to complex html expressions.
  2. Elements generated in the for-loop requires elements to be keyed, this further complicates the keying requirements. If you think the current keying requirement is peculiar (as mentioned in Make key fall back to id for VTags #3480). We should not add more rules to it.

The current key requirement can be summarised in 1 sentence:

Everything defined in html! is keyless-safe, with the exception of the top-most ancestor element if it is created from an iterator.

(I consider current {for expr} as a bug.)

@ranile
Copy link
Member

ranile commented Oct 29, 2023

I'm fine with supporting loops. This was discussed on discord as well.

I agree that breaks and continues can be problematic but we could disallow those. These loops can be a for-loop over an iterator that always yields a VNode. This allows us to also lint for presence of keys at compile time.

(I consider current {for expr} as a bug.)

I also agree, however it's not possible to impl Into<Html> for any Iterator type that can be collected into an Html, so it's kinda a necessary evil

@futursolo
Copy link
Member

I also agree, however it's not possible to impl Into for any Iterator type that can be collected into an Html, so it's kinda a necessary evil

I am specifically referring to the {for expr} rendering html! key required.
Each {...} should result in exactly 1 expression and not many.

{for expr} can be fixed by casting a VList at the top of the expression and not expanding it to the parent VList.

@futursolo
Copy link
Member

This allows us to also lint for presence of keys at compile time.

html! {
    for i in fragments {
        {i.to_html()}
    }
}

I think it would be kind of difficult to lint this at compile time.

@its-the-shrimp
Copy link
Contributor Author

its-the-shrimp commented Oct 29, 2023

we want to discourage users to write rust code in html! as much as possible. I think this also extends to complex html expressions

Which is why we can just not care about the peculiar case of nested loops shown above, it'll be rejected anyway.

Elements generated in the for-loop requires elements to be keyed

Not necessarily, unlike {for x}, the new for loops expand into a VList and are not inlined into the parent which discards the problem with keys.

Another argument that can be made in favour of the new for-loops are the possible optimisations and checks, of course one can always just

{for iter.into_iter().map(|x| html!{...})}

Yet, even ignoring how unreadable it can get, which is subjective in a way, calling html! inside html! makes the context unclear, I'll try to add more optimisations & checks to the new for-loops to demonstrate this

@futursolo
Copy link
Member

futursolo commented Oct 29, 2023

Which is why we can just not care about the peculiar case of nested loops shown above, it'll be rejected anyway.

We need to ensure that our implementation is sound. If it is a valid Rust expression, we should accept it. If we wish to reject complex expressions, it needs to be rejected based on complexity and not a limitation introduced by blocking expressions.

Not necessarily, unlike {for x}, the new for loops expand into a VList and are not inlined into the parent which discards the problem with keys.

If you reconcile a for-generated VList of 4 items into 5 items, the renderer will not understand which one you are trying to remove. (That's why you need to key the iterator expression.)

The keying requirement always presents for VList items with a non-fixed length.

The syntax proposed in this pull request (for i in 0..10 { <span key={i}>{i}</span> }) is actually a syntax sugar for (0..10).map(|i| html! {<span key={i}>{i}</span>}).
So everything that applies to an iterator also applies to for-loop.

You can read the React documentation, which explains the keying requirement for Lists in detail: https://react.dev/learn/rendering-lists

The bug for {for expr} is because it renders its adjacent elements to be non keyless-safe as it expands into a parent VList and not items yielded by expr needs keying.

Another argument that can be made in favour of the new for-loops are the possible optimisations and checks, of course one can always just

{for iter.into_iter().map(|x| html!{...})}

Optimisation is also possible for iterators-based implementations with helpers like size_hint().
I do not recommend users do this for readability, but that is also kinda a necessary evil.
However, what is in our control is to simplify and ensure soundness of the html syntax, so we do become part of that evilness.

@its-the-shrimp
Copy link
Contributor Author

its-the-shrimp commented Oct 29, 2023

Added a check to outlaw the following cases:

html!{
  for i in 0 .. 10 {
    <div key="duplicate" />
  }
}

Outlaws the most obvious cases of incorrect keying: when a key is a complex path (i.e. smth::VALUE) or a literal.

Also added an optimisation which allows for avoiding calling recheck_fully_keyed when creating a VList from a for-loop.

Regarding break & continue, I chose a dumb but durable approach of compiling the loop to a call to for_each so that any attempt to break the loop will raise a "break in a closure" error. Not the most straightforward approach but a bug-free one, an alternative would be to basically parse all the expressions by ourselves and run complex AST visitors on them.

@its-the-shrimp
Copy link
Contributor Author

There's some problem with the benchmarks again, some file missing...

@Madoshakalaka
Copy link
Member

Madoshakalaka commented Jul 8, 2025

@its-the-shrimp I'm for merging this. Can you bring this PR up to date? for loops in html makes user code a lot more concise. @ranile 's concern for complex rust code in the macro was rustfmt's inability to format, which is now alleviated by yew-fmt. And @futursolo 's concern for break/continue is kind of addressed too.

Comment on lines +30 to +33
An alternative is to use the `for` keyword, which is not native Rust syntax and instead is used by
the HTML macro to output the needed code to display the iterator.
This approach is better than the first one when the iterator is already computed and the only thing left to do
is to pass it to the macro.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not clear how this approach is different from the collect::<Html> approach. Can you expand a bit more about what the for keyword does in the to_tokens method, maybe mentioning the performance impact and keying?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{for x} appends elements as children of the enclosing element, e.g. <div>{"Hi"}{for [html!("a")].into_iter()}</div> will become <div>{"Hi"}{"a"}</div>.
The problem with that is that now all children of the element require a key, not just the ones generated dynamically. new for loops expand into a separate VList

@Madoshakalaka
Copy link
Member

thanks for the contribution 🚀

@Madoshakalaka Madoshakalaka merged commit fd96a9a into yewstack:master Jul 15, 2025
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants