Compare commits

..

2 Commits

Author SHA1 Message Date
Greg Johnston
61cd68314f cargo fmt 2023-07-02 17:23:00 -04:00
Martin
90470a6f2d Minor: Ran cargo clippy --fix. 2023-07-02 17:22:39 -04:00
12 changed files with 41 additions and 509 deletions

View File

@@ -62,8 +62,6 @@ pub async fn axum_extract(cx: Scope) -> Result<String, ServerFnError> {
These are relatively simple examples accessing basic data from the server. But you can use extractors to access things like headers, cookies, database connection pools, and more, using the exact same `extract()` pattern.
> Note: For now, the Axum `extract` function only supports extractors for which the state is `()`, i.e., you can't yet use it to extract `State(_)`. You can access `State(_)` by using a custom handler that extracts the state and then provides it via context. [Click here for an example](https://github.com/leptos-rs/leptos/blob/a5f73b441c079f9138102b3a7d8d4828f045448c/examples/session_auth_axum/src/main.rs#L91-L92).
## A Note about Data-Loading Patterns
Because Actix and (especially) Axum are built on the idea of a single round-trip HTTP request and response, you typically run extractors near the “top” of your application (i.e., before you start rendering) and use the extracted data to determine how that should be rendered. Before you render a `<button>`, you load all the data your app could need. And any given route handler needs to know all the data that will need to be extracted by that route.

View File

@@ -1,74 +1 @@
# Responses and Redirects
Extractors provide an easy way to access request data inside server functions. Leptos also provides a way to modify the HTTP response, using the `ResponseOptions` type (see docs for [Actix](https://docs.rs/leptos_actix/latest/leptos_actix/struct.ResponseOptions.html) or [Axum](https://docs.rs/leptos_axum/latest/leptos_axum/struct.ResponseOptions.html)) types and the `redirect` helper function (see docs for [Actix](https://docs.rs/leptos_actix/latest/leptos_actix/fn.redirect.html) or [Axum](https://docs.rs/leptos_axum/latest/leptos_axum/fn.redirect.html)).
## `ResponseOptions`
`ResponseOptions` is provided via context during the initial server rendering response and during any subsequent server function call. It allows you to easily set the status code for the HTTP response, or to add headers to the HTTP response, e.g., to set cookies.
```rust
#[server(TeaAndCookies)]
pub async fn tea_and_cookies(cx: Scope) -> Result<(), ServerFnError> {
use actix_web::{cookie::Cookie, http::header, http::header::HeaderValue};
use leptos_actix::ResponseOptions;
// pull ResponseOptions from context
let response = expect_context::<ResponseOptions>(cx);
// set the HTTP status code
response.set_status(StatusCode::IM_A_TEAPOT);
// set a cookie in the HTTP response
let mut cookie = Cookie::build("biscuits", "yes").finish();
if let Ok(cookie) = HeaderValue::from_str(&cookie.to_string()) {
res.insert_header(header::SET_COOKIE, cookie);
}
}
```
## `redirect`
One common modification to an HTTP response is to redirect to another page. The Actix and Axum integrations provide a `redirect` function to make this easy to do. `redirect` simply sets an HTTP status code of `302 Found` and sets the `Location` header.
Heres a simplified example from our [`session_auth_axum` example](https://github.com/leptos-rs/leptos/blob/a5f73b441c079f9138102b3a7d8d4828f045448c/examples/session_auth_axum/src/auth.rs#L154-L181).
```rust
#[server(Login, "/api")]
pub async fn login(
cx: Scope,
username: String,
password: String,
remember: Option<String>,
) -> Result<(), ServerFnError> {
// pull the DB pool and auth provider from context
let pool = pool(cx)?;
let auth = auth(cx)?;
// check whether the user exists
let user: User = User::get_from_username(username, &pool)
.await
.ok_or_else(|| {
ServerFnError::ServerError("User does not exist.".into())
})?;
// check whether the user has provided the correct password
match verify(password, &user.password)? {
// if the password is correct...
true => {
// log the user in
auth.login_user(user.id);
auth.remember_user(remember.is_some());
// and redirect to the home page
leptos_axum::redirect(cx, "/");
Ok(())
}
// if not, return an error
false => Err(ServerFnError::ServerError(
"Password does not match.".to_string(),
)),
}
}
```
This server function can then be used from your application. This `redirect` works well with the progressively-enhanced `<ActionForm/>` component: without JS/WASM, the server response will redirect because of the status code and header. With JS/WASM, the `<ActionForm/>` will detect the redirect in the server function response, and use client-side navigation to redirect to the new page.

View File

@@ -1315,12 +1315,6 @@ impl<B> From<Request<B>> for ExtractorHelper {
/// .map_err(|e| ServerFnError::ServerError("Could not extract method and query...".to_string()))
/// }
/// ```
///
/// > Note: For now, the Axum `extract` function only supports extractors for
/// which the state is `()`, i.e., you can't yet use it to extract `State(_)`.
/// You can access `State(_)` by using a custom handler that extracts the state
/// and then provides it via context.
/// [Click here for an example](https://github.com/leptos-rs/leptos/blob/a5f73b441c079f9138102b3a7d8d4828f045448c/examples/session_auth_axum/src/main.rs#L91-L92).
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub async fn extract<T, U>(
cx: Scope,

View File

@@ -271,9 +271,8 @@ where
let mut repr = ComponentRepr::new_with_id(name, id);
// disposed automatically when the parent scope is disposed
let (child, _) = cx.run_child_scope(|cx| {
cx.untrack_with_diagnostics(|| children_fn(cx).into_view(cx))
});
let (child, _) = cx
.run_child_scope(|cx| cx.untrack(|| children_fn(cx).into_view(cx)));
repr.children.push(child);

View File

@@ -822,11 +822,7 @@ fn apply_diff<T, EF, V>(
#[cfg(not(debug_assertions))]
parent.append_with_node_1(closing).unwrap();
} else {
#[cfg(debug_assertions)]
range.set_start_after(opening).unwrap();
#[cfg(not(debug_assertions))]
range.set_start_before(opening).unwrap();
range.set_end_before(closing).unwrap();
range.delete_contents().unwrap();

View File

@@ -733,24 +733,22 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
.collect::<SmallVec<[Cow<'static, str>; 4]>>(
);
let new_classes = classes
let mut new_classes = classes
.iter()
.flat_map(|classes| classes.split_whitespace());
if let Some(prev_classes) = prev_classes {
let new_classes =
new_classes.collect::<SmallVec<[_; 4]>>();
let mut old_classes = prev_classes
.iter()
.flat_map(|classes| classes.split_whitespace());
// Remove old classes
for prev_class in old_classes.clone() {
if !new_classes.iter().any(|c| c == &prev_class) {
if !new_classes.any(|c| c == prev_class) {
class_list.remove_1(prev_class).unwrap_or_else(
|err| {
panic!(
"failed to remove class \
"failed to add class \
`{prev_class}`, error: {err:#?}"
)
},
@@ -763,7 +761,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
if !old_classes.any(|c| c == class) {
class_list.add_1(class).unwrap_or_else(|err| {
panic!(
"failed to add class `{class}`, \
"failed to remove class `{class}`, \
error: {err:#?}"
)
});

View File

@@ -95,7 +95,6 @@ mod spawn_microtask;
mod stored_value;
pub mod suspense;
mod trigger;
mod watch;
pub use context::*;
pub use diagnostics::SpecialNonReactiveZone;
@@ -117,7 +116,6 @@ pub use spawn_microtask::*;
pub use stored_value::*;
pub use suspense::{GlobalSuspenseContext, SuspenseContext};
pub use trigger::*;
pub use watch::*;
mod macros {
macro_rules! debug_warn {

View File

@@ -1,12 +1,11 @@
#![forbid(unsafe_code)]
use crate::{
hydration::SharedContext,
node::{NodeId, ReactiveNode, ReactiveNodeState, ReactiveNodeType},
AnyComputation, AnyResource, Effect, Memo, MemoState, ReadSignal,
ResourceId, ResourceState, RwSignal, Scope, ScopeDisposer, ScopeId,
ScopeProperty, SerializableResource, SpecialNonReactiveZone, StoredValueId,
Trigger, UnserializableResource, WriteSignal,
ScopeProperty, SerializableResource, StoredValueId, Trigger,
UnserializableResource, WriteSignal,
};
use cfg_if::cfg_if;
use core::hash::BuildHasherDefault;
@@ -359,7 +358,6 @@ impl Debug for Runtime {
.finish()
}
}
/// Get the selected runtime from the thread-local set of runtimes. On the server,
/// this will return the correct runtime. In the browser, there should only be one runtime.
#[cfg_attr(
@@ -474,43 +472,6 @@ impl RuntimeId {
ret
}
#[cfg_attr(
any(debug_assertions, features = "ssr"),
instrument(level = "trace", skip_all,)
)]
#[inline(always)]
pub(crate) fn untrack<T>(
self,
f: impl FnOnce() -> T,
diagnostics: bool,
) -> T {
with_runtime(self, |runtime| {
let untracked_result;
if !diagnostics {
SpecialNonReactiveZone::enter();
}
let prev_observer =
SetObserverOnDrop(self, runtime.observer.take());
untracked_result = f();
runtime.observer.set(prev_observer.1);
std::mem::forget(prev_observer); // avoid Drop
if !diagnostics {
SpecialNonReactiveZone::exit();
}
untracked_result
})
.expect(
"tried to run untracked function in a runtime that has been \
disposed",
)
}
#[track_caller]
#[inline(always)] // only because it's placed here to fit in with the other create methods
pub(crate) fn create_trigger(self) -> Trigger {
@@ -720,81 +681,6 @@ impl RuntimeId {
)
}
pub(crate) fn watch<W, T>(
self,
deps: impl Fn() -> W + 'static,
callback: impl Fn(&W, Option<&W>, Option<T>) -> T + Clone + 'static,
immediate: bool,
) -> (NodeId, impl Fn() + Clone)
where
W: Clone + 'static,
T: 'static,
{
let cur_deps_value = Rc::new(RefCell::new(None::<W>));
let prev_deps_value = Rc::new(RefCell::new(None::<W>));
let prev_callback_value = Rc::new(RefCell::new(None::<T>));
let wrapped_callback = {
let cur_deps_value = Rc::clone(&cur_deps_value);
let prev_deps_value = Rc::clone(&prev_deps_value);
let prev_callback_value = Rc::clone(&prev_callback_value);
move || {
callback(
cur_deps_value.borrow().as_ref().expect(
"this will not be called before there is deps value",
),
prev_deps_value.borrow().as_ref(),
prev_callback_value.take(),
)
}
};
let effect_fn = {
let prev_callback_value = Rc::clone(&prev_callback_value);
move |did_run_before: Option<()>| {
let deps_value = deps();
let did_run_before = did_run_before.is_some();
if !immediate && !did_run_before {
prev_deps_value.replace(Some(deps_value));
return;
}
cur_deps_value.replace(Some(deps_value.clone()));
let callback_value =
Some(self.untrack(wrapped_callback.clone(), false));
prev_callback_value.replace(callback_value);
prev_deps_value.replace(Some(deps_value));
}
};
let id = self.create_concrete_effect(
Rc::new(RefCell::new(None::<()>)),
Rc::new(Effect {
f: effect_fn,
ty: PhantomData,
#[cfg(any(debug_assertions, feature = "ssr"))]
defined_at: std::panic::Location::caller(),
}),
);
(id, move || {
with_runtime(self, |runtime| {
runtime.nodes.borrow_mut().remove(id);
runtime.node_sources.borrow_mut().remove(id);
})
.expect(
"tried to stop a watch in a runtime that has been disposed",
);
})
}
#[track_caller]
#[inline(always)]
pub(crate) fn create_memo<T>(
@@ -942,13 +828,3 @@ impl std::hash::Hash for Runtime {
std::ptr::hash(&self, state);
}
}
struct SetObserverOnDrop(RuntimeId, Option<NodeId>);
impl Drop for SetObserverOnDrop {
fn drop(&mut self) {
_ = with_runtime(self.0, |rt| {
rt.observer.set(self.1);
});
}
}

View File

@@ -5,7 +5,8 @@ use crate::{
node::NodeId,
runtime::{with_runtime, RuntimeId},
suspense::StreamChunk,
PinnedFuture, ResourceId, StoredValueId, SuspenseContext,
PinnedFuture, ResourceId, SpecialNonReactiveZone, StoredValueId,
SuspenseContext,
};
use futures::stream::FuturesUnordered;
use std::{
@@ -208,14 +209,37 @@ impl Scope {
)]
#[inline(always)]
pub fn untrack<T>(&self, f: impl FnOnce() -> T) -> T {
self.runtime.untrack(f, false)
}
with_runtime(self.runtime, |runtime| {
let untracked_result;
#[doc(hidden)]
/// Suspends reactive tracking but keeps the diagnostic warnings for
/// untracked functions.
pub fn untrack_with_diagnostics<T>(&self, f: impl FnOnce() -> T) -> T {
self.runtime.untrack(f, true)
SpecialNonReactiveZone::enter();
let prev_observer =
SetObserverOnDrop(self.runtime, runtime.observer.take());
untracked_result = f();
runtime.observer.set(prev_observer.1);
std::mem::forget(prev_observer); // avoid Drop
SpecialNonReactiveZone::exit();
untracked_result
})
.expect(
"tried to run untracked function in a runtime that has been \
disposed",
)
}
}
struct SetObserverOnDrop(RuntimeId, Option<NodeId>);
impl Drop for SetObserverOnDrop {
fn drop(&mut self) {
_ = with_runtime(self.0, |rt| {
rt.observer.set(self.1);
});
}
}
@@ -325,27 +349,6 @@ impl Scope {
any(debug_assertions, features = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub(crate) fn remove_scope_property(&self, prop: ScopeProperty) {
_ = with_runtime(self.runtime, |runtime| {
let scopes = runtime.scopes.borrow();
if let Some(scope) = scopes.get(self.id) {
let mut scope = scope.borrow_mut();
if let Some(index) = scope.iter().position(|p| p == &prop) {
scope.swap_remove(index);
}
} else {
console_warn(
"tried to remove property to a scope that has been \
disposed",
)
}
})
}
#[cfg_attr(
any(debug_assertions, features = "ssr"),
instrument(level = "trace", skip_all,)
)]
/// Returns the the parent Scope, if any.
pub fn parent(&self) -> Option<Scope> {
match with_runtime(self.runtime, |runtime| {
@@ -389,7 +392,7 @@ slotmap::new_key_type! {
pub struct ScopeId;
}
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
#[derive(Debug)]
pub(crate) enum ScopeProperty {
Trigger(NodeId),
Signal(NodeId),

View File

@@ -68,7 +68,6 @@ use crate::{
/// // setting name only causes name to log, not count
/// set_name.set("Bob".into());
/// ```
#[track_caller]
pub fn create_slice<T, O, S>(
cx: Scope,
signal: RwSignal<T>,
@@ -86,7 +85,6 @@ where
/// Takes a memoized, read-only slice of a signal. This is equivalent to the
/// read-only half of [`create_slice`].
#[track_caller]
pub fn create_read_slice<T, O>(
cx: Scope,
signal: RwSignal<T>,
@@ -100,7 +98,6 @@ where
/// Creates a setter to access one slice of a signal. This is equivalent to the
/// write-only half of [`create_slice`].
#[track_caller]
pub fn create_write_slice<T, O>(
cx: Scope,
signal: RwSignal<T>,

View File

@@ -1,114 +0,0 @@
use crate::{Scope, ScopeProperty};
/// A version of [`create_effect`] that listens to any dependency that is accessed inside `deps` and returns
/// a stop handler.
/// The return value of `deps` is passed into `callback` as an argument together with the previous value.
/// Additionally the last return value of `callback` is provided as a third argument as is done in [`create_effect`].
///
/// ## Usage
///
/// ```
/// # use leptos_reactive::*;
/// # use log;
/// # create_scope(create_runtime(), |cx| {
/// let (num, set_num) = create_signal(cx, 0);
///
/// let stop = watch(
/// cx,
/// move || num.get(),
/// move |num, prev_num, _| {
/// log::debug!("Number: {}; Prev: {:?}", num, prev_num);
/// },
/// false,
/// );
///
/// set_num.set(1); // > "Number: 1; Prev: Some(0)"
///
/// stop(); // stop watching
///
/// set_num.set(2); // (nothing happens)
/// # }).dispose();
/// ```
///
/// The callback itself doesn't track any signal that is accessed within it.
///
/// ```
/// # use leptos_reactive::*;
/// # use log;
/// # create_scope(create_runtime(), |cx| {
/// let (num, set_num) = create_signal(cx, 0);
/// let (cb_num, set_cb_num) = create_signal(cx, 0);
///
/// watch(
/// cx,
/// move || num.get(),
/// move |num, _, _| {
/// log::debug!("Number: {}; Cb: {}", num, cb_num.get());
/// },
/// false,
/// );
///
/// set_num.set(1); // > "Number: 1; Cb: 0"
///
/// set_cb_num.set(1); // (nothing happens)
///
/// set_num.set(2); // > "Number: 2; Cb: 1"
/// # }).dispose();
/// ```
///
/// ## Immediate
///
/// If the final parameter `immediate` is true, the `callback` will run immediately.
/// If it's `false`, the `callback` will run only after
/// the first change is detected of any signal that is accessed in `deps`.
///
/// ```
/// # use leptos_reactive::*;
/// # use log;
/// # create_scope(create_runtime(), |cx| {
/// let (num, set_num) = create_signal(cx, 0);
///
/// watch(
/// cx,
/// move || num.get(),
/// move |num, prev_num, _| {
/// log::debug!("Number: {}; Prev: {:?}", num, prev_num);
/// },
/// true,
/// ); // > "Number: 0; Prev: None"
///
/// set_num.set(1); // > "Number: 1; Prev: Some(0)"
/// # }).dispose();
/// ```
#[cfg_attr(
any(debug_assertions, feature="ssr"),
instrument(
level = "trace",
skip_all,
fields(
scope = ?cx.id,
ty = %std::any::type_name::<T>()
)
)
)]
#[track_caller]
#[inline(always)]
pub fn watch<W, T>(
cx: Scope,
deps: impl Fn() -> W + 'static,
callback: impl Fn(&W, Option<&W>, Option<T>) -> T + Clone + 'static,
immediate: bool,
) -> impl Fn() + Clone
where
W: Clone + 'static,
T: 'static,
{
let (e, stop) = cx.runtime.watch(deps, callback, immediate);
let prop = ScopeProperty::Effect(e);
cx.push_scope_property(prop);
move || {
stop();
cx.remove_scope_property(prop);
}
}

View File

@@ -1,140 +0,0 @@
use leptos_reactive::{
create_runtime, create_scope, create_signal, watch, SignalGet, SignalSet,
};
use std::{cell::RefCell, rc::Rc};
#[test]
fn watch_runs() {
create_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, -1);
// simulate an arbitrary side effect
let b = Rc::new(RefCell::new(String::new()));
let stop = watch(
cx,
move || a.get(),
{
let b = b.clone();
move |a, prev_a, prev_ret| {
let formatted = format!(
"Value is {}; Prev is {:?}; Prev return is {:?}",
a, prev_a, prev_ret
);
*b.borrow_mut() = formatted;
a + 10
}
},
false,
);
assert_eq!(b.borrow().as_str(), "");
set_a.set(1);
assert_eq!(
b.borrow().as_str(),
"Value is 1; Prev is Some(-1); Prev return is None"
);
set_a.set(2);
assert_eq!(
b.borrow().as_str(),
"Value is 2; Prev is Some(1); Prev return is Some(11)"
);
stop();
*b.borrow_mut() = "nothing happened".to_string();
set_a.set(3);
assert_eq!(b.borrow().as_str(), "nothing happened");
})
.dispose()
}
#[test]
fn watch_runs_immediately() {
create_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, -1);
// simulate an arbitrary side effect
let b = Rc::new(RefCell::new(String::new()));
let _ = watch(
cx,
move || a.get(),
{
let b = b.clone();
move |a, prev_a, prev_ret| {
let formatted = format!(
"Value is {}; Prev is {:?}; Prev return is {:?}",
a, prev_a, prev_ret
);
*b.borrow_mut() = formatted;
a + 10
}
},
true,
);
assert_eq!(
b.borrow().as_str(),
"Value is -1; Prev is None; Prev return is None"
);
set_a.set(1);
assert_eq!(
b.borrow().as_str(),
"Value is 1; Prev is Some(-1); Prev return is Some(9)"
);
})
.dispose()
}
#[test]
fn watch_ignores_callback() {
create_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, -1);
let (b, set_b) = create_signal(cx, 0);
// simulate an arbitrary side effect
let s = Rc::new(RefCell::new(String::new()));
let _ = watch(
cx,
move || a.get(),
{
let s = s.clone();
move |a, _, _| {
let formatted =
format!("Value a is {}; Value b is {}", a, b.get());
*s.borrow_mut() = formatted;
}
},
false,
);
set_a.set(1);
assert_eq!(s.borrow().as_str(), "Value a is 1; Value b is 0");
*s.borrow_mut() = "nothing happened".to_string();
set_b.set(10);
assert_eq!(s.borrow().as_str(), "nothing happened");
set_a.set(2);
assert_eq!(s.borrow().as_str(), "Value a is 2; Value b is 10");
})
.dispose()
}