mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 23:04:24 +00:00
Allow accessing Scope
from server functions, which can be used to inject server-only dependencies like HttpRequest
This commit is contained in:
parent
8077ae9ead
commit
6ddc720227
9 changed files with 201 additions and 55 deletions
|
@ -27,6 +27,7 @@ cfg_if! {
|
|||
let app = move |cx| {
|
||||
let integration = ServerIntegration { path: path.clone() };
|
||||
provide_context(cx, RouterIntegrationContext::new(integration));
|
||||
provide_context(cx, req.clone());
|
||||
|
||||
view! { cx, <TodoApp/> }
|
||||
};
|
||||
|
@ -67,7 +68,9 @@ cfg_if! {
|
|||
|
||||
if let Some(server_fn) = server_fn_by_path(path.as_str()) {
|
||||
let body: &[u8] = &body;
|
||||
match server_fn(&body).await {
|
||||
let (cx, disposer) = raw_scope_and_disposer();
|
||||
provide_context(cx, req.clone());
|
||||
match server_fn(cx, &body).await {
|
||||
Ok(serialized) => {
|
||||
// if this is Accept: application/json then send a serialized JSON response
|
||||
if let Some("application/json") = accept_header {
|
||||
|
|
|
@ -34,7 +34,12 @@ cfg_if! {
|
|||
}
|
||||
|
||||
#[server(GetTodos, "/api")]
|
||||
pub async fn get_todos() -> Result<Vec<Todo>, ServerFnError> {
|
||||
pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
|
||||
// this is just an example of how to access server context injected in the handlers
|
||||
let req =
|
||||
use_context::<actix_web::HttpRequest>(cx).expect("couldn't get HttpRequest from context");
|
||||
println!("req.path = {:?}", req.path());
|
||||
|
||||
use futures::TryStreamExt;
|
||||
|
||||
let mut conn = db().await?;
|
||||
|
@ -114,7 +119,11 @@ pub fn Todos(cx: Scope) -> Element {
|
|||
let todo_deleted = delete_todo.version;
|
||||
|
||||
// list of todos is loaded from the server in reaction to changes
|
||||
let todos = create_resource(cx, move || (add_changed(), todo_deleted()), |_| get_todos());
|
||||
let todos = create_resource(
|
||||
cx,
|
||||
move || (add_changed(), todo_deleted()),
|
||||
move |_| get_todos(cx),
|
||||
);
|
||||
|
||||
view! {
|
||||
cx,
|
||||
|
|
|
@ -7,6 +7,21 @@ use syn::{
|
|||
*,
|
||||
};
|
||||
|
||||
fn fn_arg_is_cx(f: &syn::FnArg) -> bool {
|
||||
if let FnArg::Typed(t) = f {
|
||||
if let Type::Path(path) = &*t.ty {
|
||||
path.path
|
||||
.segments
|
||||
.iter()
|
||||
.any(|segment| segment.ident == "Scope")
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Result<TokenStream2> {
|
||||
let ServerFnName {
|
||||
struct_name,
|
||||
|
@ -31,7 +46,7 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
|
|||
}
|
||||
}
|
||||
|
||||
let fields = body.inputs.iter().map(|f| {
|
||||
let fields = body.inputs.iter().filter(|f| !fn_arg_is_cx(f)).map(|f| {
|
||||
let typed_arg = match f {
|
||||
FnArg::Receiver(_) => panic!("cannot use receiver types in server function macro"),
|
||||
FnArg::Typed(t) => t,
|
||||
|
@ -39,6 +54,28 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
|
|||
quote! { pub #typed_arg }
|
||||
});
|
||||
|
||||
let cx_arg = body
|
||||
.inputs
|
||||
.iter()
|
||||
.next()
|
||||
.and_then(|f| if fn_arg_is_cx(f) { Some(f) } else { None });
|
||||
let cx_assign_statement = if let Some(FnArg::Typed(arg)) = cx_arg {
|
||||
if let Pat::Ident(id) = &*arg.pat {
|
||||
quote! {
|
||||
let #id = cx;
|
||||
}
|
||||
} else {
|
||||
quote! {}
|
||||
}
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
let cx_fn_arg = if cx_arg.is_some() {
|
||||
quote! { cx, }
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
|
||||
let fn_args = body.inputs.iter().map(|f| {
|
||||
let typed_arg = match f {
|
||||
FnArg::Receiver(_) => panic!("cannot use receiver types in server function macro"),
|
||||
|
@ -50,7 +87,13 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
|
|||
|
||||
let field_names = body.inputs.iter().filter_map(|f| match f {
|
||||
FnArg::Receiver(_) => todo!(),
|
||||
FnArg::Typed(t) => Some(&t.pat),
|
||||
FnArg::Typed(t) => {
|
||||
if fn_arg_is_cx(f) {
|
||||
None
|
||||
} else {
|
||||
Some(&t.pat)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let field_names_2 = field_names.clone();
|
||||
|
@ -93,15 +136,16 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
|
|||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
fn call_fn(self) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Output, ::leptos::ServerFnError>> + Send>> {
|
||||
fn call_fn(self, cx: Scope) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Output, ::leptos::ServerFnError>>>> {
|
||||
let #struct_name { #(#field_names),* } = self;
|
||||
Box::pin(async move { #fn_name( #(#field_names_2),*).await })
|
||||
#cx_assign_statement;
|
||||
Box::pin(async move { #fn_name( #cx_fn_arg #(#field_names_2),*).await })
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
fn call_fn_client(self) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Output, ::leptos::ServerFnError>>>> {
|
||||
fn call_fn_client(self, cx: Scope) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Output, ::leptos::ServerFnError>>>> {
|
||||
let #struct_name { #(#field_names_3),* } = self;
|
||||
Box::pin(async move { #fn_name( #(#field_names_4),*).await })
|
||||
Box::pin(async move { #fn_name( #cx_fn_arg #(#field_names_4),*).await })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,24 @@
|
|||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::{quote, quote_spanned};
|
||||
use syn::{spanned::Spanned, ExprPath};
|
||||
use syn_rsx::{Node, NodeName, NodeElement, NodeAttribute, NodeValueExpr};
|
||||
use syn_rsx::{Node, NodeAttribute, NodeElement, NodeName, NodeValueExpr};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{is_component_node, Mode};
|
||||
|
||||
const NON_BUBBLING_EVENTS: [&str; 11] = ["load", "unload", "scroll", "focus", "blur", "loadstart", "progress", "error", "abort", "load", "loadend"];
|
||||
const NON_BUBBLING_EVENTS: [&str; 11] = [
|
||||
"load",
|
||||
"unload",
|
||||
"scroll",
|
||||
"focus",
|
||||
"blur",
|
||||
"loadstart",
|
||||
"progress",
|
||||
"error",
|
||||
"abort",
|
||||
"load",
|
||||
"loadend",
|
||||
];
|
||||
|
||||
pub(crate) fn render_view(cx: &Ident, nodes: &[Node], mode: Mode) -> TokenStream {
|
||||
let template_uid = Ident::new(
|
||||
|
@ -160,10 +172,13 @@ enum PrevSibChange {
|
|||
}
|
||||
|
||||
fn attributes(node: &NodeElement) -> impl Iterator<Item = &NodeAttribute> {
|
||||
node
|
||||
.attributes
|
||||
.iter()
|
||||
.filter_map(|node| if let Node::Attribute(attribute) = node { Some(attribute )} else { None })
|
||||
node.attributes.iter().filter_map(|node| {
|
||||
if let Node::Attribute(attribute) = node {
|
||||
Some(attribute)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
|
@ -210,10 +225,19 @@ fn element_to_tokens(
|
|||
}
|
||||
|
||||
// for SSR: merge all class: attributes and class attribute
|
||||
if mode == Mode::Ssr {
|
||||
let class_attr = attributes(node).find(|a| a.key.to_string() == "class")
|
||||
if mode == Mode::Ssr {
|
||||
let class_attr = attributes(node)
|
||||
.find(|a| a.key.to_string() == "class")
|
||||
.map(|node| {
|
||||
(node.key.span(), node.value.as_ref().and_then(|n| String::try_from(n).ok()).unwrap_or_default().trim().to_string())
|
||||
(
|
||||
node.key.span(),
|
||||
node.value
|
||||
.as_ref()
|
||||
.and_then(|n| String::try_from(n).ok())
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_string(),
|
||||
)
|
||||
});
|
||||
|
||||
let class_attrs = attributes(node).filter_map(|node| {
|
||||
|
@ -237,7 +261,7 @@ fn element_to_tokens(
|
|||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
|
||||
if class_attr.is_some() || !class_attrs.is_empty() {
|
||||
expressions.push(quote::quote_spanned! {
|
||||
span => leptos_buffer.push_str(" class=\"");
|
||||
|
@ -354,7 +378,7 @@ fn element_to_tokens(
|
|||
expressions,
|
||||
multi,
|
||||
mode,
|
||||
idx == 0
|
||||
idx == 0,
|
||||
);
|
||||
|
||||
prev_sib = match curr_id {
|
||||
|
@ -393,10 +417,10 @@ fn next_sibling_node(children: &[Node], idx: usize, next_el_id: &mut usize) -> O
|
|||
} else {
|
||||
Some(child_ident(*next_el_id + 1, sibling.name.span()))
|
||||
}
|
||||
},
|
||||
}
|
||||
Node::Block(sibling) => Some(child_ident(*next_el_id + 1, sibling.value.span())),
|
||||
Node::Text(sibling) => Some(child_ident(*next_el_id + 1, sibling.value.span())),
|
||||
_ => panic!("expected either an element or a block")
|
||||
_ => panic!("expected either an element or a block"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -485,7 +509,7 @@ fn attr_to_tokens(
|
|||
.as_ref();
|
||||
|
||||
if mode != Mode::Ssr {
|
||||
let name = name.replacen("on:", "", 1);
|
||||
let name = name.replacen("on:", "", 1);
|
||||
if NON_BUBBLING_EVENTS.contains(&name.as_str()) {
|
||||
expressions.push(quote_spanned! {
|
||||
span => ::leptos::add_event_listener_undelegated(#el_id.unchecked_ref(), #name, #handler);
|
||||
|
@ -504,23 +528,31 @@ fn attr_to_tokens(
|
|||
}
|
||||
}
|
||||
// Properties
|
||||
else if name.starts_with("prop:") {
|
||||
else if name.starts_with("prop:") {
|
||||
let name = name.replacen("prop:", "", 1);
|
||||
// can't set properties in SSR
|
||||
if mode != Mode::Ssr {
|
||||
let value = node.value.as_ref().expect("prop: blocks need values").as_ref();
|
||||
if mode != Mode::Ssr {
|
||||
let value = node
|
||||
.value
|
||||
.as_ref()
|
||||
.expect("prop: blocks need values")
|
||||
.as_ref();
|
||||
expressions.push(quote_spanned! {
|
||||
span => leptos_dom::property(#cx, #el_id.unchecked_ref(), #name, #value.into_property(#cx))
|
||||
});
|
||||
}
|
||||
}
|
||||
// Classes
|
||||
else if name.starts_with("class:") {
|
||||
else if name.starts_with("class:") {
|
||||
let name = name.replacen("class:", "", 1);
|
||||
if mode == Mode::Ssr {
|
||||
// handled separately because they need to be merged
|
||||
} else {
|
||||
let value = node.value.as_ref().expect("class: attributes need values").as_ref();
|
||||
let value = node
|
||||
.value
|
||||
.as_ref()
|
||||
.expect("class: attributes need values")
|
||||
.as_ref();
|
||||
expressions.push(quote_spanned! {
|
||||
span => leptos_dom::class(#cx, #el_id.unchecked_ref(), #name, #value.into_class(#cx))
|
||||
});
|
||||
|
@ -599,7 +631,7 @@ fn child_to_tokens(
|
|||
expressions: &mut Vec<TokenStream>,
|
||||
multi: bool,
|
||||
mode: Mode,
|
||||
is_first_child: bool
|
||||
is_first_child: bool,
|
||||
) -> PrevSibChange {
|
||||
match node {
|
||||
Node::Element(node) => {
|
||||
|
@ -617,7 +649,7 @@ fn child_to_tokens(
|
|||
next_co_id,
|
||||
multi,
|
||||
mode,
|
||||
is_first_child
|
||||
is_first_child,
|
||||
)
|
||||
} else {
|
||||
PrevSibChange::Sib(element_to_tokens(
|
||||
|
@ -635,12 +667,34 @@ fn child_to_tokens(
|
|||
))
|
||||
}
|
||||
}
|
||||
Node::Text(node) => {
|
||||
block_to_tokens(cx, &node.value, node.value.span(), parent, prev_sib, next_sib, next_el_id, next_co_id, template, expressions, navigations, mode)
|
||||
}
|
||||
Node::Block(node) => {
|
||||
block_to_tokens(cx, &node.value, node.value.span(), parent, prev_sib, next_sib, next_el_id, next_co_id, template, expressions, navigations, mode)
|
||||
}
|
||||
Node::Text(node) => block_to_tokens(
|
||||
cx,
|
||||
&node.value,
|
||||
node.value.span(),
|
||||
parent,
|
||||
prev_sib,
|
||||
next_sib,
|
||||
next_el_id,
|
||||
next_co_id,
|
||||
template,
|
||||
expressions,
|
||||
navigations,
|
||||
mode,
|
||||
),
|
||||
Node::Block(node) => block_to_tokens(
|
||||
cx,
|
||||
&node.value,
|
||||
node.value.span(),
|
||||
parent,
|
||||
prev_sib,
|
||||
next_sib,
|
||||
next_el_id,
|
||||
next_co_id,
|
||||
template,
|
||||
expressions,
|
||||
navigations,
|
||||
mode,
|
||||
),
|
||||
_ => panic!("unexpected child node type"),
|
||||
}
|
||||
}
|
||||
|
@ -669,7 +723,7 @@ fn block_to_tokens(
|
|||
syn::Lit::Float(f) => Some(f.base10_digits().to_string()),
|
||||
_ => None,
|
||||
},
|
||||
_ => None
|
||||
_ => None,
|
||||
};
|
||||
let current: Option<Ident> = None;
|
||||
|
||||
|
@ -804,7 +858,7 @@ fn component_to_tokens(
|
|||
next_co_id: &mut usize,
|
||||
multi: bool,
|
||||
mode: Mode,
|
||||
is_first_child: bool
|
||||
is_first_child: bool,
|
||||
) -> PrevSibChange {
|
||||
let create_component = create_component(cx, node, mode);
|
||||
let span = node.name.span();
|
||||
|
@ -892,11 +946,13 @@ fn component_to_tokens(
|
|||
|
||||
match current {
|
||||
Some(el) => PrevSibChange::Sib(el),
|
||||
None => if is_first_child {
|
||||
PrevSibChange::Parent
|
||||
} else {
|
||||
PrevSibChange::Skip
|
||||
},
|
||||
None => {
|
||||
if is_first_child {
|
||||
PrevSibChange::Parent
|
||||
} else {
|
||||
PrevSibChange::Skip
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -57,6 +57,13 @@ impl Runtime {
|
|||
Self::default()
|
||||
}
|
||||
|
||||
pub fn raw_scope_and_disposer(&'static self) -> (Scope, ScopeDisposer) {
|
||||
let id = { self.scopes.borrow_mut().insert(Default::default()) };
|
||||
let scope = Scope { runtime: self, id };
|
||||
let disposer = ScopeDisposer(Box::new(move || scope.dispose()));
|
||||
(scope, disposer)
|
||||
}
|
||||
|
||||
pub fn run_scope_undisposed<T>(
|
||||
&'static self,
|
||||
f: impl FnOnce(Scope) -> T,
|
||||
|
|
|
@ -13,10 +13,22 @@ use std::{future::Future, pin::Pin};
|
|||
/// This should usually only be used once, at the root of an application, because its reactive
|
||||
/// values will not have access to values created under another `create_scope`.
|
||||
pub fn create_scope(f: impl FnOnce(Scope) + 'static) -> ScopeDisposer {
|
||||
// TODO leak
|
||||
let runtime = Box::leak(Box::new(Runtime::new()));
|
||||
runtime.run_scope_undisposed(f, None).2
|
||||
}
|
||||
|
||||
#[must_use = "Scope will leak memory if the disposer function is never called"]
|
||||
/// Creates a new reactive system and root reactive scope, and returns them.
|
||||
///
|
||||
/// This should usually only be used once, at the root of an application, because its reactive
|
||||
/// values will not have access to values created under another `create_scope`.
|
||||
pub fn raw_scope_and_disposer() -> (Scope, ScopeDisposer) {
|
||||
// TODO leak
|
||||
let runtime = Box::leak(Box::new(Runtime::new()));
|
||||
runtime.raw_scope_and_disposer()
|
||||
}
|
||||
|
||||
/// Creates a temporary scope, runs the given function, disposes of the scope,
|
||||
/// and returns the value returned from the function. This is very useful for short-lived
|
||||
/// applications like SSR, where actual reactivity is not required beyond the end
|
||||
|
|
|
@ -264,8 +264,8 @@ where
|
|||
S: Clone + ServerFn,
|
||||
{
|
||||
#[cfg(feature = "ssr")]
|
||||
let c = |args: &S| S::call_fn(args.clone());
|
||||
let c = move |args: &S| S::call_fn(args.clone(), cx);
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
let c = |args: &S| S::call_fn_client(args.clone());
|
||||
let c = move |args: &S| S::call_fn_client(args.clone(), cx);
|
||||
create_action(cx, c).using_server_fn::<S>()
|
||||
}
|
||||
|
|
|
@ -31,18 +31,21 @@
|
|||
//! on the server (i.e., when you have an `ssr` feature in your crate that is enabled).
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! # use leptos_reactive::*;
|
||||
//! #[server(ReadFromDB)]
|
||||
//! async fn read_posts(how_many: usize, query: String) -> Result<Vec<Posts>, ServerFnError> {
|
||||
//! async fn read_posts(cx: Scope, how_many: usize, query: String) -> Result<Vec<Posts>, ServerFnError> {
|
||||
//! // do some server-only work here to access the database
|
||||
//! let posts = ...;
|
||||
//! Ok(posts)
|
||||
//! }
|
||||
//!
|
||||
//! // call the function
|
||||
//! # run_scope(|cx| {
|
||||
//! spawn_local(async {
|
||||
//! let posts = read_posts(3, "my search".to_string()).await;
|
||||
//! log::debug!("posts = {posts{:#?}");
|
||||
//! })
|
||||
//! # });
|
||||
//! ```
|
||||
//!
|
||||
//! If you call this function from the client, it will serialize the function arguments and `POST`
|
||||
|
@ -55,9 +58,14 @@
|
|||
//! - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done
|
||||
//! inside the function body can’t fail, the processes of serialization/deserialization and the
|
||||
//! network call are fallible.
|
||||
//! - **Server function arguments and return types must be [Serializable](leptos_reactive::Serializable).**
|
||||
//! - **Return types must be [Serializable](leptos_reactive::Serializable).**
|
||||
//! This should be fairly obvious: we have to serialize arguments to send them to the server, and we
|
||||
//! need to deserialize the result to return it to the client.
|
||||
//! - **Arguments must be implement [serde::Serialize].** They are serialized as an `application/x-www-form-urlencoded`
|
||||
//! form data using [`serde_urlencoded`](https://docs.rs/serde_urlencoded/latest/serde_urlencoded/).
|
||||
//! - **The [Scope](leptos_reactive::Scope) comes from the server.** Optionally, the first argument of a server function
|
||||
//! can be a Leptos [Scope](leptos_reactive::Scope). This scope can be used to inject dependencies like the HTTP request
|
||||
//! or response or other server-only dependencies, but it does *not* have access to reactive state that exists in the client.
|
||||
|
||||
pub use form_urlencoded;
|
||||
use leptos_reactive::*;
|
||||
|
@ -77,8 +85,9 @@ use std::{
|
|||
};
|
||||
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
type ServerFnTraitObj =
|
||||
dyn Fn(&[u8]) -> Pin<Box<dyn Future<Output = Result<String, ServerFnError>>>> + Send + Sync;
|
||||
type ServerFnTraitObj = dyn Fn(Scope, &[u8]) -> Pin<Box<dyn Future<Output = Result<String, ServerFnError>>>>
|
||||
+ Send
|
||||
+ Sync;
|
||||
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
lazy_static::lazy_static! {
|
||||
|
@ -163,18 +172,24 @@ where
|
|||
|
||||
/// Runs the function on the server.
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
fn call_fn(self) -> Pin<Box<dyn Future<Output = Result<Self::Output, ServerFnError>> + Send>>;
|
||||
fn call_fn(
|
||||
self,
|
||||
cx: Scope,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Self::Output, ServerFnError>>>>;
|
||||
|
||||
/// Runs the function on the client by sending an HTTP request to the server.
|
||||
#[cfg(any(not(feature = "ssr"), doc))]
|
||||
fn call_fn_client(self) -> Pin<Box<dyn Future<Output = Result<Self::Output, ServerFnError>>>>;
|
||||
fn call_fn_client(
|
||||
self,
|
||||
cx: Scope,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Self::Output, ServerFnError>>>>;
|
||||
|
||||
/// Registers the server function, allowing the server to query it by URL.
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
fn register() -> Result<(), ServerFnError> {
|
||||
// create the handler for this server function
|
||||
// takes a String -> returns its async value
|
||||
let run_server_fn = Arc::new(|data: &[u8]| {
|
||||
let run_server_fn = Arc::new(|cx: Scope, data: &[u8]| {
|
||||
// decode the args
|
||||
let value = serde_urlencoded::from_bytes::<Self>(data)
|
||||
.map_err(|e| ServerFnError::Deserialization(e.to_string()));
|
||||
|
@ -185,7 +200,7 @@ where
|
|||
};
|
||||
|
||||
// call the function
|
||||
let result = match value.call_fn().await {
|
||||
let result = match value.call_fn(cx).await {
|
||||
Ok(r) => r,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
|
|
@ -274,8 +274,8 @@ where
|
|||
S: Clone + ServerFn,
|
||||
{
|
||||
#[cfg(feature = "ssr")]
|
||||
let c = |args: &S| S::call_fn(args.clone());
|
||||
let c = move |args: &S| S::call_fn(args.clone(), cx);
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
let c = |args: &S| S::call_fn_client(args.clone());
|
||||
let c = move |args: &S| S::call_fn_client(args.clone(), cx);
|
||||
create_multi_action(cx, c).using_server_fn::<S>()
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue