Skip to content
ExponaBLOG
RUST PROGRAMMING · DAY 046 / 100

Leptos, Yew and Dioxus: a practical guide for developers

Rust frontend frameworks.

Expona Admin7 min read

Leptos, Yew and Dioxus: a practical guide for developers

After spending years building UIs with JavaScript frameworks, many of us are asking whether Rust can offer a better path forward for web development. The short answer is yes—but the answer changes depending on your project's constraints. In this guide, we'll explore three production-ready Rust frontend frameworks and help you decide which one fits your team's needs.

Why Rust for the frontend?

Before diving into the frameworks themselves, let's establish why you'd consider Rust at all. JavaScript's flexibility is both its superpower and its vulnerability: runtime errors slip through to production, bundle sizes grow unchecked, and performance tuning becomes a guessing game. Rust gives us compile-time guarantees around type safety and memory management without a garbage collector, producing WebAssembly that's smaller and faster than equivalent JavaScript.

The trade-off is ecosystem maturity. The Rust web ecosystem is younger, which means fewer third-party libraries, less Stack Overflow guidance, and smaller communities. For many teams, this is a non-starter. For others—particularly those shipping performance-critical applications or willing to invest in building utilities in-house—it's worth the friction.

Leptos: fine-grained reactivity and SSR

Leptos is built around fine-grained reactivity, a concept borrowed from Solid.js. Instead of re-rendering entire component trees when state changes, Leptos updates only the DOM nodes that depend on that specific piece of state. This architectural choice produces exceptionally lean WebAssembly and predictable performance characteristics.

What makes Leptos distinct is its first-class support for server-side rendering (SSR). You write your components once and run them on the server to generate initial HTML, then hydrate that HTML on the client. This unlocks faster first contentful paint and better SEO—critical for public-facing applications.

Here's a practical example: a task list component that filters on the server and client:

rust
use leptos::*;

#[component]
fn TaskList() -> impl IntoView {
    let (tasks, set_tasks) = create_signal(vec![
        "Review pull requests".to_string(),
        "Deploy to staging".to_string(),
        "Update documentation".to_string(),
    ]);

    let (filter, set_filter) = create_signal(String::new());

    let filtered_tasks = move || {
        let search = filter.get().to_lowercase();
        tasks
            .get()
            .into_iter()
            .filter(|task| task.to_lowercase().contains(&search))
            .collect::>()
    };

    view! {
        <div>
            
            <ul>
                {task} }
                    }
                />
            </ul>
        </div>
    }
}

#[tokio::main]
async fn main() {
    mount_to_body(|| view! {  })
}

Notice the create_signal pattern: it returns both a getter and setter, and changes to that signal automatically propagate to any component that reads it. No context providers, no selector functions—just dependency tracking. On the server, this same code produces HTML; on the client, it hydrates and becomes interactive.

Where Leptos shines: applications where you control both client and server, can standardize on a single Rust codebase, and need predictable performance. Where it struggles: if you're integrating with existing JavaScript libraries or if your team lacks Rust experience, the learning curve is steep.

Yew: component model like React

If your team has shipped React applications, Yew will feel immediately familiar. It uses a component-based architecture with props, hooks, and local state management. The mental model maps directly to React, which means onboarding is faster and decision-making is less novel.

Yew doesn't have native SSR support in the same way Leptos does, but you can render components to HTML on the server using yew::ServerRenderer. The ecosystem around it—routers, state management libraries, UI component kits—is more mature than the alternatives.

Here's the same task list in Yew:

rust
use yew::prelude::*;
use web_sys::HtmlInputElement;
use gloo_utils::window;

#[function_component(TaskList)]
fn task_list() -> Html {
    let tasks = use_state(|| vec![
        "Review pull requests".to_string(),
        "Deploy to staging".to_string(),
        "Update documentation".to_string(),
    ]);

    let filter = use_state(String::new);
    let input_ref = use_node_ref();

    let on_input = {
        let filter = filter.clone();
        let input_ref = input_ref.clone();
        Callback::from(move |_| {
            if let Some(input) = input_ref.cast::() {
                filter.set(input.value());
            }
        })
    };

    let filtered: Vec = tasks
        .iter()
        .filter(|task| task.to_lowercase().contains(&filter.to_lowercase()))
        .cloned()
        .collect();

    html! {
        <div>
            
            <ul>
                {filtered.into_iter().map(|task| html! {
                    <li>{task}</li>
                }).collect::()}
            </ul>
        </div>
    }
}

fn main() {
    yew::Renderer::::new().render();
}

The patterns are recognizable: use_state for local state, callbacks for event handling, JSX-like syntax via macros. If you've debugged React race conditions or managed callback props, you'll recognize the tradeoffs here too.

Yew's strength is compatibility with existing Rust web infrastructure and community packages. It's a safe choice if your team wants Rust's safety guarantees without completely abandoning familiar patterns. The weakness is that it doesn't feel like Rust as much as React-in-Rust; you're not leveraging Rust's unique strengths around ownership and fine-grained control.

Dioxus: web, desktop, and mobile

Dioxus targets a broader ecosystem than Leptos or Yew. Write once, and you can ship to the web (as WebAssembly), desktop (using Tauri or native rendering), and mobile (iOS and Android via React Native semantics). This is powerful for teams building cross-platform applications where code reuse matters.

Dioxus combines fine-grained reactivity (like Leptos) with a React-like component model (like Yew). The signal system is ergonomic, and the framework handles the complexity of managing resources across different platforms.

A simple counter in Dioxus illustrates the signal-based API:

rust
use dioxus::prelude::*;

fn main() {
    dioxus_web::launch(app);
}

fn app() -> Element {
    let mut count = use_signal(|| 0);

    rsx! {
        div {
            h1 { "Counter: {count}" }
            button {
                onclick: move |_| count += 1,
                "Increment"
            }
            button {
                onclick: move |_| count -= 1,
                "Decrement"
            }
        }
    }
}

#[cfg(target_os = "macos")]
fn main() {
    dioxus_desktop::launch(app);
}

Notice how the same component code can be compiled for web or desktop by simply changing the main function. The use_signal hook returns a signal that you can read and write directly; no setter callback needed.

Dioxus excels when your business requirements span multiple platforms and your team wants to maximize shared logic. The downside is that it's still maturing—the desktop and mobile stories are solid but not as battle-tested as web frameworks in production at scale.

Comparing the three: practical considerations

Dimension Leptos Yew Dioxus
SSR support Native, integrated Manual implementation Experimental
Learning curve Steep (fine-grained reactivity) Gentle (React-like) Medium (signals + components)
Platform targets Web only Web only Web, desktop, mobile
Ecosystem maturity Growing Most mature Good, expanding
Bundle size (min+gzip) ~30-50 KB ~50-80 KB ~40-70 KB
Performance (TTFB) Excellent with SSR Good Good

Choose Leptos if you're building a server-rendered application and can commit your full stack to Rust. Choose Yew if your team knows React and wants incremental adoption of Rust on the frontend. Choose Dioxus if cross-platform shipping is a core requirement.

Common mistakes

  • Underestimating the ecosystem gap. You'll hit moments where a JavaScript library has ten solutions and Rust has zero. Plan for building utilities in-house or wrapping JavaScript via wasm-bindgen.
  • Treating Rust web dev like JavaScript web dev. The performance profile is different, the debugging experience is different, and the error messages—while helpful—require Rust literacy. Don't expect the same iteration speed initially.
  • Ignoring hydration complexity with SSR frameworks. Leptos's SSR is powerful, but mismatches between server and client rendering are subtle bugs. Test this path thoroughly before shipping.
  • Over-engineering signal dependencies. Fine-grained reactivity is elegant but can lead to premature optimization. Start simple, profile, then optimize specific hot paths.
  • Assuming one framework fits all projects. A real-time collaborative editor has different needs than a content site. Use the framework that fits your constraints, not the one you're most curious about.

The ecosystem reality

All three frameworks produce excellent WebAssembly. The runtime performance is often comparable. The real differences lie in developer experience, ecosystem maturity, and target use cases. If you need logging, testing utilities, or form handling, you'll find mature libraries in JavaScript but will likely assemble solutions from smaller crates in Rust.

This isn't a flaw—it's a tradeoff. Smaller ecosystems force intentional choices and often result in cleaner, more focused libraries. But it requires a team comfortable with evaluating and sometimes maintaining solutions themselves.

Key takeaways

  • Leptos's fine-grained reactivity and integrated SSR make it ideal for full-stack Rust applications where performance and SEO matter.
  • Yew's React-like component model offers the gentlest learning curve for teams migrating from JavaScript to Rust.
  • Dioxus's cross-platform approach lets you share logic across web, desktop, and mobile with a single codebase.
  • All three produce smaller bundles and faster initial loads than equivalent JavaScript applications, but the ecosystem is still younger and requires more in-house development.
  • Choose based on your constraints—team expertise, platform targets, SSR requirements—rather than framework popularity.
  • Start with a small, non-critical feature to evaluate the fit before committing a major project to Rust frontend development.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles