Skip to content

Windows Reactor - #4479

Merged
Kenny Kerr (kennykerr) merged 1 commit into
masterfrom
reactor
May 28, 2026
Merged

Windows Reactor#4479
Kenny Kerr (kennykerr) merged 1 commit into
masterfrom
reactor

Conversation

@kennykerr

@kennykerr Kenny Kerr (kennykerr) commented May 28, 2026

Copy link
Copy Markdown
Collaborator

Windows Reactor is a UI library for Rust developers targeting WinUI 3 to deliver native, efficient Windows experiences.

Overview

windows-reactor brings a React-like component model to native Windows desktop apps:

  • Declarative UI, no macros — build UIs as pure functions of state using a builder DSL and tuple-based children
  • Function components — typed props, context propagation, and error boundaries
  • Hooksuse_state, use_reducer, use_effect, use_context, use_memo, use_callback, use_resource, and more
  • 55+ widgets — Button, TextBlock, NavigationView, Grid, and many more, plus virtualized lists (ListView, GridView, FlipView) with templated row rendering
  • Asyncuse_resource and use_mutation for background data loading
  • Theming — runtime light/dark switching with ThemeRef brush bindings
  • Accessibility — built-in automation property modifiers, keyboard accelerators, and tooltips
  • Small footprint — single ~3 MB binary, no runtime framework to deploy

Getting Started

Prerequisites

Creating a new app

  1. Add the dependency:

    [dependencies]
    windows-reactor = { git = "https://github.com/microsoft/windows-rs" }
    
    [build-dependencies]
    windows-reactor-setup = { git = "https://github.com/microsoft/windows-rs" }
  2. Add the build step:

    fn main() {
        windows_reactor_setup::as_self_contained();
    }
  3. Write your app:

    use windows_reactor::*;
    
    fn main() -> Result<()> {
        App::new().title("sample").render(app)
    }
    
    fn app(cx: &mut RenderCx) -> Element {
        let (count, set_count) = cx.use_state(0);
        let click = move || set_count.call(count + 1);
    
        vstack((
            button("Click").on_click(click),
            text_block(format!("count = {count}"))
                .font_size(18.0)
                .bold(),
        ))
        .into()
    }

Running the samples

cargo run -p gallery
image
cargo run --example solitaire
image
cargo run --example calculator
image

Performance

Compared to the equivalent C# Reactor gallery app (measured 2026-05-27):

Metric Rust C# (JIT) C# (PublishAOT)
Clean build time 11.0 s 23.9 s 50.8 s
Deploy size 3.34 MB 128 MB 163 MB
Time to first window 160 ms 465 ms 364 ms
Working set (after settle) 109.5 MB 162.6 MB 128.4 MB
Private memory 101.0 MB 121.0 MB 117.3 MB
CPU time (startup + settle) 594 ms 1,063 ms 906 ms
Reconcile time (4,900 cells @ 10%) 3.1 ms 27.0 ms 29.4 ms

Special Thanks

Thanks to Chris Anderson for kickstarting this project and convincing me to give WinUI another try. Chris is the brains behind Reactor for C#. And I couldn't have done this without Rafael Rivera whose knowledge of Windows internals and WinUI continues to impress.

This builds on a mountain of work in windows-rs to optimize code generation, build time, and ergonomics. Over the last few weeks alone, the bindgen pipeline gained method-level filtering and mixed allow/deny lists with vtable demotion so that windows-reactor can generate only the exact COM surface it needs — no dead vtable slots, no unused methods. Delegate code gen was minimized to emit void-returning handlers with direct S_OK returns, and event registration now accepts closures directly with a non-generic EventRevoker. Two new crates landed: windows-reference for zero-overhead IReference<T> boxing (used throughout the reactor for nullable property values) and windows-time for idiomatic TimeSpan/DateTime conversions. The metadata reader was simplified by merging TypeIndex and ItemIndex, and the tokenizer switched to the quote crate — both reducing bindgen build time. Even if you're not interested in UI development, the profiling that went into this project greatly improved the core windows-* crates as well.

@kennykerr
Kenny Kerr (kennykerr) merged commit 65066a7 into master May 28, 2026
29 checks passed
@kennykerr
Kenny Kerr (kennykerr) deleted the reactor branch May 28, 2026 19:02
@dongle-the-gadget

Dongle (dongle-the-gadget) commented May 28, 2026

Copy link
Copy Markdown

Looking at your benchmarks: The C# AOT numbers don't make sense to me (my non-trivial WinUI 3 AOT app for all three architectures take 6 minutes to build, and takes up ~50 MB of disk space). I think that the AOT version might have actually been self-contained rather than AOT compiled.

@SaverinOnRails

Copy link
Copy Markdown

Looking at your benchmarks: The C# AOT numbers don't make sense to me (my non-trivial WinUI 3 AOT app for all three architectures take 6 minutes to build, and takes up ~50 MB of disk space). I think that the AOT version might have actually been self-contained rather than AOT compiled.

Yh these stats are funny. Rust would out perform C# of course but not by this much.

@kennykerr

Copy link
Copy Markdown
Collaborator Author

There are some internal discussions about deployment size, now that I have created a table. Stay tuned. 🙃

The actual numbers aren't that important - what matters is they were all measured on the same (old) machine.

@davidfowl

David Fowler (davidfowl) commented May 29, 2026

Copy link
Copy Markdown

Looking at your benchmarks: The C# AOT numbers don't make sense to me (my non-trivial WinUI 3 AOT app for all three architectures take 6 minutes to build, and takes up ~50 MB of disk space). I think that the AOT version might have actually been self-contained rather than AOT compiled.

It's getting fixed 😄 (the app and benchmarks). We're working with the reactor team to push some updated numbers.

@tusharsnx

Copy link
Copy Markdown

Are we going to take the same wild ride that react did from use_memo, use_callback to automatic memoization (with React compiler)?

@nicoburns

Copy link
Copy Markdown

Is there an API to use the widgets without the reactivity layer? I'm interested in doing cross-platform UI, and parts of this look like they could be a great foundation for the window part of a "react native in rust". But in that case you'd probably want to cross-platform framework to own the reactivity and state management...

@kennykerr

Copy link
Copy Markdown
Collaborator Author

Are we going to take the same wild ride that react did from use_memo, use_callback to automatic memoization (with React compiler)?

Interesting question. Rust's type system eliminates some classes of bugs the React Compiler catches (use-after-move, data races), and our reconciler already skips unchanged subtrees via PartialEq diffing. That said, there's a lot still to learn about making this work well in Rust; whether there's value in automating this further at compile time is something we'll consider as the library matures and we gain more experience in this space.

@kennykerr

Copy link
Copy Markdown
Collaborator Author

Is there an API to use the widgets without the reactivity layer? I'm interested in doing cross-platform UI, and parts of this look like they could be a great foundation for the window part of a "react native in rust". But in that case you'd probably want to cross-platform framework to own the reactivity and state management...

Yes, windows-reactor is built on windows-bindgen, which provides the bindings for the APIs used for everything you see.

@Agritite

Agritite commented May 30, 2026

Copy link
Copy Markdown

when I cargo build with

[dependencies]
windows-reactor = { git = "https://github.com/microsoft/windows-rs" }

This error occurs

PS D:\projects\rust-playground> cargo build
   Compiling windows-reactor v0.0.0 (https://github.com/microsoft/windows-rs#e875dea0)
   Compiling windows-reference v0.1.0 (https://github.com/microsoft/windows-rs#e875dea0)
error: failed to run custom build command for `windows-reactor v0.0.0 (https://github.com/microsoft/windows-rs#e875dea0)`

Caused by:
  process didn't exit successfully: `D:\projects\rust-playground\target\debug\build\windows-reactor-352a580a57f9edc8\build-script-build` (exit code: 101)
  --- stdout
  cargo:rerun-if-changed=build.rs

  --- stderr

  thread 'main' (14464) panicked at C:\Users\user\.cargo\git\checkouts\windows-rs-a5de4a2dc783ec71\e875dea\crates\libs\reactor\build.rs:51:72:
  called `Result::unwrap()` on an `Err` value: NotPresent
  note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
warning: build failed, waiting for other jobs to finish...

CARGO_WORKSPACE_DIR is not defined. Am I missing something?

@kennykerr

Kenny Kerr (kennykerr) commented May 30, 2026

Copy link
Copy Markdown
Collaborator Author

Yes, Rafael Rivera (@riverar) is working on a fix for this: #4487

You can obviously work around this (by cloning the repo) but the PR should resolve this pain point permanently.

@riverar

Copy link
Copy Markdown
Collaborator

windows-reactor-setup is now available #4487. This lets developers pick the deployment model they need for their app. Feedback welcome!

@elibroftw

Elijah Lopez (elibroftw) commented Jun 3, 2026

Copy link
Copy Markdown

The gallery app runs, but when I try to run the demo code or some more complicated code I get the following errors.

Demo Code Error
S D:\repos\winui-reactor-playground> cargo run
   Compiling winui-reactor-playground v0.1.0 (D:\repos\winui-reactor-playground)
error[E0271]: expected `app` to return `Element`, but it returns `impl Into<Element>`                                                                       
   --> src\main.rs:4:39
    |
  4 |     App::new().title("sample").render(app)
    |                                ------ ^^^ expected `Element`, found opaque type
    |                                |
    |                                required by a bound introduced by this call
...
  7 | fn app(cx: &mut RenderCx) -> impl Into<Element> {
    |                              ------------------ the found opaque type
    |
    = note:     expected enum `windows_reactor::Element`
            found opaque type `impl Into<windows_reactor::Element>`
note: required by a bound in `windows_reactor::App::render`
   --> C:\Users\maste\.cargo\git\checkouts\windows-rs-a5de4a2dc783ec71\b9e91e0\crates\libs\reactor\src\app.rs:227:62
    |
225 |     pub fn render<F>(self, f: F) -> Result<()>
    |            ------ required by a bound in this associated function
226 |     where
227 |         F: Fn(&mut crate::core::render_context::RenderCx) -> crate::core::element::Element
    |                                                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `App::render`

For more information about this error, try `rustc --explain E0271`.                                                                                         
error: could not compile `winui-reactor-playground` (bin "winui-reactor-playground") due to 1 previous error     
error when running custom code
PS D:\repos\music-caster\migration-bootstrapper> cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.26s
     Running `target\debug\MigrationBootstrapper.exe`
windows_reactor: Application::Start failed: Error { code: HRESULT(0x80040154), message: "Class not registered" }
Error: Error { code: HRESULT(0x80040154), message: "Class not registered" }
error: process didn't exit successfully: `target\debug\MigrationBootstrapper.exe` (exit code: 1)

@kennykerr

Copy link
Copy Markdown
Collaborator Author

Elijah Lopez (@elibroftw) please share a minimal repro in a new issue and I'll be happy to take a look.

@gamersekofy

Copy link
Copy Markdown

Elijah Lopez (@elibroftw) regarding your demo code error, I ran into this exact issue as well when trying out the examples. The error is misleading at first glance because it looks like a generic type mismatch, but the fix is straightforward.

Kenny Kerr (@kennykerr)'s intro to windows-reactor post (first comment of this PR) and some docs show the render function signature as:

fn app(cx: &mut RenderCx) -> impl Into<Element> {

But if you check App::render() in the source, its bound is:

pub fn render<F>(self, f: F) -> Result<()>
where
    F: Fn(&mut RenderCx) -> Element + Send + 'static,

It requires a concrete Element, not impl Into<Element>. The impl Into<Element> return type is an opaque type that the compiler can't prove satisfies the bound, so it rejects the call.

To fix, just change the return type of your view function to Element and append .into() to your builder:

fn app(cx: &mut RenderCx) -> Element {
    // ...
    vstack((...))
        .spacing(8.0)
        .into()  // ← converts the builder into Element
}

You can see this pattern in the working sample apps in the repo, like the calendar_view example:

fn app(cx: &mut RenderCx) -> Element {
    let (count, set_count) = cx.use_state(0_u32);
    let bump = move || set_count.call(count + 1);

    vstack((
        calendar_view().today_highlighted(true).on_changed(bump),
        text_block(format!("Selection changed {count} time(s)")),
    ))
    .spacing(8.0)
    .into()
}

In short: always return Element + .into(), not impl Into<Element>.

@kennykerr

Copy link
Copy Markdown
Collaborator Author

Uzair Mohammed (@gamersekofy) good catch!

I've updated the PR description to match the latest changes in the master branch to avoid this confusion.

@paulo-assoc

Copy link
Copy Markdown

This is great, but I would prefer a signals-based approach to reactivity inspired by SolidJS, which allows fine-grained reactivity and avoids the need to compute differences. This avoids taking the same wild ride that react did from use_memo, use_callback to automatic memoization as pointed out by Tushar (@tusharsnx).

@styris-ame

Styris (styris-ame) commented Jun 21, 2026

Copy link
Copy Markdown

This looks awesome! Are there any plans to add full support for animations? Currently it looks like animations are quite limited, and the gallery example isn't even faithful to the C# WinUI 3 Gallery that has a page switching animation.

I also noticed that opening the gallery example starts with a black screen, then snaps to the mica background with the content. For a proper app, I think it's important that the app either waits for the content to render before making the window visible, or else at least open the window with Mica from the very start.

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.