search

Communicating with the server

dioxus-fullstack provides server functions that allow you to call an automatically generated API on the server from the client as if it were a local function.

To make a server function, simply add the #[server(YourUniqueType)] attribute to a function. The function must:

  • Be an async function
  • Have arguments and a return type that both implement serialize and deserialize (with serde).
  • Return a Result with an error type of ServerFnError

You must call register on the type you passed into the server macro in your main function before starting your server to tell Dioxus about the server function.

Let's continue building on the app we made in the getting started guide. We will add a server function to our app that allows us to double the count on the server.

First, add serde as a dependency:

cargo add serde

Next, add the server function to your main.rs:

#![allow(non_snake_case)]

use dioxus::prelude::*;

fn main() {
    launch(App)
}

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

    rsx! {
        h1 { "High-Five counter: {count}" }
        button { onclick: move |_| count += 1, "Up high!" }
        button { onclick: move |_| count -= 1, "Down low!" }
        button {
            onclick: move |_| {
                async move {
                    if let Ok(new_count) = double_server(count()).await {
                        count.set(new_count);
                    }
                }
            },
            "Double"
        }
    }
}

#[server]
async fn double_server(number: i32) -> Result<i32, ServerFnError> {
    // Perform some expensive computation or access a database on the server
    tokio::time::sleep(std::time::Duration::from_secs(1)).await;
    let result = number * 2;
    println!("server calculated {result}");
    Ok(result)
}

Now, build your client-side bundle with dx build --features web and run your server with cargo run --features ssr. You should see a new button that multiplies the count by 2.

Cached data fetching

One common use case for server functions is fetching data from the server:

#![allow(non_snake_case, unused)]

use dioxus::prelude::*;

fn main() {
    launch(app)
}

fn app() -> Element {
    let mut count = use_resource(get_server_data);

    rsx! {"server data is {count.value():?}"}
}

#[server]
async fn get_server_data() -> Result<String, ServerFnError> {
    // Access a database
    tokio::time::sleep(std::time::Duration::from_millis(100)).await;
    Ok("Hello from the server!".to_string())
}

If you navigate to the site above, you will first see server data is None, then after the WASM has loaded and the request to the server has finished, you will see server data is Some(Ok("Hello from the server!")).

This approach works, but it can be slow. Instead of waiting for the client to load and send a request to the server, what if we could get all of the data we needed for the page on the server and send it down to the client with the initial HTML page?

This is exactly what the use_server_future hook allows us to do! use_server_future is similar to the use_resource hook, but it allows you to wait for a future on the server and send the result of the future down to the client.

Let's change our data fetching to use use_server_future:

#![allow(non_snake_case, unused)]

use dioxus::prelude::*;

fn main() {
    launch(app);
}

fn app() -> Element {
    let mut count = use_server_future(get_server_data)?;

    rsx! {"server data is {count.value():?}"}
}

#[server]
async fn get_server_data() -> Result<String, ServerFnError> {
    // Access a database
    tokio::time::sleep(std::time::Duration::from_millis(100)).await;
    Ok("Hello from the server!".to_string())
}

Notice the ? after use_server_future. This is what tells Dioxus fullstack to wait for the future to resolve before continuing rendering. If you want to not wait for a specific future, you can just remove the ? and deal with the Option manually.

Now when you load the page, you should see server data is Ok("Hello from the server!"). No need to wait for the WASM to load or wait for the request to finish!

Running the client with dioxus-desktop

The project presented so far makes a web browser interact with the server, but it is also possible to make a desktop program interact with the server in a similar fashion. (The full example code is available in the Dioxus repo)

First, we need to make two binary targets, one for the desktop program (the client.rs file), one for the server (the server.rs file). The client app and the server functions are written in a shared lib.rs file.

The desktop and server targets have slightly different build configuration to enable additional dependencies or features. The Cargo.toml in the full example has more information, but the main points are:

  • the client.rs has to be run with the desktop feature, so that the optional dioxus-desktop dependency is included
  • the server.rs has to be run with the ssr features; this will generate the server part of the server functions and will run our backend server.

Once you create your project, you can run the server executable with:

cargo run --bin server --features ssr

and the client desktop executable with:

cargo run --bin client --features desktop

Client code

The client file is pretty straightforward. You only need to set the server url in the client code, so it knows where to send the network requests. Then, dioxus_desktop launches the app.

For development, the example project runs the server on localhost:8080. Before you release remember to update the url to your production url.

Server code

In the server code, first you have to set the network address and port where the server will listen to.

let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
    .await
    .unwrap();
println!("listening on http://127.0.0.1:3000");

Then, you have to register the types declared in the server function macros into the server. For example, consider this server function:

#[server(GetServerData)]
async fn get_server_data() -> Result<String, ServerFnError> {
    Ok("Hello from the server!".to_string())
}

The GetServerData type has to be registered in the server, which will add the corresponding route to the server.

register_explicit::<GetServerData>();

Finally, the server is started and it begins responding to requests.