Leptos vs Dioxus vs Sycamore (vs Svelte?): Part 1 — Syntax comparison
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 —
- 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 itbar
. - Reactivity — Reactivity is achieved via declaring variables directly in the
<script>
tag, which is neat. No special syntax likeuseState()
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 byon:deleteMe={deleteTodo}
heredeleteme
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 tokey=id
, and{title}
is equivalent totitle={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 ofuseMemo
in svelte because like states, functions are also marked during compilation so explicit memoization is not needed.
Leptos
- 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 aScope
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 hasget()
andset()
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, andview
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
- 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 butinline_props
macro allows for, well, inlining props. Here thersx!
macro is similar toview!
macro in leptos, defining the part to be rendered. - Reactivity — Similar to react, there is a
use_state
for scalar reactive values, anduse_ref
for vectors(not same as theuseRef
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
- 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 usinginline_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
andKeyed
components for unkeyed and keyed iterables respectively. The syntax forKeyed
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.