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

+89
View File
@@ -0,0 +1,89 @@
//! Demonstrates cross-component state sharing using Dioxus' Context API
//!
//! Features:
//! - Context provider initialization
//! - Nested component consumption
//! - Reactive state updates
//! - Error handling for missing context
//! - Platform-agnostic implementation
use dioxus::prelude::*;
const STYLE: Asset = asset!("/examples/assets/context_api.css");
fn main() {
launch(app);
}
#[component]
fn app() -> Element {
// Provide theme context at root level
use_context_provider(|| Signal::new(Theme::Light));
rsx! {
Stylesheet { href: STYLE }
main {
class: "main-container",
h1 { "Theme Switcher" }
ThemeControls {}
ThemeDisplay {}
}
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
enum Theme {
Light,
Dark,
}
impl Theme {
fn stylesheet(&self) -> &'static str {
match self {
Theme::Light => "light-theme",
Theme::Dark => "dark-theme",
}
}
}
#[component]
fn ThemeControls() -> Element {
let mut theme = use_theme_context();
let current_theme = *theme.read();
rsx! {
div {
class: "controls",
button {
class: "btn",
onclick: move |_| theme.set(Theme::Light),
disabled: current_theme== Theme::Light,
"Switch to Light"
}
button {
class: "btn",
onclick: move |_| theme.set(Theme::Dark),
disabled: current_theme == Theme::Dark,
"Switch to Dark"
}
}
}
}
#[component]
fn ThemeDisplay() -> Element {
let theme = use_theme_context();
rsx! {
div {
class: "display {theme.read().stylesheet()}",
p { "Current theme: {theme:?}" }
p { "Try switching themes using the buttons above!" }
}
}
}
fn use_theme_context() -> Signal<Theme> {
try_use_context::<Signal<Theme>>()
.expect("Theme context not found. Ensure <App> is the root component.")
}
@@ -0,0 +1,156 @@
//! This example showcases how to use the ErrorBoundary component to handle errors in your app.
//!
//! The ErrorBoundary component is a special component that can be used to catch panics and other errors that occur.
//! By default, Dioxus will catch panics during rendering, async, and handlers, and bubble them up to the nearest
//! error boundary. If no error boundary is present, it will be caught by the root error boundary and the app will
//! render the error message as just a string.
//!
//! NOTE: In wasm, panics can currently not be caught by the error boundary. This is a limitation of WASM in rust.
#![allow(non_snake_case)]
use dioxus::prelude::*;
fn main() {
dioxus::launch(|| rsx! { Router::<Route> {} });
}
/// You can use an ErrorBoundary to catch errors in children and display a warning
fn Simple() -> Element {
rsx! {
GoBackButton { "Home" }
ErrorBoundary {
handle_error: |error: ErrorContext| rsx! {
h1 { "An error occurred" }
pre { "{error:#?}" }
},
ParseNumber {}
}
}
}
#[component]
fn ParseNumber() -> Element {
rsx! {
h1 { "Error handler demo" }
button {
onclick: move |_| {
// You can return a result from an event handler which lets you easily quit rendering early if something fails
let data: i32 = "0.5".parse()?;
println!("parsed {data}");
Ok(())
},
"Click to throw an error"
}
}
}
// You can provide additional context for the Error boundary to visualize
fn Show() -> Element {
rsx! {
GoBackButton { "Home" }
div {
ErrorBoundary {
handle_error: |errors: ErrorContext| {
rsx! {
for error in errors.error() {
// You can downcast the error to see if it's a specific type and render something specific for it
if let Some(_error) = error.downcast_ref::<std::num::ParseIntError>() {
div {
background_color: "red",
border: "black",
border_width: "2px",
border_radius: "5px",
p { "Failed to parse data" }
Link {
to: Route::Home {},
"Go back to the homepage"
}
}
} else {
pre {
color: "red",
"{error}"
}
}
}
}
},
ParseNumberWithShow {}
}
}
}
}
#[component]
fn ParseNumberWithShow() -> Element {
rsx! {
h1 { "Error handler demo" }
button {
onclick: move |_| {
let request_data = "0.5";
let data: i32 = request_data.parse()?;
println!("parsed {data}");
Ok(())
},
"Click to throw an error"
}
}
}
// On desktop, dioxus will catch panics in components and insert an error automatically
fn Panic() -> Element {
rsx! {
GoBackButton { "Home" }
ErrorBoundary {
handle_error: |errors: ErrorContext| rsx! {
h1 { "Another error occurred" }
pre { "{errors:#?}" }
},
ComponentPanic {}
}
}
}
#[component]
fn ComponentPanic() -> Element {
panic!("This component panics")
}
#[derive(Routable, Clone, Debug, PartialEq)]
enum Route {
#[route("/")]
Home {},
#[route("/simple")]
Simple {},
#[route("/panic")]
Panic {},
#[route("/show")]
Show {},
}
fn Home() -> Element {
rsx! {
ul {
li {
Link {
to: Route::Simple {},
"Simple errors"
}
}
li {
Link {
to: Route::Panic {},
"Capture panics"
}
}
li {
Link {
to: Route::Show {},
"Show errors"
}
}
}
}
}
+60
View File
@@ -0,0 +1,60 @@
//! Example: Global signals and memos
//!
//! This example demonstrates how to use global signals and memos to share state across your app.
//! Global signals are simply signals that live on the root of your app and are accessible from anywhere. To access a
//! global signal, simply use its methods like a regular signal. Calls to `read` and `write` will be forwarded to the
//! signal at the root of your app using the `static`'s address.
use dioxus::prelude::*;
const STYLE: Asset = asset!("/examples/assets/counter.css");
static COUNT: GlobalSignal<i32> = Signal::global(|| 0);
static DOUBLED_COUNT: GlobalMemo<i32> = Memo::global(|| COUNT() * 2);
fn main() {
dioxus::launch(app);
}
fn app() -> Element {
rsx! {
Stylesheet { href: STYLE }
Increment {}
Decrement {}
Reset {}
Display {}
}
}
#[component]
fn Increment() -> Element {
rsx! {
button { onclick: move |_| *COUNT.write() += 1, "Up high!" }
}
}
#[component]
fn Decrement() -> Element {
rsx! {
button { onclick: move |_| *COUNT.write() -= 1, "Down low!" }
}
}
#[component]
fn Display() -> Element {
rsx! {
p { "Count: {COUNT}" }
p { "Doubled: {DOUBLED_COUNT}" }
}
}
#[component]
fn Reset() -> Element {
// Not all write methods are available on global signals since `write` requires a mutable reference. In these cases,
// We can simply pull out the actual signal using the signal() method.
let mut as_signal = use_hook(|| COUNT.resolve());
rsx! {
button { onclick: move |_| as_signal.set(0), "Reset" }
}
}
+45
View File
@@ -0,0 +1,45 @@
//! This example shows how you can chain memos together to create a tree of memoized values.
//!
//! Memos will also pause when their parent component pauses, so if you have a memo that depends on a signal, and the
//! signal pauses, the memo will pause too.
use dioxus::prelude::*;
fn main() {
dioxus::launch(app);
}
fn app() -> Element {
let mut value = use_signal(|| 0);
let mut depth = use_signal(|| 0_usize);
let items = use_memo(move || (0..depth()).map(|f| f as _).collect::<Vec<isize>>());
let state = use_memo(move || value() + 1);
println!("rendering app");
rsx! {
button { onclick: move |_| value += 1, "Increment" }
button { onclick: move |_| depth += 1, "Add depth" }
button { onclick: move |_| depth -= 1, "Remove depth" }
if depth() > 0 {
Child { depth, items, state }
}
}
}
#[component]
fn Child(state: Memo<isize>, items: Memo<Vec<isize>>, depth: ReadSignal<usize>) -> Element {
// These memos don't get re-computed when early returns happen
let state = use_memo(move || state() + 1);
let item = use_memo(move || items()[depth() - 1]);
let depth = use_memo(move || depth() - 1);
println!("rendering child: {}", depth());
rsx! {
h3 { "Depth({depth})-Item({item}): {state}"}
if depth() > 0 {
Child { depth, state, items }
}
}
}
+56
View File
@@ -0,0 +1,56 @@
//! Example: Reducer Pattern
//! -----------------
//!
//! This example shows how to encapsulate state in dioxus components with the reducer pattern.
//! This pattern is very useful when a single component can handle many types of input that can
//! be represented by an enum.
use dioxus::prelude::*;
const STYLE: Asset = asset!("/examples/assets/radio.css");
fn main() {
dioxus::launch(app);
}
fn app() -> Element {
let mut state = use_signal(|| PlayerState { is_playing: false });
rsx!(
Stylesheet { href: STYLE }
h1 {"Select an option"}
// Add some cute animations if the radio is playing!
div { class: if state.read().is_playing { "bounce" },
"The radio is... " {state.read().is_playing()} "!"
}
button { id: "play", onclick: move |_| state.write().reduce(PlayerAction::Pause), "Pause" }
button { id: "pause", onclick: move |_| state.write().reduce(PlayerAction::Play), "Play" }
)
}
enum PlayerAction {
Pause,
Play,
}
#[derive(Clone)]
struct PlayerState {
is_playing: bool,
}
impl PlayerState {
fn reduce(&mut self, action: PlayerAction) {
match action {
PlayerAction::Pause => self.is_playing = false,
PlayerAction::Play => self.is_playing = true,
}
}
fn is_playing(&self) -> &'static str {
match self.is_playing {
true => "currently playing!",
false => "not currently playing",
}
}
}
+90
View File
@@ -0,0 +1,90 @@
//! A simple example demonstrating how to use signals to modify state from several different places.
//!
//! This simple example implements a counter that can be incremented, decremented, and paused. It also demonstrates
//! that background tasks in use_futures can modify the value as well.
//!
//! Most signals implement Into<ReadSignal<T>>, making ReadSignal a good default type when building new
//! library components that don't need to modify their values.
use async_std::task::sleep;
use dioxus::prelude::*;
fn main() {
dioxus::launch(app);
}
fn app() -> Element {
let mut running = use_signal(|| true);
let mut count = use_signal(|| 0);
let mut saved_values = use_signal(|| vec![0.to_string()]);
// use_memo will recompute the value of the signal whenever the captured signals change
let doubled_count = use_memo(move || count() * 2);
// use_effect will subscribe to any changes in the signal values it captures
// effects will always run after first mount and then whenever the signal values change
use_effect(move || println!("Count changed to {count}"));
// We can do early returns and conditional rendering which will pause all futures that haven't been polled
if count() > 30 {
return rsx! {
h1 { "Count is too high!" }
button { onclick: move |_| count.set(0), "Press to reset" }
};
}
// use_future will spawn an infinitely running future that can be started and stopped
use_future(move || async move {
loop {
if running() {
count += 1;
}
sleep(std::time::Duration::from_millis(400)).await;
}
});
// use_resource will spawn a future that resolves to a value
let _slow_count = use_resource(move || async move {
sleep(std::time::Duration::from_millis(200)).await;
count() * 2
});
rsx! {
h1 { "High-Five counter: {count}" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
button { onclick: move |_| running.toggle(), "Toggle counter" }
button { onclick: move |_| saved_values.push(count.to_string()), "Save this value" }
button { onclick: move |_| saved_values.clear(), "Clear saved values" }
// We can do boolean operations on the current signal value
if count() > 5 {
h2 { "High five!" }
}
// We can cleanly map signals with iterators
for value in saved_values.iter() {
h3 { "Saved value: {value}" }
}
// We can also use the signal value as a slice
if let [first, .., last] = saved_values.read().as_slice() {
li { "First and last: {first}, {last}" }
} else {
"No saved values"
}
// You can pass a value directly to any prop that accepts a signal
Child { count: doubled_count() }
Child { count: doubled_count }
}
}
#[component]
fn Child(mut count: ReadSignal<i32>) -> Element {
println!("rendering child with count {count}");
rsx! {
h1 { "{count}" }
}
}