{"id":11672,"date":"2025-08-23T09:44:10","date_gmt":"2025-08-23T09:44:10","guid":{"rendered":"https:\/\/putridparrot.com\/blog\/?p=11672"},"modified":"2025-08-23T09:44:10","modified_gmt":"2025-08-23T09:44:10","slug":"wasm-with-rust-and-leptos","status":"publish","type":"post","link":"https:\/\/putridparrot.com\/blog\/wasm-with-rust-and-leptos\/","title":{"rendered":"WASM with Rust (and Leptos)"},"content":{"rendered":"<p>I&#8217;m going to be going through some of the steps from <a href=\"https:\/\/book.leptos.dev\/getting_started\/\" target=\"_blank\">Leptos Getting Started<\/a>. Hopefully we&#8217;ll be able to add something here.<\/p>\n<p><strong>Prerequisites<\/strong><\/p>\n<p>We&#8217;re going to use Trunk to run our application, so first off we need to make sure we&#8217;ve installed it<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\ncargo install trunk\r\n<\/pre>\n<p>Trunk allows us to build our code, run a server up and run our WASM application, moreover it&#8217;s watching for changes and so will rebuild and redeploy things as you go. Web style development with a compiled language. We&#8217;ll cover more on trunk later.<\/p>\n<p><strong>Creating our project<\/strong><\/p>\n<p>Create yourself a folder for your application and run the terminal in that folder.<\/p>\n<p>We need to create our project &#8211; although I use RustRover from JetBrains, let&#8217;s go &#8220;old school&#8221; as use cargo to create our project etc. <\/p>\n<p>Run the following (obviously change <em>wasm_app<\/em> to something more meaningful for your app name<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\ncargo new wasm_app --bin\r\n<\/pre>\n<p>cd into the application (as above, mine&#8217;s named wasm_app).<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\ncargo add leptos --features=csr\r\n<\/pre>\n<p>We&#8217;ll need to add the WASN target<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\nrustup target add wasm32-unknown-unknown\r\n<\/pre>\n<p>in the root folder (where your Cargo.toml file is) create an index.html with the following<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\n&lt;!DOCTYPE html&gt;\r\n&lt;html&gt;\r\n  &lt;head&gt;&lt;\/head&gt;\r\n  &lt;body&gt;&lt;\/body&gt;\r\n&lt;\/html&gt;\r\n<\/pre>\n<p>Next create a cd into the src folder and edit the main.rs &#8211; it should look like the following<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\nuse leptos::prelude::*;\r\n\r\nfn main() {\r\n    leptos::mount::mount_to_body(|| view! { &lt;p&gt;&quot;Hello, world!&quot;&lt;\/p&gt; })\r\n}\r\n<\/pre>\n<p>The <em>mount_to_body<\/em>, as the name suggests, essentially injects your WASM code into the &lt;body&gt;&lt;\/body&gt; element.<\/p>\n<p>Now, from the root folder (i.e. where index.html is) run<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\ntrunk serve --open\r\n<\/pre>\n<p>If the default port (8080) is already in use we can specify the port using<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\ntrunk serve --open --port 8081\r\n<\/pre>\n<p>If all went well then you see your default browser showing the web page, if not then open a browser window and navigate to http:\/\/localhost:8081\/<\/p>\n<p>To save having to set the port via the CLI, you can also create a trunk.toml file in the root folder with something like this in it<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\n&#x5B;serve]\r\naddress = &quot;127.0.0.1&quot;\r\nport = 8081\r\n<\/pre>\n<p><strong>Taking it a bit further<\/strong><\/p>\n<p>We&#8217;ve got ourselves a really simple WASM page. <\/p>\n<p>Let&#8217;s move this a little further by creating a component for our application.<\/p>\n<p>Create a file in src named app.rs and we&#8217;ll add the following<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\nuse leptos::prelude::*;\r\n\r\n#&#x5B;component]\r\npub fn App() -&gt; impl IntoView {\r\n    view! {\r\n        &lt;p&gt;&quot;Hello, world!&quot;&lt;\/p&gt;\r\n    }\r\n}\r\n<\/pre>\n<p>and change the main.rs to this<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\nmod app;\r\n\r\nuse leptos::mount::mount_to_body;\r\nuse crate::app::App;\r\n\r\nfn main() {\r\n    mount_to_body(App);\r\n}\r\n<\/pre>\n<p>If you kept trunk running it will automatically rebuild the code and refresh the browser.<\/p>\n<p><strong>Components<\/strong><\/p>\n<p>The component (below) returns HTML via the view macro and as you can see, this returns a trait IntoView. As you can see we mark the function as a #[component]<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\n#&#x5B;component]\r\npub fn App() -&gt; impl IntoView {\r\n    view! {\r\n        &lt;p&gt;&quot;Hello, world!&quot;&lt;\/p&gt;\r\n    }\r\n}\r\n<\/pre>\n<p><em>Note: you might want to change your text to prove to yourself that the did indeed get updated in the web page when you saved it &#8211; assuming trunk was running.<\/em><\/p>\n<p>This is a pretty simple starting point, so let&#8217;s add some more bits to this&#8230;<\/p>\n<p>Let&#8217;s change the App function to this<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\nuse leptos::prelude::*;\r\n \r\n#&#x5B;component]\r\npub fn App() -&gt; impl IntoView {\r\n    let (count, set_count) = signal(0);\r\n\r\n    view! {\r\n\r\n        &lt;button on:click=move |_| set_count.set(count.get() + 1)&gt;Up&lt;\/button&gt;\r\n        &lt;div&gt;{count}&lt;\/div&gt;\r\n        &lt;button on:click=move |_| set_count.set(count.get() - 1)&gt;Down&lt;\/button&gt;\r\n    }\r\n}\r\n<\/pre>\n<p>The signal (which is a reactive variable) may remind you of something like useState in React, we deconstruct the signal (which has the default of 0) into a count (getter) and a set_count (setter). To be honest the set and get functions seem odd if you&#8217;re using the properties such as C#, but that&#8217;s the way it is is Rust.<\/p>\n<p>The count value is of type ReadSignal and set_count is of type WriteSignal.<\/p>\n<p>We can also set the value of count using the following within the closure. Ultimately this should be a more performant way of doing things. The example above might be preferred for readability (although that&#8217;s debatable) &#8211; it does however look more inline with the way we get values etc. I&#8217;ll leave others to debate the pros and cons, for me the line below is effecient.<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\n*set_count.write() += 1\r\n<\/pre>\n<p><strong>Routing<\/strong><\/p>\n<p>Let&#8217;s rename the app.rs file to counter.rs (also rename the function to Counter) and create a new app.rs file which will acts as the router to our components. We&#8217;ll need to add this to the Cargo.toml dependencies<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\nleptos_router = &quot;0.8.5&quot;\r\n<\/pre>\n<p>and in the app.rs paste the following code<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\nuse leptos::prelude::*;\r\nuse leptos_router::{\r\n    components::{Route, Router, Routes},\r\n    StaticSegment,\r\n};\r\nuse crate::counter::Counter;\r\nuse crate::home::Home;\r\n\r\n#&#x5B;component]\r\npub fn App() -&gt; impl IntoView {\r\n    view! {\r\n        &lt;Router&gt;\r\n            &lt;Routes fallback=|| &quot;Page not found.&quot;&gt;\r\n                &lt;Route path=StaticSegment(&quot;&quot;) view=Home \/&gt;\r\n                &lt;Route path=StaticSegment(&quot;counter&quot;) view=Counter \/&gt;\r\n            &lt;\/Routes&gt;\r\n        &lt;\/Router&gt;\r\n    }\r\n}\r\n<\/pre>\n<p>You&#8217;ll need to add the mod to the main.rs file to include the home reference.<\/p>\n<p>I&#8217;ve also added a home.rs file with the following<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\nuse leptos::prelude::*;\r\n\r\n#&#x5B;component]\r\npub fn Home() -&gt; impl IntoView {\r\n    view! {\r\n        &lt;p&gt;Welcome to your new app!&lt;\/p&gt;\r\n    }\r\n}\r\n<\/pre>\n<p>As you can see, the router routes \/ to our Home component and the \/counter to our Counter component.<\/p>\n<p><strong>Meta data from code<\/strong><\/p>\n<p>Whilst we have an index.html which you can edit, we might want to supply some of the meta data etc. via the app&#8217;s code. <\/p>\n<p>Add the following to Cargo.toml dependencies<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\nleptos_meta = &quot;0.8.5&quot;\r\n<\/pre>\n<p>Now in main.rs add<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\nuse leptos::view;\r\nuse leptos_meta::*;\r\n<\/pre>\n<p>and now change the code to <\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\nfn main() {\r\n    mount_to_body(|| {\r\n        provide_meta_context();\r\n        view! { \r\n            &lt;Title text=&quot;Welcome to My App&quot; \/&gt;\r\n            &lt;Meta name=&quot;description&quot; content=&quot;This is my app.&quot; \/&gt;\r\n            &lt;App \/&gt;\r\n        }\r\n    });\r\n}<\/pre>\n<p>The provide_meta_context() function allows us to inject metadata such as &lt;title&gt;, &lt;meta&gt; and &lt;script&gt;<\/p>\n<p><strong>Code<\/strong><\/p>\n<p>Code for this post is available on <a href=\"https:\/\/github.com\/putridparrot\/blog-projects\/tree\/master\/wasm_leptos\/wasm_app\" target=\"_blank\">GitHub<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>I&#8217;m going to be going through some of the steps from Leptos Getting Started. Hopefully we&#8217;ll be able to add something here. Prerequisites We&#8217;re going to use Trunk to run our application, so first off we need to make sure we&#8217;ve installed it cargo install trunk Trunk allows us to build our code, run a [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[191,748],"tags":[],"class_list":["post-11672","post","type-post","status-publish","format-standard","hentry","category-rust","category-wasm"],"jetpack_sharing_enabled":true,"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/posts\/11672","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/comments?post=11672"}],"version-history":[{"count":5,"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/posts\/11672\/revisions"}],"predecessor-version":[{"id":11695,"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/posts\/11672\/revisions\/11695"}],"wp:attachment":[{"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/media?parent=11672"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/categories?post=11672"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/tags?post=11672"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}