codify "platform" into resolution logic, server-fn/fullstack overhaul (#4596)

overhaul dioxus fullstack using a new implementation of server_fn

This PR is a complete rewrite of dioxus fullstack designed to improve compatibility with axum and to fix a number of compilation errors based on feature flag unification plumbing through macros.
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2025-10-03 18:26:19 -07:00
1 parent 8421c3aef0
commit f2b1c6d97f
424 files changed
+17284 -6624

No files matched your search

+86
View File
@@ -0,0 +1,86 @@
//! This example shows how to use the `Router` component to create a simple navigation system.
//! The more complex router example uses all of the router features, while this simple example showcases
//! just the `Layout` and `Route` features.
//!
//! Layouts let you wrap chunks of your app with a component. This is useful for things like a footers, heeaders, etc.
//! Routes are enum variants with that match the name of a component in scope. This way you can create a new route
//! in your app simply by adding the variant to the enum and creating a new component with the same name. You can
//! override this of course.
use dioxus::prelude::*;
const STYLE: Asset = asset!("/examples/assets/flat_router.css");
fn main() {
dioxus::launch(|| {
rsx! {
Stylesheet { href: STYLE }
Router::<Route> {}
}
})
}
#[derive(Routable, Clone)]
#[rustfmt::skip]
enum Route {
#[layout(Footer)] // wrap the entire app in a footer
#[route("/")]
Home {},
#[route("/games")]
Games {},
#[route("/play")]
Play {},
#[route("/settings")]
Settings {},
}
#[component]
fn Footer() -> Element {
rsx! {
nav {
Link { to: Route::Home {}, class: "nav-btn", "Home" }
Link { to: Route::Games {}, class: "nav-btn", "Games" }
Link { to: Route::Play {}, class: "nav-btn", "Play" }
Link { to: Route::Settings {}, class: "nav-btn", "Settings" }
}
div { id: "content",
Outlet::<Route> {}
}
}
}
#[component]
fn Home() -> Element {
rsx!(
h1 { "Home" }
p { "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." }
)
}
#[component]
fn Games() -> Element {
rsx!(
h1 { "Games" }
// Dummy text that talks about video games
p { "Lorem games are sit amet Sed do eiusmod tempor et dolore magna aliqua." }
)
}
#[component]
fn Play() -> Element {
rsx!(
h1 { "Play" }
p { "Always play with your full heart adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." }
)
}
#[component]
fn Settings() -> Element {
rsx!(
h1 { "Settings" }
p { "Settings are consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." }
)
}
+130
View File
@@ -0,0 +1,130 @@
//! This example shows how to use the hash segment to store state in the url.
//!
//! You can set up two way data binding between the url hash and signals.
//!
//! Run this example on desktop with
//! ```sh
//! dx serve --example hash_fragment_state --features=ciborium,base64
//! ```
//! Or on web with
//! ```sh
//! dx serve --platform web --features web --example hash_fragment_state --features=ciborium,base64 -- --no-default-features
//! ```
use std::{fmt::Display, str::FromStr};
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use dioxus::prelude::*;
use serde::{Deserialize, Serialize};
fn main() {
dioxus::launch(|| {
rsx! {
Router::<Route> {}
}
});
}
#[derive(Routable, Clone, Debug, PartialEq)]
#[rustfmt::skip]
enum Route {
#[route("/#:url_hash")]
Home {
url_hash: State,
},
}
// You can use a custom type with the hash segment as long as it implements Display, FromStr and Default
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
struct State {
counters: Vec<usize>,
}
// Display the state in a way that can be parsed by FromStr
impl Display for State {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut serialized = Vec::new();
if ciborium::into_writer(self, &mut serialized).is_ok() {
write!(f, "{}", STANDARD.encode(serialized))?;
}
Ok(())
}
}
enum StateParseError {
DecodeError(base64::DecodeError),
CiboriumError(ciborium::de::Error<std::io::Error>),
}
impl std::fmt::Display for StateParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::DecodeError(err) => write!(f, "Failed to decode base64: {}", err),
Self::CiboriumError(err) => write!(f, "Failed to deserialize: {}", err),
}
}
}
// Parse the state from a string that was created by Display
impl FromStr for State {
type Err = StateParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let decompressed = STANDARD
.decode(s.as_bytes())
.map_err(StateParseError::DecodeError)?;
let parsed = ciborium::from_reader(std::io::Cursor::new(decompressed))
.map_err(StateParseError::CiboriumError)?;
Ok(parsed)
}
}
#[component]
fn Home(url_hash: ReadSignal<State>) -> Element {
// The initial state of the state comes from the url hash
let mut state = use_signal(&*url_hash);
// Change the state signal when the url hash changes
use_memo(move || {
if *state.peek() != *url_hash.read() {
state.set(url_hash());
}
});
// Change the url hash when the state changes
use_memo(move || {
if *state.read() != *url_hash.peek() {
navigator().replace(Route::Home { url_hash: state() });
}
});
rsx! {
button {
onclick: move |_| state.write().counters.clear(),
"Reset"
}
button {
onclick: move |_| {
state.write().counters.push(0);
},
"Add Counter"
}
for counter in 0..state.read().counters.len() {
div {
button {
onclick: move |_| {
state.write().counters.remove(counter);
},
"Remove"
}
button {
onclick: move |_| {
state.write().counters[counter] += 1;
},
"Count: {state.read().counters[counter]}"
}
}
}
}
}
+81
View File
@@ -0,0 +1,81 @@
//! How to use links in Dioxus
//!
//! The `router` crate gives us a `Link` component which is a much more powerful version of the standard HTML link.
//! However, you can use the traditional `<a>` tag if you want to build your own `Link` component.
//!
//! The `Link` component integrates with the Router and is smart enough to detect if the link is internal or external.
//! It also allows taking any `Route` as a target, making your links typesafe
use dioxus::prelude::*;
const STYLE: Asset = asset!("/examples/assets/links.css");
fn main() {
dioxus::launch(app);
}
fn app() -> Element {
rsx! (
Stylesheet { href: STYLE }
Router::<Route> {}
)
}
#[derive(Routable, Clone)]
#[rustfmt::skip]
enum Route {
#[layout(Header)]
#[route("/")]
Home {},
#[route("/default-links")]
DefaultLinks {},
#[route("/settings")]
Settings {},
}
#[component]
fn Header() -> Element {
rsx! {
h1 { "Your app here" }
nav { id: "nav",
Link { to: Route::Home {}, "home" }
Link { to: Route::DefaultLinks {}, "default links" }
Link { to: Route::Settings {}, "settings" }
}
Outlet::<Route> {}
}
}
#[component]
fn Home() -> Element {
rsx!( h1 { "Home" } )
}
#[component]
fn Settings() -> Element {
rsx!( h1 { "Settings" } )
}
#[component]
fn DefaultLinks() -> Element {
rsx! {
// Just some default links
div { id: "external-links",
// This link will open in a webbrowser
a { href: "http://dioxuslabs.com/", "Default link - links outside of your app" }
// This link will do nothing - we're preventing the default behavior
// It will just log "Hello Dioxus" to the console
a {
href: "http://dioxuslabs.com/",
onclick: |event| {
event.prevent_default();
println!("Hello Dioxus")
},
"Custom event link - links inside of your app"
}
}
}
}
+117
View File
@@ -0,0 +1,117 @@
//! This example shows how to access and use query segments present in an url on the web.
//!
//! The enum router makes it easy to use your route as state in your app. This example shows how to use the router to encode search text into the url and decode it back into a string.
//!
//! Run this example on desktop with
//! ```sh
//! dx serve --example query_segment_search
//! ```
//! Or on web with
//! ```sh
//! dx serve --platform web --features web --example query_segment_search -- --no-default-features
//! ```
use dioxus::prelude::*;
fn main() {
dioxus::launch(|| {
rsx! {
Router::<Route> {}
}
});
}
#[derive(Routable, Clone, Debug, PartialEq)]
#[rustfmt::skip]
enum Route {
#[route("/")]
Home {},
// The each query segment must implement <https://docs.rs/dioxus-router/latest/dioxus_router/routable/trait.FromQueryArgument.html> and Display.
// You can use multiple query segments separated by `&`s.
#[route("/search?:query&:word_count")]
Search {
query: String,
word_count: usize,
},
}
#[component]
fn Home() -> Element {
// Display a list of example searches in the home page
rsx! {
ul {
li {
Link {
to: Route::Search {
query: "hello".to_string(),
word_count: 1
},
"Search for results containing 'hello' and at least one word"
}
}
li {
Link {
to: Route::Search {
query: "dioxus".to_string(),
word_count: 2
},
"Search for results containing 'dioxus' and at least two word"
}
}
}
}
}
// Instead of accepting String and usize directly, we use ReadSignal to make the parameters `Copy` and let us subscribe to them automatically inside the meme
#[component]
fn Search(query: ReadSignal<String>, word_count: ReadSignal<usize>) -> Element {
const ITEMS: &[&str] = &[
"hello",
"world",
"hello world",
"hello dioxus",
"hello dioxus-router",
];
// Find all results that contain the query and the right number of words
// This memo will automatically rerun when the query or word count changes because we read the signals inside the closure
let results = use_memo(move || {
ITEMS
.iter()
.filter(|item| {
item.contains(&*query.read()) && item.split_whitespace().count() >= word_count()
})
.collect::<Vec<_>>()
});
rsx! {
h1 { "Search for {query}" }
input {
oninput: move |e| {
// Every time the query changes, we change the current route to the new query
navigator().replace(Route::Search {
query: e.value(),
word_count: word_count(),
});
},
value: "{query}",
}
input {
r#type: "number",
oninput: move |e| {
// Every time the word count changes, we change the current route to the new query
if let Ok(word_count) = e.value().parse() {
navigator().replace(Route::Search {
query: query(),
word_count,
});
}
},
value: "{word_count}",
}
for result in results.read().iter() {
div { "{result}" }
}
}
}
+128
View File
@@ -0,0 +1,128 @@
//! An advanced usage of the router with nested routes and redirects.
//!
//! Dioxus implements an enum-based router, which allows you to define your routes in a type-safe way.
//! However, since we need to bake quite a bit of logic into the enum, we have to add some extra syntax.
//!
//! Note that you don't need to use advanced features like nest, redirect, etc, since these can all be implemented
//! manually, but they are provided as a convenience.
use dioxus::prelude::*;
const STYLE: Asset = asset!("/examples/assets/router.css");
fn main() {
dioxus::launch(|| {
rsx! {
Stylesheet { href: STYLE }
Router::<Route> {}
}
});
}
// Turn off rustfmt since we're doing layouts and routes in the same enum
#[derive(Routable, Clone, Debug, PartialEq)]
#[rustfmt::skip]
#[allow(clippy::empty_line_after_outer_attr)]
enum Route {
// Wrap Home in a Navbar Layout
#[layout(NavBar)]
// The default route is always "/" unless otherwise specified
#[route("/")]
Home {},
// Wrap the next routes in a layout and a nest
#[nest("/blog")]
#[layout(Blog)]
// At "/blog", we want to show a list of blog posts
#[route("/")]
BlogList {},
// At "/blog/:name", we want to show a specific blog post, using the name slug
#[route("/:name")]
BlogPost { name: String },
// We need to end the blog layout and nest
// Note we don't need either - we could've just done `/blog/` and `/blog/:name` without nesting,
// but it's a bit cleaner this way
#[end_layout]
#[end_nest]
// And the regular page layout
#[end_layout]
// Add some redirects for the `/myblog` route
#[nest("/myblog")]
#[redirect("/", || Route::BlogList {})]
#[redirect("/:name", |name: String| Route::BlogPost { name })]
#[end_nest]
// Finally, we need to handle the 404 page
#[route("/:..route")]
PageNotFound {
route: Vec<String>,
},
}
#[component]
fn NavBar() -> Element {
rsx! {
nav { id: "navbar",
Link { to: Route::Home {}, "Home" }
Link { to: Route::BlogList {}, "Blog" }
}
Outlet::<Route> {}
}
}
#[component]
fn Home() -> Element {
rsx! { h1 { "Welcome to the Dioxus Blog!" } }
}
#[component]
fn Blog() -> Element {
rsx! {
h1 { "Blog" }
Outlet::<Route> {}
}
}
#[component]
fn BlogList() -> Element {
rsx! {
h2 { "Choose a post" }
div { id: "blog-list",
Link { to: Route::BlogPost { name: "Blog post 1".into() },
"Read the first blog post"
}
Link { to: Route::BlogPost { name: "Blog post 2".into() },
"Read the second blog post"
}
}
}
}
// We can use the `name` slug to show a specific blog post
// In theory we could read from the filesystem or a database here
#[component]
fn BlogPost(name: String) -> Element {
let contents = match name.as_str() {
"Blog post 1" => "This is the first blog post. It's not very interesting.",
"Blog post 2" => "This is the second blog post. It's not very interesting either.",
_ => "This blog post doesn't exist.",
};
rsx! {
h2 { "{name}" }
p { "{contents}" }
}
}
#[component]
fn PageNotFound(route: Vec<String>) -> Element {
rsx! {
h1 { "Page not found" }
p { "We are terribly sorry, but the page you requested doesn't exist." }
pre { color: "red", "log:\nattempted to navigate to: {route:?}" }
}
}
+64
View File
@@ -0,0 +1,64 @@
//! Example: Updating components with use_resource
//! -----------------
//!
//! This example shows how to use ReadSignal to make props reactive
//! when linking to it from the same component, when using use_resource
use dioxus::prelude::*;
#[derive(Clone, Routable, Debug, PartialEq)]
enum Route {
#[route("/")]
Home {},
#[route("/blog/:id")]
Blog { id: i32 },
}
fn main() {
dioxus::launch(App);
}
#[component]
fn App() -> Element {
rsx! {
Router::<Route> {}
}
}
// We use id: ReadSignal<i32> instead of id: i32 to make id work with reactive hooks
// Any i32 we pass in will automatically be converted into a ReadSignal<i32>
#[component]
fn Blog(id: ReadSignal<i32>) -> Element {
async fn future(n: i32) -> i32 {
n
}
// Because we accept ReadSignal<i32> instead of i32, the resource will automatically subscribe to the id when we read it
let res = use_resource(move || future(id()));
match res() {
Some(id) => rsx! {
div {
"Blog post {id}"
}
for i in 0..10 {
div {
Link { to: Route::Blog { id: i }, "Go to Blog {i}" }
}
}
},
None => rsx! {},
}
}
#[component]
fn Home() -> Element {
rsx! {
Link {
to: Route::Blog {
id: 0
},
"Go to blog"
}
}
}
@@ -0,0 +1,73 @@
use std::rc::Rc;
use dioxus::html::geometry::PixelsVector2D;
use dioxus::prelude::*;
#[derive(Clone, Routable, Debug, PartialEq)]
enum Route {
#[route("/")]
Home {},
#[route("/blog/:id")]
Blog { id: i32 },
}
fn main() {
dioxus::launch(App);
}
#[component]
fn App() -> Element {
use_context_provider(|| Signal::new(Scroll::default()));
rsx! {
Router::<Route> {}
}
}
#[component]
fn Blog(id: i32) -> Element {
rsx! {
GoBackButton { "Go back" }
div { "Blog post {id}" }
}
}
type Scroll = Option<PixelsVector2D>;
#[component]
fn Home() -> Element {
let mut element: Signal<Option<Rc<MountedData>>> = use_signal(|| None);
let mut scroll = use_context::<Signal<Scroll>>();
_ = use_resource(move || async move {
if let (Some(element), Some(scroll)) = (element.read().as_ref(), *scroll.peek()) {
element
.scroll(scroll, ScrollBehavior::Instant)
.await
.unwrap();
}
});
rsx! {
div {
height: "300px",
overflow_y: "auto",
border: "1px solid black",
onmounted: move |event| element.set(Some(event.data())),
onscroll: move |_| async move {
if let Some(element) = element.cloned() {
scroll.set(Some(element.get_scroll_offset().await.unwrap()))
}
},
for i in 0..100 {
div { height: "20px",
Link { to: Route::Blog { id: i }, "Blog {i}" }
}
}
}
}
}
+68
View File
@@ -0,0 +1,68 @@
//! A simple example of a router with a few routes and a nav bar.
use dioxus::prelude::*;
fn main() {
// launch the router, using our `Route` component as the generic type
// This will automatically boot the app to "/" unless otherwise specified
dioxus::launch(|| rsx! { Router::<Route> {} });
}
/// By default, the Routable derive will use the name of the variant as the route
/// You can also specify a specific component by adding the Component name to the `#[route]` attribute
#[rustfmt::skip]
#[derive(Routable, Clone, PartialEq)]
enum Route {
// Wrap the app in a Nav layout
#[layout(Nav)]
#[route("/")]
Homepage {},
#[route("/blog/:id")]
Blog { id: String },
}
#[component]
fn Homepage() -> Element {
rsx! { h1 { "Welcome home" } }
}
#[component]
fn Blog(id: String) -> Element {
rsx! {
h1 { "How to make: " }
p { "{id}" }
}
}
/// A simple nav bar that links to the homepage and blog pages
///
/// The `Route` enum gives up typesafe routes, allowing us to rename routes and serialize them automatically
#[component]
fn Nav() -> Element {
rsx! {
nav {
li {
Link { to: Route::Homepage {}, "Go home" }
}
li {
Link {
to: Route::Blog {
id: "Brownies".to_string(),
},
onclick: move |_| { println!("Clicked on Brownies") },
"Learn Brownies"
}
}
li {
Link {
to: Route::Blog {
id: "Cookies".to_string(),
},
"Learn Cookies"
}
}
}
div { Outlet::<Route> {} }
}
}