Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/typos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,5 @@ jobs:
- name: Run lychee
uses: lycheeverse/lychee-action@v2
with:
args: --base . --config ./lychee.toml './**/*.md'
args: --config ./lychee.toml './**/*.md'
fail: true
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,11 @@ name = "flat_router"
path = "examples/06-routing/flat_router.rs"
doc-scrape-examples = true

[[example]]
name = "query_params"
path = "examples/07-fullstack/query_params.rs"
doc-scrape-examples = true

[[example]]
name = "server_functions"
path = "examples/07-fullstack/server_functions.rs"
Expand Down
2 changes: 1 addition & 1 deletion examples/07-fullstack/fullstack_hello_world.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ fn main() {
});
}

#[get("/api/{name}/?age")]
#[get("/api/:name/?age")]
async fn get_message(name: String, age: i32) -> Result<String> {
Ok(format!("Hello {}, you are {} years old!", name, age))
}
55 changes: 55 additions & 0 deletions examples/07-fullstack/query_params.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//! An example showcasing query parameters in Dioxus Fullstack server functions.
//!
//! The query parameter syntax mostly follows axum, but with a few extra conveniences.
//! - can rename parameters in the function signature with `?age=age_in_years` where `age_in_years` is Rust variable name
//! - can absorb all query params with `?{object}` directly into a struct implementing `Deserialize`

use dioxus::prelude::*;

fn main() {
dioxus::launch(|| {
let mut message = use_action(get_message);
let mut message_rebind = use_action(get_message_rebind);
let mut message_all = use_action(get_message_all);

rsx! {
h1 { "Server says: "}
div {
button { onclick: move |_| message.call(22), "Single" }
pre { "{message:?}"}
}
div {
button { onclick: move |_| message_rebind.call(25), "Rebind" }
pre { "{message_rebind:?}"}
}
div {
button { onclick: move |_| message_all.call(Params { age: 30, name: "world".into() }), "Bind all" }
pre { "{message_all:?}"}
}
}
});
}

#[get("/api/message/?age")]
async fn get_message(age: i32) -> Result<String> {
Ok(format!("You are {} years old!", age))
}

#[get("/api/rebind/?age=age_in_years")]
async fn get_message_rebind(age_in_years: i32) -> Result<String> {
Ok(format!("You are {} years old!", age_in_years))
}

#[derive(serde::Deserialize, serde::Serialize, Debug)]
struct Params {
age: i32,
name: String,
}

#[get("/api/all/?{query}")]
async fn get_message_all(query: Params) -> Result<String> {
Ok(format!(
"Hello {}, you are {} years old!",
query.name, query.age
))
}
4 changes: 2 additions & 2 deletions examples/07-fullstack/server_functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@
//! take a `State<T>` extractor cannot be automatically added to the router since the dioxus router
//! type does not know how to construct the `T` type.
//!
//! These server functions will be registered once the `ServerFnState<T>` layer is added to the app with
//! `router = router.layer(ServerFnState::new(your_state))`.
//! These server functions will be registered once the `ServerState<T>` layer is added to the app with
//! `router = router.layer(ServerState::new(your_state))`.
//!
//! ## Middleware
//!
Expand Down
74 changes: 73 additions & 1 deletion examples/07-fullstack/server_state.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
//! This example shows how to use global state to maintain state between server functions.

use dioxus::prelude::*;
use std::rc::Rc;

use axum_core::extract::{FromRef, FromRequest};
use dioxus::{
fullstack::{FullstackContext, extract::State},
prelude::*,
};
use reqwest::header::HeaderMap;

#[cfg(feature = "server")]
use {
Expand Down Expand Up @@ -77,7 +84,68 @@ type BroadcastExtension = axum::Extension<tokio::sync::broadcast::Sender<String>

#[post("/api/broadcast", ext: BroadcastExtension)]
async fn broadcast_message() -> Result<()> {
let rt = Rc::new("asdasd".to_string());
ext.send("New broadcast message".to_string())?;
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
println!("rt: {}", rt);

Ok(())
}

/*
Option 4:

You can use Axum's `State` extractor to provide custom application state to your server functions.

All ServerFunctions pull in `FullstackContext`, so you need to implement `FromRef<FullstackContext>` for your
custom state type. To add your state to your app, you can use `.register_server_functions()` on a router
for a given state type, which will automatically add your state into the `FullstackContext` used by your server functions.

There are two details to note here:

- You need to implement `FromRef<FullstackContext>` for your custom state type.
- Custom extractors need to implement `FromRequest<S>` where `S` is the state type that implements `FromRef<FullstackContext>`.
*/
#[derive(Clone)]
struct MyAppState {
abc: i32,
}

impl FromRef<FullstackContext> for MyAppState {
fn from_ref(state: &FullstackContext) -> Self {
state.extension::<MyAppState>().unwrap()
}
}

struct CustomExtractor {
abc: i32,
headermap: HeaderMap,
}

impl<S> FromRequest<S> for CustomExtractor
where
MyAppState: FromRef<S>,
S: Send + Sync,
{
type Rejection = ();

async fn from_request(
_req: axum::extract::Request,
state: &S,
) -> std::result::Result<Self, Self::Rejection> {
let state = MyAppState::from_ref(state);
Ok(CustomExtractor {
abc: state.abc,
headermap: HeaderMap::new(),
})
}
}

#[post("/api/stateful", state: State<MyAppState>, ex: CustomExtractor)]
async fn app_state() -> Result<()> {
println!("abc: {}", state.abc);
println!("state abc: {:?}", ex.abc);
println!("headermap: {:?}", ex.headermap);
Ok(())
}

Expand All @@ -95,6 +163,10 @@ fn main() {
let router = dioxus::server::router(app)
.layer(Extension(tokio::sync::broadcast::channel::<String>(16).0));

// To use our custom app state with `State<MyAppState>`, we need to register it
// as an extension since our `FromRef<FullstackContext>` implementation relies on it.
let router = router.layer(Extension(MyAppState { abc: 42 }));

Ok(router)
});
}
Expand Down
4 changes: 1 addition & 3 deletions packages/dioxus/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,9 +211,7 @@ pub mod prelude {
#[cfg(feature = "server")]
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
#[doc(inline)]
pub use dioxus_server::{
self, serve, DioxusRouterExt, DioxusRouterFnExt, ServeConfig, ServerFunction,
};
pub use dioxus_server::{self, serve, DioxusRouterExt, ServeConfig, ServerFunction};

#[cfg(feature = "router")]
#[cfg_attr(docsrs, doc(cfg(feature = "router")))]
Expand Down
14 changes: 14 additions & 0 deletions packages/fullstack-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,20 @@ impl From<RequestError> for ServerFnError {
}
}

impl From<ServerFnError> for HttpError {
fn from(value: ServerFnError) -> Self {
let status = StatusCode::from_u16(match &value {
ServerFnError::ServerError { code, .. } => *code,
_ => 500,
})
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
HttpError {
status,
message: Some(value.to_string()),
}
}
}

impl From<HttpError> for ServerFnError {
fn from(value: HttpError) -> Self {
ServerFnError::ServerError {
Expand Down
3 changes: 0 additions & 3 deletions packages/fullstack-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,3 @@ pub use error::*;

pub mod httperror;
pub use httperror::*;

#[derive(Clone, Default)]
pub struct DioxusServerState {}
25 changes: 18 additions & 7 deletions packages/fullstack-core/src/streaming.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@ use std::collections::HashSet;
use std::fmt::Debug;
use std::sync::Arc;

tokio::task_local! {
static FULLSTACK_CONTEXT: FullstackContext;
}

/// The context provided by dioxus fullstack for server-side rendering.
///
/// This context will only be set on the server during the initial streaming response
Expand All @@ -21,10 +17,18 @@ tokio::task_local! {
pub struct FullstackContext {
// We expose the lock for request headers directly so it needs to be in a separate lock
request_headers: Arc<RwLock<http::request::Parts>>,

// The rest of the fields are only held internally, so we can group them together
lock: Arc<RwLock<FullstackContextInner>>,
}

// `FullstackContext` is always set when either
// 1. rendering the app via SSR
// 2. handling a server function request
tokio::task_local! {
static FULLSTACK_CONTEXT: FullstackContext;
}

pub struct FullstackContextInner {
current_status: StreamingStatus,
current_status_subscribers: HashSet<ReactiveContext>,
Expand Down Expand Up @@ -78,6 +82,7 @@ impl FullstackContext {
pub fn commit_initial_chunk(&mut self) {
let mut lock = self.lock.write();
lock.current_status = StreamingStatus::InitialChunkCommitted;

// The key type is mutable, but the hash is stable through mutations because we hash by pointer
#[allow(clippy::mutable_key_type)]
let subscribers = std::mem::take(&mut lock.current_status_subscribers);
Expand Down Expand Up @@ -110,17 +115,23 @@ impl FullstackContext {
FULLSTACK_CONTEXT.scope(self, fut).await
}

/// Extract an extension from the current request.
pub fn extension<T: Clone + Send + Sync + 'static>(&self) -> Option<T> {
let lock = self.request_headers.read();
lock.extensions.get::<T>().cloned()
}

/// Extract an axum extractor from the current request.
///
/// The body of the request is always empty when using this method, as the body can only be consumed once in the server
/// function extractors.
pub async fn extract<T: FromRequest<(), M>, M>() -> Result<T, ServerFnError> {
pub async fn extract<T: FromRequest<Self, M>, M>() -> Result<T, ServerFnError> {
let this = Self::current()
.ok_or_else(|| ServerFnError::new("No FullstackContext found".to_string()))?;

let parts = this.request_headers.read().clone();
let request = axum_core::extract::Request::from_parts(parts, Default::default());
match T::from_request(request, &()).await {
match T::from_request(request, &this).await {
Ok(res) => Ok(res),
Err(err) => {
let resp = err.into_response();
Expand Down Expand Up @@ -255,7 +266,7 @@ pub fn commit_initial_chunk() {

/// Extract an axum extractor from the current request.
#[deprecated(note = "Use FullstackContext::extract instead", since = "0.7.0")]
pub fn extract<T: FromRequest<(), M>, M>(
pub fn extract<T: FromRequest<FullstackContext, M>, M>(
) -> impl std::future::Future<Output = Result<T, ServerFnError>> {
FullstackContext::extract::<T, M>()
}
Expand Down
Loading
Loading