Managing Fullstack Dependencies
Fullstack applications build to at least two different binaries:
- The client application that runs the desktop, mobile, or web application
- The server that renders the initial HTML and runs server functions
Those binaries tend to have different dependencies and those dependencies often are only compatible with a specific target platform. This guide will cover how fullstack manages each binary's dependencies and how to add dependencies that are only compatible with one binary/target.
Client and Server Feature Flags
Dioxus uses feature flags to differentiate between the different binaries a single library can produce. Each target binary should have a feature flag in your Cargo.toml
file that enables the corresponding feature in dioxus. For example, if you are targeting web
and desktop
with a fullstack server, you would add the following to your Cargo.toml
:
[dependencies] # Don't include any renderer features in your dioxus dependency directly. They will be added in feature flags. # The fullstack feature enables the bindings between the server and client without enabling a specific binary target. dioxus = { version = "0.6", features = ["fullstack"] } [features] # The web feature enables the web renderer. Dioxus will automatically enable the feature you define that activates `dioxus/web` when building the client WASM bundle. web = ["dioxus/web"] # The desktop feature enables the desktop renderer. Dioxus will automatically enable the feature you define that activates `dioxus/desktop` when building the client native bundle. desktop = ["dioxus/desktop"] # The server feature enables server functions and server-side rendering. Dioxus will automatically enable the feature you define that activates `dioxus/server` when building the server binary. server = ["dioxus/server"]
Feature flags like these for the client and server are automatically generated by the CLI when you run dx new
with fullstack enabled. If you are creating a project from scratch, you will need to add the feature flags manually.
If you are not familiar with features in rust, you can read more about feature flags in the cargo reference.
Adding Server Only Dependencies
Many dependencies like tokio
and axum
are only compatible with the server. If these dependencies are enabled when building a WASM bundle for the browser client, you will get a compilation error. For example, if we want to interact with the filesystem in a server function, we might want to add tokio
. tokio
has utilities for working with async IO like tokio::fs::File
. Let's try it as a dependency to our fullstack project:
[dependencies] # ... # ❌ If tokio is added as a required dependency, it will be included in both the server # and the web bundle. The web bundle will fail to build because tokio is not # compatible with wasm tokio = { version = "1", features = ["full"] }
If we try to compile with tokio as a required dependency, we will get a compilation error like this:
error[E0432]: unresolved import `crate::sys::IoSourceState` --> /Users/user/.cargo/registry/src/index.crates.io-6f17d22bba15001f/mio-1.0.2/src |source.rs:14:5 14 | use crate::sys::IoSourceState; | ^^^^^^^^^^^^^^^^^^^^^^^^^ no `IoSourceState` in `sys` ...
Since we added tokio
as a dependency for all three binaries, cargo tries to compile it for each target. This fails because tokio
is not compatible with the wasm32-unknown-unknown
target.
To fix the issue, we can make the dependency optional and only enable it in the server feature:
[dependencies] # ... # ✅ Since the tokio dependency is optional, it is not included in the web and desktop # bundles. tokio = { version = "1", features = ["full"], optional = true } [features] # ... # ✅ Since the tokio dependency is enabled in the server feature, it is included in # the server binary. server = ["dioxus/server", "dep:tokio"]
Now when we build with dx serve
, the project compiles successfully.
Adding Client Only Dependencies
Many dependencies like wasm-bindgen
and web-sys
are only compatible with the client. Unlike server-only dependencies, these dependencies can generally compile on native targets, but they will panic when used outside of the browser.
You can cut down on build times for your server and native binaries by only including web dependencies in the browser client binary.
Instead of adding web only dependencies every binary in your project like this:
[dependencies] # ... # ❌ If web-sys is added as a required dependency, it will be included in the server, # native, and the web bundle which makes build times longer. web-sys = { version = "0.3.60", features = ["console"] }
You can make the dependency optional and only enable it in the web
feature in your Cargo.toml
:
[dependencies] # ... # ✅ Since the web-sys dependency is optional, it is not included in the server and # native bundles. web-sys = { version = "0.3.60", features = ["console"], optional = true } [features] # ... # ✅ Since the web-sys dependency is enabled in the web feature, it is included in # the web bundle. web = ["dioxus/web", "dep:web-sys"]
Managing Binary Specific Imports
Once you have set up binary specific dependencies, you need to adjust any of your imports to only import the dependencies when building for the binary that includes those dependencies.
For example, if tokio
is only enabled in the server feature, you will need to import it like this:
// Since the tokio dependency is only enabled in the server feature, // we need to only import it when the server feature is enabled. #[cfg(feature = "server")] use tokio::fs::File; #[cfg(feature = "server")] use tokio::io::AsyncReadExt;
You also need to only compile any usage of the dependency when the feature is enabled:
// Since the tokio dependency is only enabled in the server feature, // we need to only compile any usage of the dependency when the server feature is enabled. #[cfg(feature = "server")] async fn read_file() -> Result<String, std::io::Error> { let mut file = File::open("path/to/file").await?; let mut contents = String::new(); file.read_to_string(&mut contents).await?; Ok(contents) } // The bodies of server functions automatically only compile when the server feature is enabled. #[server] async fn get_file_contents() -> Result<String, ServerFnError> { let mut file = File::open("path/to/file").await?; let mut contents = String::new(); file.read_to_string(&mut contents).await?; Ok(contents) }
It may be more convenient to group server or client specific code into a module that is only compiled when the feature is enabled:
// Instead of configuring each item that is only used in the server, you can group // them into a module that is only compiled when the server feature is enabled. #[cfg(feature = "server")] mod tokio_utilities { use std::path::PathBuf; use tokio::fs::File; use tokio::io::AsyncReadExt; pub async fn read_file(path: PathBuf) -> Result<String, std::io::Error> { let mut file = File::open(path).await?; let mut contents = String::new(); file.read_to_string(&mut contents).await?; Ok(contents) } } // Then you can define your server functions using shared utilities you defined for // server only code. #[server] async fn get_file_contents() -> Result<String, ServerFnError> { let file = tokio_utilities::read_file(PathBuf::from("path/to/file")).await?; Ok(file) } #[server] async fn get_other_file_contents() -> Result<String, ServerFnError> { let file = tokio_utilities::read_file(PathBuf::from("path/to/other/file")).await?; Ok(file) }
The rust reference has more information about conditional compilation in rust.