Written by Eze Sunday
✏️
JavaScript remains the dominant programming language for the web, with several amazing frontend frameworks like React, Vue, Next.js, and more. However, in recent times, the Rust programming language has been gaining a lot of buzz for its performance and safety features.
Lots of new frontend frameworks have been built on top of Rust, including Leptos, which happens to be one of the most popular ones. In this guide, we'll highlight why and how to migrate your JavaScript frontend to use the Leptos Rust frontend framework.
Let's get into it.
What is Leptos?
Leptos is a relatively new Rust frontend framework for building interactive UI interfaces. It leverages signals and fine-grained reactivity with no virtual DOM to build complex systems at scale. It also supports server-side rendering and client-side rendering.
Recent performance metrics have shown that Leptos performs better than most popular JavaScript frameworks: Not only do Leptos apps perform well, but you can build aesthetically pleasing UIs with Leptos as well.
What are the benefits of migrating from JavaScript to Rust Leptos?
Migrating your frontend to a new language and framework is a complex process so, it’s imperative that you have a good reason to take the leap. So, let’s highlight some of the reasons why you might want to migrate your JavaScript frontend to the Leptos Rust frontend framework.
Performance
Leptos applications are built with Rust and compile to WebAssembly (Wasm). Rust and Wasm are known for the their enhanced performance compared to JavaScript. So, migrating a JavaScript frontend to Rust means you are most likely to boost the performance of your application.
Memory safety
JavaScript is not memory-safe. In other words, it doesn’t have any features that prevent you from accidentally or intentionally introducing memory related bugs or vulnerabilities — for example, accessing memory that has been freed or using memory that is outside the bounds of an array.
This lack of memory safety can lead to your application crashing on your users frequently.
One of Rust’s selling points is its guarantee of memory safety without sacrificing performance. Rust solves this problem with its ownership and borrowing feature, so your chances of introducing memory related bugs into your application becomes very slim or in-existence.
Concurrency
In today’s day and time, where we have computers with multiple cores, concurrency is an important concept and paradigm for building fast and scalable software. Interestingly, Rust has first-class support for concurrent programming.
Meanwhile, JavaScript is a single-threaded programming language. Even though a lot of work has gone in to allow JavaScript to support concurrency through various mechanisms such as asynchronous programming and web workers, JavaScript still doesn’t guarantee concurrency.
Strong typing
JavaScript being dynamically typed means the type of a variable is determined at runtime. This makes your JavaScript code prone to type-related errors that might not be caught until runtime.
On the other hand, Rust is statically typed, which means the types are checked at compile-time. This early detection of type errors eliminates or reduces the chances of runtime errors and improves code reliability.
Technical considerations for a JavaScript to Rust migration
While migrating from JavaScript to the Leptos Rust frontend framework sounds like a good idea with all the benefits, there are still some technical requirements for you and your team to be aware of before diving in. Let’s discuss them.
Learning curve
It’s important to know that Rust has a steeper learning curve compare to JavaScript. Your team needs a solid knowledge of the language fundamentals before proceeding with your migration.
While Leptos abstracts away some complexities, a solid understanding of core Rust concepts will make your migration a whole lot fun and will save you time.
Library support
Leptos supports a good number of popular libraries like Tailwind and its related CSS libraries. Even so, there’s a chance that you might not be able to use some of the libraries you’re currently using in your project and might have to look for their Rust alternatives.
So, be sure to double-check your critical libraries and be sure their alternatives exist in the Rust ecosystem. There’s a good chance the crates you need are available in Rust's crates.io repository.
Now that we know exactly what we’re getting involved in, let’s get started with the actual migration process.
Demonstrating a JavaScript to Rust Leptos migration process
First things first, you’ll need to know the complexity, the features, and literally everything else about your existing app. Make a note of all of it, including all the available components. Nothing should be left out, as you might need to map functionalities to their Leptos implementations.
Once you have that clarity, go ahead and start setting up Leptos. All you need is to have Rust installed. From then onward, the Leptos documentation will be your best friend. There is also a video on YouTube to get you started with Leptos that you should check out.
Set up Rust and Leptos
Rust and Leptos are both easy to set up. First, install Rust with Rustup by running the command below:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
This will install everything you need to start working with Rust, including Cargo, the package manager for Rust.
Next, install Trunk and create a Rust project and by running the code below:
cargo install trunk && cargo init leptos-project
Note that Leptos uses Trunk to serve the client side application. Trunk is a zero-config Wasm web application bundler for Rust.
Now, install Leptos as a dependency in your Rust project:
cargo add leptos --features=csr
I am adding the csr
features flag so that Cargo knows to install only the necessary features for building a client-side application with Leptos. This will enable us to leave out any features that are specific to the server-side functionality.
For illustration purposes, we’ll use bits and pieces of a React application I created as a case study throughout the rest of this guide. Right now, you have a basic Rust application with Leptos installed. To follow along, clone the React Todo project by running the command below:
git clone https://github.com/ezesundayeze/react-todo-sample-article
Install the dependencies and run the application by running the code below:
npm install && npm run dev
The output should look like this:
Main Leptos app entry point
Just like in React and most other frontend applications, Leptos has an entry point. You need to start by creating one for your project.
Let’s start by converting the React app entry point to Leptos entry point. Here is the entry point for the React app on GitHub:
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
And this is the equivalent in Leptos:
use leptos::*;
fn main() {
leptos::mount_to_body(App);
}
Secondly, to complete the main entry setup, create an index.html
file in the root of your Leptos project. This file will serve as the root of the RSX. Add the following content to it:
<!DOCTYPE html>
<html>
<head></head>
<body></body>
</html>
Now, let’s make a simple “Hello World” app to confirm our setup:
use leptos::*;
#[component]
fn App() -> impl IntoView {
view!{
<p>{"Hello World!"}</p>
}
}
fn main() {
leptos::mount_to_body(App);
}
Then go ahead and test your setup by running the command below in your terminal:
trunk serve --open
The output should look the the one below:
Migrating your JSX components to Leptos templates
While React uses JSX to create component structures, Leptos uses its own unique template syntax called RSX that follows the same rules as HTML, but is written in Rust and type-safe.
Let’s look at a simple example of how a component signature works in Leptos. Below is a simple React base Button
component that receives text
as a prop:
// button.jsx
function Button({ text }) {
return (
<button>{text}</button>
);
}
And here is the equivalent in the Rust Leptos framework:
// button.rs
#[component]
fn Button(text: Text) -> impl IntoView {
view!{
<button >{text}</button>
}
}
In the React JSX Button
component example above, we just have to return the JSX to the function.
Meanwhile, in the Leptos example, we need to do more than that. We must define the returned type and also return a view!
macro, which is of type impl IntoView
. Additionally, the function must be decorated with the #[component]
annotation.
More so, just like in React, props are passed as function arguments. The component that receives the props receives them as HTML attributes as well. So, that’s a common feature for both Leptos and React.
Essentially, a component that receives props will be like the one below:
view!{
<TodoList todos={todos} />
}
The code above contains a TodoList
component that accepts a list of to-dos as props. Following this signature, it woudn’t be too big of a deal to migrate basic components. However, some will require a state management implementation. So, let’s explore how state management works in the Leptos Rust frontend framework.
Addressing state management in Leptos
State management is the process of handling and modifying data that affects the component's rendering. Leptos has a robust statement management system that powers its reactivity with signals.
Signals in Leptos are the center of reactivity. When a signal's value changes, it signals all its subscribers, updating their corresponding components.
The way state management works in Rust is different, but some of the syntax may be similar to those of popular JavaScript frameworks, including React. So, it will likely not be too cumbersome to migrate your state, especially for small projects.
Let’s return to our linked GitHub project to see a practical example of how to address state management when migrating from React to Leptos. Here is the React version with the useState
Hook:
import React, { useState } from "react";
import TodoList from "./TodoList";
import "./App.css";
function App() {
const [tasks, setTasks] = useState([]);
return (
<div className="App">
<h1>Todo List</h1>
<TodoList
tasks={tasks}
/>
</div>
);
}
export default App;
The equivalent in Leptos will look like the below:
#[component]
fn App() -> impl IntoView {
let todos = create_signal(vec![]);
view! {
<div class="todo-app">
<h1>"Todo App"</h1>
<TodoList todos={todos} />
</div>
}
}
Routing in React vs. Leptos
The Leptos team has designed parts of the framework to resemble what developers are used to seeing in other frameworks. As a result, the routing mechanism for Leptos looks quite similar to that of React Router, as you can see in the code below:
view! {
<Routes>
<Route path="/" view=Home/>
<Route path="/todos" view=todos/>
<Route path="/todos/:id" view=singleTodo/>
<Route path="/*any" view=|| view! { <h1>"Not Found"</h1> }/>
</Routes>
}
I find it very interesting to work with the Leptos Rust frontend framework because in most cases, a lot of the details are abstracted so you can just focus on writing your application logic. It also looks familiar to other frameworks I am used to, which helps reduce the learning curve.
You can learn more about Leptos routing in the documentation.
Reimplementing component logic
There is no doubt that a huge part of the migration process will involve reimplementing your component logic. While some UI-related parts might seem similar, the core logic will most likely be more Rust-y than the template-related code, which is where your knowledge of Rust will shine.
For example, Leptos uses the concept of signals to handle its state. This means you won’t need an external state management tool, as we’ve already seen previously.
Here is an example of Leptos signal initialization:
let todos = create_signal(vec![]);
While here is how the same thing works in React:
const [tasks, setTasks] = useState([]);
In our linked project, the implementation for deleting a task looks like this:
const deleteTask = (taskId) => {
setTasks(tasks.filter((task) => task.id !== taskId));
};
A similar functionality, but in Leptos, looks like the code below:
let delete_task = create_action(cx, |task_id: u32| {
set_tasks.update(|tasks| tasks.retain(|task| task.id != task_id))
});
Likewise, you’ll have to map out and re-implement the logic for each functionality.
Conclusion
Migrating your JavaScript frontend to Rust can be a tough and yet interesting challenge. In this guide, we covered some considerations and steps to help you migrate your frontend from Javascript UI frameworks and libraries like React to the Leptos Rust frontend framework.
I hope you found this tutorial helpful. To be honest, to have an even more successful migration experience, you should consider going through the Leptos documentation and follow along with the our previous tutorial on Leptos, which shows in more detail how to work with Leptos.
Later in your migration process, you should ensure you write tests for your Leptos components using the Leptos testing tool. Also, write your business logic tests with Rust to ensure that your migration is working as expected.
Happy hacking!
LogRocket: Full visibility into web frontends for Rust apps
Debugging Rust applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking the performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Rust application. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.
Modernize how you debug your Rust apps — start monitoring for free.
Top comments (1)
Wow this is amazing. It reminds me to yew.rs this is another rust gem!!