Leptos vs Dioxus vs Sycamore (vs Svelte?): Part 1 — Syntax comparison

Vedant Pandey
9 min readJul 21, 2023

Introduction

From quite sometime I have been wanting to try out the different WASM frameworks and compare the experience of working with them compared to common front end frameworks. I tried using Svelte as a reference point because it was new enough for me to experiment but still a js framework.

All the code referred in this blog can be found in this repo.

What is compared?

The app is a simple todo app, I’ve tried to keep my experiment relatively small with the core functionalities

  • Reactivity, specifically states— What one would refer as useState in react
  • Children components
  • Child to parent communication
  • Keyed iterated rendering
  • Add/deleting elements from rendered list
  • Props
  • Tailwind integration — I just wanted to use tailwind, because it simplifies styling for me.
  • Conditional rendering/attributes
  • Event handlers

These might seem pretty basic for many people but given here we are comparing Rust generated WASM, and many concepts, especially lifetimes, can be not very intuitively mapped at first, alongside the fact that most of these libraries are not in stable release.

Additionally, there are a lot of things that aren’t here yet but I would definitely like to incorporate in future, like passing components as props, context providers, side-effects, borrowed props, and many more. Feel free to drop any suggestions, or if you think certain things can be done more idiomatically.

Let’s get started.

Svelte

Parent component —

+page.svelte
Todo.svelte
  • First thoughts — Right from start svelte looks a lot like plain HTML, going distinctly away from JSX style frameworks, like React, and Solidjs.
  • Components — Files are there own components, there is no concept of components as exported functions, you can import a component from Foo.svelte and call it bar.
  • Reactivity — Reactivity is achieved via declaring variables directly in the <script> tag, which is neat. No special syntax like useState() is needed here, this doesn’t mean that everything in script tag becomes a state, instead, svelte determines this by marking the variables which are interpolated JSX style and making only them as part of states.
  • Child to parent communication — Svelte provides a createEventDispatcher to send events from child to parent(important thing to note here is that these events do not bubble beyond one level). It is bound to a handler by on:deleteMe={deleteTodo} here deleteme is the event name it can be anything, but has to be same in both child dispatch and parent bind.
  • Rendering in loop & props — For iteration svelte provides its own syntax, here we are destructuring a todoList item as { title, id } and the (id) is equivalent to key=id, and {title} is equivalent to title={title} in JSX.
{#each todoList as { title, id } (id)}
<Todo on:deleteMe={deleteTodo} {title} {id} />
{/each}
  • Updating iterables — Updating items in iterables can be done, either via the spread operator like JSX, or by just reassigning. I’m unsure of how svelte handles this internally, but in my guess one benefit of reassigning over spread and append would be the fact that if the internal array holding the state value has the capacity for then it won’t be reassigned instead only the reference should be updated, on the other hand if we do a spread first, then even if the array has the capacity, all of its values would be copied once before updating the state reference, which could potentially create it once again. V8 has great optimizations, but in my knowledge, this is not something that can easily be tackled and also the reason why libraries like ImmerJs exist.
  • Tailwind integration — Being a js framework, tailwind support is out of box and nothing special is really required to be done here
  • Event handlers — Similar to states, handlers are also defined in script tag, there is no need of useMemo in svelte because like states, functions are also marked during compilation so explicit memoization is not needed.

Leptos

Leptos syntax
  • First thoughts — Leptos tries to stay syntactically close to JSX, most things are intuitive, it is the only one which creates HTML elements in HTML-like syntax, sycamore and dioxus have their own flavour for HTML syntax.
  • Conditional attributes — To conditionally set attributes, we have to redeclare attributes(L132–134) which apparently appends to the existing class string, I was opinionated against at first, but looking at the same problem in POV of rest of frameworks, this seems to be the best approach.
  • Components — Components are defined using the component macro, and each component should have a Scope parameter, defining the reactive scope of the component.
  • Reactivity — Leptos uses create_signal to define reactive states(L45–47). One thing to note here is that I’m using nightly version(as recommended by leptos) for a terse syntax, but the stable build as of now has get() and set() methods.
  • Child to parent communication — One thing which is useful in rust is the fact that we can pass down closure to any functions, and that by itself might not be as impressive as the fact that one can pass a closure not just as reference but also as value, using the move keyword. This way makes code look more intuitive while giving more fine tuned control to the developer, along with lifetimes.
  • Rendering in loop & props — Leptos provides a nice For component to allow iterations along with (optional) key attribute, each takes in an iterable, and view defines how each iterable item should be rendered.
  • Updating iterables — Vector reactive states are not differentiated from scalar ones, which makes handling really easy, here we take a vector as a reactive state (L47) so just updating the underlying vector updates the component.
  • Tailwind integration — Leptos uses trunk(for the frontend version, although it does have full stack version which I haven’t tried yet) which allows user to create a Trunk.toml where one can define pre-build hooks, for tailwind this is done as below.
[[hooks]]
stage = "pre_build"
command = "sh"
command_arguments = ["-c", "npx tailwindcss -i input.css -o style/output.css"]
  • Event Handler —Simple closures can be used everywhere it seems, making the whole code look a lot like rust shade of jsx, which makes things really intuitive, below is one such example.
<input
...
on:input=move |ev| {
set_title(event_target_value(&ev))
}
...
prop:value=title />
...

Dioxus

Dioxus syntax
  • First thoughts — Dioxus uses dictionary like syntax with everything within curly braces. It took me a long time to understand that attributes require a colon after their name, while components must not have that, along with the fact that all children must be declared strictly after the attributes(although this is sensible) but the unclear errors along with the fact that docs aren’t mature meant I had to go through the codebase for a lot of things. But it would be unfair to just dismiss saying the above statement without mentioning the fact that dioxus is really performant(it uses the sledgehammer internally), and it has the scope of not just webapps but also desktop, mobile, and cli apps, which makes it a lot of work to keep up the docs, although syntax is something I personally don’t like. Also there is a bug which I wasn’t able to fix or even debug, when first add is done without deleting a todo first, the wasm code seems to fail, and nothing works after that. I found no meaningful errors, but my guess is that it has something to do with keys since post deleting any existing todo the bug never happens, but no clue on it, this issue didn’t happen with other frameworks.
  • Conditional attributes — I couldn’t find anything for conditional attributes so I ended up using normal rust syntax, and it isn’t exactly great. Unlike leptos we can’t have some conditionals with some fixed classes, instead, the whole classes list should be in both conditionals(L57–60). This can be a nightmare for people using a lot of tailwind. ¯\_(ツ)_/¯
  • Components — Similar to leptos, component functions take a reactive scope but don’t require a component like macro, by default props should be defined explicitly but inline_props macro allows for, well, inlining props. Here the rsx! macro is similar to view! macro in leptos, defining the part to be rendered.
  • Reactivity — Similar to react, there is a use_state for scalar reactive values, and use_ref for vectors(not same as the useRef in react). The second parameter takes a closure instead of a value.
  • Child to parent communication — Same as before we can pass closures to the children, for them to call, using any parameters held by them.
  • Rendering in loop & props —We just need plain rust syntax to do so, which is neat, and key attribute can be added which dioxus will identify and use.
  • Updating iterables — We need to use a write() reference of states(or refs in this case), in which any update made is updated to dom. One point to note is the default syntax of leptos is also same, if one is not using the nightly toolchain of rust.
  • Tailwind integration — We don’t have a special toml file since dioxus uses its own cli tool, but the same can be achieved using build.rs file which is essentially a pre-build script for any rust binary.
  • Event Handler — Same as before, using closures, the only difference is that I found, the syntax is more like html attributes(oninput) and not directive style(on:input), but I don’t think I personally prefer one over the other.

Sycamore

Sycamore syntax
  • First thoughts — The syntax sycamore follows is again unlike html, but more like a function style syntax, with parameters to set attributes, which does a better job of separating components from attributes than dioxus. To do more programmatic stuff you have to wrap everything in parentheses which makes html syntax look more like lisp. Other than that the syntax is really close to leptos, but the docs are still young.
  • Conditional attributes— We can use conditionals wrapped in parantheses, but like dioxus it requires whole string to be passed instead of something like interpolation, or leptos’ repeated attributes.
  • Components — Following leptos style component macros for defining a function as component and dioxus style props syntax which can be inlined using inline_props macro.
  • Reactivity — Everything is used via signals, both scalars and vectors. One neat thing is that two way binding can be achieved via bind directive instead of requiring to set both the value and the event handler.
  • Child to parent communication — Like rest it is done via passing closures, although for some reason I wasn’t able to get it using 'static lifetime and had to use explicit lifetimes.
  • Rendering in loop & props — Sycamore also has its own For and Keyed components for unkeyed and keyed iterables respectively. The syntax for Keyed is close to Leptos’ For syntax.
  • Updating iterables — One thing I didn’t like was that there wasn’t a smoother way to update vector states like previous 2 frameworks. We first need to create a copy and then update the original state via the copy.
  • Tailwind integration — Sycamore also uses trunk so the integration is straight forward and similar to leptos.
  • Event Handler — Same as leptos.

Final words

Each framework has there own opinions and many might prefer non-jsx style syntax over jsx-style, it has no objectivity other than the fact that a large part of web dev community lives in react, and they might find it more intuitive to switch to leptos over others but that can vary a lot.

I have another draft waiting which compares the performance of these frameworks in depth, I didn’t include it in this blog because it was already getting too big. Subscribe to stay updated for the next part.

--

--