feat: make hooks free-functions

This commit is contained in:
Jonathan Kelley 2021-07-27 11:28:05 -04:00
parent f782e14211
commit e5c88fe3a4
13 changed files with 373 additions and 323 deletions

View file

@ -84,6 +84,7 @@ If you know React, then you already know Dioxus.
- Starting a new app takes zero templates or special tools - get a new app running in just seconds.
- Desktop apps running natively (no Electron!) in less than 10 lines of code.
- The most ergonomic and powerful state management of any Rust UI toolkit.
- Multithreaded asynchronous coroutine scheduler for powerful async code.
- And more! Read the full release post here.
## Get Started with...

View file

@ -12,7 +12,7 @@ pub static App: FC<()> = |cx| {
let mut direction = use_state(cx, || 1);
let (async_count, dir) = (count.for_async(), *direction);
let (task, _result) = cx.use_task(move || async move {
let (task, _result) = use_task(cx, move || async move {
loop {
gloo_timers::future::TimeoutFuture::new(250).await;
*async_count.get_mut() += dir;

View file

@ -32,13 +32,13 @@ static App: FC<()> = |cx| {
let p2 = use_state(cx, || 0);
let (mut p1_async, mut p2_async) = (p1.for_async(), p2.for_async());
let (p1_handle, _) = cx.use_task(|| async move {
let (p1_handle, _) = use_task(cx, || async move {
loop {
*p1_async.get_mut() += 1;
async_std::task::sleep(std::time::Duration::from_millis(75)).await;
}
});
let (p2_handle, _) = cx.use_task(|| async move {
let (p2_handle, _) = use_task(cx, || async move {
loop {
*p2_async.get_mut() += 1;
async_std::task::sleep(std::time::Duration::from_millis(100)).await;

View file

@ -15,7 +15,8 @@ struct DogApi {
const ENDPOINT: &str = "https://dog.ceo/api/breeds/image/random";
pub static Example: FC<()> = |cx| {
let doggo = cx.use_suspense(
let doggo = use_suspense(
cx,
|| surf::get(ENDPOINT).recv_json::<DogApi>(),
|cx, res| match res {
Ok(res) => rsx!(in cx, img { src: "{res.message}" }),

View file

@ -30,7 +30,7 @@ pub static Example: FC<()> = |cx| {
// Tasks are 'static, so we need to copy relevant items in
let (async_count, dir) = (count.for_async(), *direction);
let (task, result) = cx.use_task(move || async move {
let (task, result) = use_task(cx, move || async move {
// Count infinitely!
loop {
gloo_timers::future::TimeoutFuture::new(250).await;

View file

@ -21,7 +21,7 @@ const App: FC<()> = |cx| {
};
const Task: FC<()> = |cx| {
let (task, res) = cx.use_task(|| async { true });
let (task, res) = use_task(cx, || async { true });
// task.pause();
// task.restart();
// task.stop();
@ -29,7 +29,7 @@ const Task: FC<()> = |cx| {
//
let _s = cx.use_task(|| async { "hello world".to_string() });
let _s = use_task(cx, || async { "hello world".to_string() });
todo!()
};

View file

@ -4,7 +4,8 @@ use dioxus_core::prelude::*;
fn App(cx: Context<()>) -> DomTree {
//
let vak = cx.use_suspense(
let vak = use_suspense(
cx,
|| async {},
|c, res| {
//

View file

@ -111,7 +111,7 @@ impl<'src, P> Context<'src, P> {
Rc::new(move || cb(id))
}
fn prepare_update(&self) -> Rc<dyn Fn(ScopeId)> {
pub fn prepare_update(&self) -> Rc<dyn Fn(ScopeId)> {
self.scope.vdom.schedule_update()
}
@ -155,8 +155,82 @@ impl<'src, P> Context<'src, P> {
}))
}
/// `submit_task` will submit the future to be polled.
///
/// This is useful when you have some async task that needs to be progressed.
///
/// This method takes ownership over the task you've provided, and must return (). This means any work that needs to
/// happen must occur within the future or scheduled for after the future completes (through schedule_update )
///
/// ## Explanation
/// Dioxus will step its internal event loop if the future returns if the future completes while waiting.
///
/// Tasks can't return anything, but they can be controlled with the returned handle
///
/// Tasks will only run until the component renders again. Because `submit_task` is valid for the &'src lifetime, it
/// is considered "stable"
///
///
///
pub fn submit_task(&self, task: FiberTask) -> TaskHandle {
self.scope.vdom.submit_task(task)
}
/// Add a state globally accessible to child components via tree walking
pub fn add_shared_state<T: 'static>(self, val: T) -> Option<Rc<dyn Any>> {
self.scope
.shared_contexts
.borrow_mut()
.insert(TypeId::of::<T>(), Rc::new(val))
}
/// Walk the tree to find a shared state with the TypeId of the generic type
///
pub fn consume_shared_state<T: 'static>(self) -> Option<Rc<T>> {
let mut scope = Some(self.scope);
let mut par = None;
let ty = TypeId::of::<T>();
while let Some(inner) = scope {
log::debug!(
"Searching {:#?} for valid shared_context",
inner.our_arena_idx
);
let shared_ctx = {
let shared_contexts = inner.shared_contexts.borrow();
log::debug!(
"This component has {} shared contexts",
shared_contexts.len()
);
shared_contexts.get(&ty).map(|f| f.clone())
};
if let Some(shared_cx) = shared_ctx {
log::debug!("found matching cx");
let rc = shared_cx
.clone()
.downcast::<T>()
.expect("Should not fail, already validated the type from the hashmap");
par = Some(rc);
break;
} else {
match inner.parent_idx {
Some(parent_id) => {
scope = unsafe { inner.vdom.get_scope(parent_id) };
}
None => break,
}
}
}
par
}
/// Store a value between renders
///
/// This is *the* foundational hook for all other hooks.
///
/// - Initializer: closure used to create the initial hook state
/// - Runner: closure used to output a value every time the hook is used
/// - Cleanup: closure used to teardown the hook once the dom is cleaned up
@ -200,312 +274,4 @@ Any function prefixed with "use" should not be called conditionally.
runner(self.scope.hooks.next::<State>().expect(ERR_MSG))
}
/// This hook enables the ability to expose state to children further down the VirtualDOM Tree.
///
/// This is a hook, so it may not be called conditionally!
///
/// The init method is ran *only* on first use, otherwise it is ignored. However, it uses hooks (ie `use`)
/// so don't put it in a conditional.
///
/// When the component is dropped, so is the context. Be aware of this behavior when consuming
/// the context via Rc/Weak.
///
///
///
pub fn use_provide_context<T, F>(self, init: F) -> &'src Rc<T>
where
T: 'static,
F: FnOnce() -> T,
{
let ty = TypeId::of::<T>();
let contains_key = self.scope.shared_contexts.borrow().contains_key(&ty);
let is_initialized = self.use_hook(
|_| false,
|s| {
let i = s.clone();
*s = true;
i
},
|_| {},
);
match (is_initialized, contains_key) {
// Do nothing, already initialized and already exists
(true, true) => {}
// Needs to be initialized
(false, false) => {
log::debug!("Initializing context...");
let initialized = Rc::new(init());
let p = self
.scope
.shared_contexts
.borrow_mut()
.insert(ty, initialized);
log::info!(
"There are now {} shared contexts for scope {:?}",
self.scope.shared_contexts.borrow().len(),
self.scope.our_arena_idx,
);
}
_ => debug_assert!(false, "Cannot initialize two contexts of the same type"),
};
self.use_context::<T>()
}
/// There are hooks going on here!
pub fn use_context<T: 'static>(self) -> &'src Rc<T> {
self.try_use_context().unwrap()
}
/// Uses a context, storing the cached value around
///
/// If a context is not found on the first search, then this call will be "dud", always returning "None" even if a
/// context was added later. This allows using another hook as a fallback
///
pub fn try_use_context<T: 'static>(self) -> Option<&'src Rc<T>> {
struct UseContextHook<C> {
par: Option<Rc<C>>,
}
self.use_hook(
move |_| {
let mut scope = Some(self.scope);
let mut par = None;
let ty = TypeId::of::<T>();
while let Some(inner) = scope {
log::debug!(
"Searching {:#?} for valid shared_context",
inner.our_arena_idx
);
let shared_ctx = {
let shared_contexts = inner.shared_contexts.borrow();
log::debug!(
"This component has {} shared contexts",
shared_contexts.len()
);
shared_contexts.get(&ty).map(|f| f.clone())
};
if let Some(shared_cx) = shared_ctx {
log::debug!("found matching cx");
let rc = shared_cx
.clone()
.downcast::<T>()
.expect("Should not fail, already validated the type from the hashmap");
par = Some(rc);
break;
} else {
match inner.parent_idx {
Some(parent_id) => {
scope = unsafe { inner.vdom.get_scope(parent_id) };
}
None => break,
}
}
}
//
UseContextHook { par }
},
move |hook| hook.par.as_ref(),
|_| {},
)
}
/// `submit_task` will submit the future to be polled.
///
/// This is useful when you have some async task that needs to be progressed.
///
/// This method takes ownership over the task you've provided, and must return (). This means any work that needs to
/// happen must occur within the future or scheduled for after the future completes (through schedule_update )
///
/// ## Explanation
/// Dioxus will step its internal event loop if the future returns if the future completes while waiting.
///
/// Tasks can't return anything, but they can be controlled with the returned handle
///
/// Tasks will only run until the component renders again. Because `submit_task` is valid for the &'src lifetime, it
/// is considered "stable"
///
///
///
pub fn submit_task(&self, task: FiberTask) -> TaskHandle {
self.scope.vdom.submit_task(task)
}
/// Awaits the given task, forcing the component to re-render when the value is ready.
///
///
///
///
pub fn use_task<Out, Fut, Init>(
self,
task_initializer: Init,
) -> (&'src TaskHandle, &'src Option<Out>)
where
Out: 'static,
Fut: Future<Output = Out> + 'static,
Init: FnOnce() -> Fut + 'src,
{
struct TaskHook<T> {
handle: TaskHandle,
task_dump: Rc<RefCell<Option<T>>>,
value: Option<T>,
}
// whenever the task is complete, save it into th
self.use_hook(
move |hook_idx| {
let task_fut = task_initializer();
let task_dump = Rc::new(RefCell::new(None));
let slot = task_dump.clone();
let updater = self.prepare_update();
let update_id = self.get_scope_id();
let originator = self.scope.our_arena_idx.clone();
let handle = self.submit_task(Box::pin(task_fut.then(move |output| async move {
*slot.as_ref().borrow_mut() = Some(output);
updater(update_id);
EventTrigger {
event: VirtualEvent::AsyncEvent { hook_idx },
originator,
priority: EventPriority::Low,
real_node_id: None,
}
})));
TaskHook {
task_dump,
value: None,
handle,
}
},
|hook| {
if let Some(val) = hook.task_dump.as_ref().borrow_mut().take() {
hook.value = Some(val);
}
(&hook.handle, &hook.value)
},
|_| {},
)
}
}
pub(crate) struct SuspenseHook {
pub value: Rc<RefCell<Option<Box<dyn Any>>>>,
pub callback: SuspendedCallback,
pub dom_node_id: Rc<Cell<Option<ElementId>>>,
}
type SuspendedCallback = Box<dyn for<'a> Fn(SuspendedContext<'a>) -> DomTree<'a>>;
impl<'src, P> Context<'src, P> {
/// Asynchronously render new nodes once the given future has completed.
///
/// # Easda
///
///
///
///
/// # Example
///
///
pub fn use_suspense<Out, Fut, Cb>(
self,
task_initializer: impl FnOnce() -> Fut,
user_callback: Cb,
) -> DomTree<'src>
where
Fut: Future<Output = Out> + 'static,
Out: 'static,
Cb: for<'a> Fn(SuspendedContext<'a>, &Out) -> DomTree<'a> + 'static,
{
self.use_hook(
move |hook_idx| {
let value = Rc::new(RefCell::new(None));
let dom_node_id = Rc::new(empty_cell());
let domnode = dom_node_id.clone();
let slot = value.clone();
let callback: SuspendedCallback = Box::new(move |ctx: SuspendedContext| {
let v: std::cell::Ref<Option<Box<dyn Any>>> = slot.as_ref().borrow();
match v.as_ref() {
Some(a) => {
let v: &dyn Any = a.as_ref();
let real_val = v.downcast_ref::<Out>().unwrap();
user_callback(ctx, real_val)
}
None => {
//
Some(VNode {
dom_id: empty_cell(),
key: None,
kind: VNodeKind::Suspended {
node: domnode.clone(),
},
})
}
}
});
let originator = self.scope.our_arena_idx.clone();
let task_fut = task_initializer();
let domnode = dom_node_id.clone();
let slot = value.clone();
self.submit_task(Box::pin(task_fut.then(move |output| async move {
// When the new value arrives, set the hooks internal slot
// Dioxus will call the user's callback to generate new nodes outside of the diffing system
*slot.borrow_mut() = Some(Box::new(output) as Box<dyn Any>);
EventTrigger {
event: VirtualEvent::SuspenseEvent { hook_idx, domnode },
originator,
priority: EventPriority::Low,
real_node_id: None,
}
})));
SuspenseHook {
value,
callback,
dom_node_id,
}
},
move |hook| {
let cx = Context {
scope: &self.scope,
props: &(),
};
let csx = SuspendedContext { inner: cx };
(&hook.callback)(csx)
},
|_| {},
)
}
}
pub struct SuspendedContext<'a> {
pub(crate) inner: Context<'a, ()>,
}
impl<'src> SuspendedContext<'src> {
pub fn render<F: FnOnce(NodeFactory<'src>) -> VNode<'src>>(
self,
lazy_nodes: LazyNodes<'src, F>,
) -> DomTree<'src> {
let scope_ref = self.inner.scope;
Some(lazy_nodes.into_vnode(NodeFactory { scope: scope_ref }))
}
}

256
packages/core/src/hooks.rs Normal file
View file

@ -0,0 +1,256 @@
//! Built-in hooks
//!
//! This module contains all the low-level built-in hooks that require 1st party support to work.
//!
//! Hooks:
//! - use_hook
//! - use_state_provider
//! - use_state_consumer
//! - use_task
//! - use_suspense
use crate::innerlude::*;
use futures_util::FutureExt;
use std::{
any::{Any, TypeId},
cell::{Cell, RefCell},
future::Future,
rc::Rc,
};
/// This hook enables the ability to expose state to children further down the VirtualDOM Tree.
///
/// This is a hook, so it may not be called conditionally!
///
/// The init method is ran *only* on first use, otherwise it is ignored. However, it uses hooks (ie `use`)
/// so don't put it in a conditional.
///
/// When the component is dropped, so is the context. Be aware of this behavior when consuming
/// the context via Rc/Weak.
///
///
///
pub fn use_provide_state<'src, Pr, T, F>(cx: Context<'src, Pr>, init: F) -> &'src Rc<T>
where
T: 'static,
F: FnOnce() -> T,
{
let ty = TypeId::of::<T>();
let contains_key = cx.scope.shared_contexts.borrow().contains_key(&ty);
let is_initialized = cx.use_hook(
|_| false,
|s| {
let i = s.clone();
*s = true;
i
},
|_| {},
);
match (is_initialized, contains_key) {
// Do nothing, already initialized and already exists
(true, true) => {}
// Needs to be initialized
(false, false) => {
log::debug!("Initializing context...");
cx.add_shared_state(init());
log::info!(
"There are now {} shared contexts for scope {:?}",
cx.scope.shared_contexts.borrow().len(),
cx.scope.our_arena_idx,
);
}
_ => debug_assert!(false, "Cannot initialize two contexts of the same type"),
};
use_consume_state::<T, _>(cx)
}
/// There are hooks going on here!
pub fn use_consume_state<'src, T: 'static, P>(cx: Context<'src, P>) -> &'src Rc<T> {
use_try_consume_state::<T, _>(cx).unwrap()
}
/// Uses a context, storing the cached value around
///
/// If a context is not found on the first search, then this call will be "dud", always returning "None" even if a
/// context was added later. This allows using another hook as a fallback
///
pub fn use_try_consume_state<'src, T: 'static, P>(cx: Context<'src, P>) -> Option<&'src Rc<T>> {
struct UseContextHook<C>(Option<Rc<C>>);
cx.use_hook(
move |_| UseContextHook(cx.consume_shared_state::<T>()),
move |hook| hook.0.as_ref(),
|_| {},
)
}
/// Awaits the given task, forcing the component to re-render when the value is ready.
///
///
///
///
pub fn use_task<'src, Out, Fut, Init, P>(
cx: Context<'src, P>,
task_initializer: Init,
) -> (&'src TaskHandle, &'src Option<Out>)
where
Out: 'static,
Fut: Future<Output = Out> + 'static,
Init: FnOnce() -> Fut + 'src,
{
struct TaskHook<T> {
handle: TaskHandle,
task_dump: Rc<RefCell<Option<T>>>,
value: Option<T>,
}
// whenever the task is complete, save it into th
cx.use_hook(
move |hook_idx| {
let task_fut = task_initializer();
let task_dump = Rc::new(RefCell::new(None));
let slot = task_dump.clone();
let updater = cx.prepare_update();
let update_id = cx.get_scope_id();
let originator = cx.scope.our_arena_idx.clone();
let handle = cx.submit_task(Box::pin(task_fut.then(move |output| async move {
*slot.as_ref().borrow_mut() = Some(output);
updater(update_id);
EventTrigger {
event: VirtualEvent::AsyncEvent { hook_idx },
originator,
priority: EventPriority::Low,
real_node_id: None,
}
})));
TaskHook {
task_dump,
value: None,
handle,
}
},
|hook| {
if let Some(val) = hook.task_dump.as_ref().borrow_mut().take() {
hook.value = Some(val);
}
(&hook.handle, &hook.value)
},
|_| {},
)
}
/// Asynchronously render new nodes once the given future has completed.
///
/// # Easda
///
///
///
///
/// # Example
///
///
pub fn use_suspense<'src, Out, Fut, Cb, P>(
cx: Context<'src, P>,
task_initializer: impl FnOnce() -> Fut,
user_callback: Cb,
) -> DomTree<'src>
where
Fut: Future<Output = Out> + 'static,
Out: 'static,
Cb: for<'a> Fn(SuspendedContext<'a>, &Out) -> DomTree<'a> + 'static,
{
cx.use_hook(
move |hook_idx| {
let value = Rc::new(RefCell::new(None));
let dom_node_id = Rc::new(empty_cell());
let domnode = dom_node_id.clone();
let slot = value.clone();
let callback: SuspendedCallback = Box::new(move |ctx: SuspendedContext| {
let v: std::cell::Ref<Option<Box<dyn Any>>> = slot.as_ref().borrow();
match v.as_ref() {
Some(a) => {
let v: &dyn Any = a.as_ref();
let real_val = v.downcast_ref::<Out>().unwrap();
user_callback(ctx, real_val)
}
None => {
//
Some(VNode {
dom_id: empty_cell(),
key: None,
kind: VNodeKind::Suspended {
node: domnode.clone(),
},
})
}
}
});
let originator = cx.scope.our_arena_idx.clone();
let task_fut = task_initializer();
let domnode = dom_node_id.clone();
let slot = value.clone();
cx.submit_task(Box::pin(task_fut.then(move |output| async move {
// When the new value arrives, set the hooks internal slot
// Dioxus will call the user's callback to generate new nodes outside of the diffing system
*slot.borrow_mut() = Some(Box::new(output) as Box<dyn Any>);
EventTrigger {
event: VirtualEvent::SuspenseEvent { hook_idx, domnode },
originator,
priority: EventPriority::Low,
real_node_id: None,
}
})));
SuspenseHook {
value,
callback,
dom_node_id,
}
},
move |hook| {
let cx = Context {
scope: &cx.scope,
props: &(),
};
let csx = SuspendedContext { inner: cx };
(&hook.callback)(csx)
},
|_| {},
)
}
pub(crate) struct SuspenseHook {
pub value: Rc<RefCell<Option<Box<dyn Any>>>>,
pub callback: SuspendedCallback,
pub dom_node_id: Rc<Cell<Option<ElementId>>>,
}
type SuspendedCallback = Box<dyn for<'a> Fn(SuspendedContext<'a>) -> DomTree<'a>>;
pub struct SuspendedContext<'a> {
pub(crate) inner: Context<'a, ()>,
}
impl<'src> SuspendedContext<'src> {
pub fn render<F: FnOnce(NodeFactory<'src>) -> VNode<'src>>(
self,
lazy_nodes: LazyNodes<'src, F>,
) -> DomTree<'src> {
let scope_ref = self.inner.scope;
Some(lazy_nodes.into_vnode(NodeFactory { scope: scope_ref }))
}
}

View file

@ -11,13 +11,14 @@
pub use crate::innerlude::{
format_args_f, html, rsx, Context, DioxusElement, DomEdit, DomTree, ElementId, EventPriority,
EventTrigger, LazyNodes, NodeFactory, Properties, RealDom, ScopeId, VNode, VNodeKind,
VirtualDom, VirtualEvent, FC,
EventTrigger, LazyNodes, NodeFactory, Properties, RealDom, ScopeId, SuspendedContext, VNode,
VNodeKind, VirtualDom, VirtualEvent, FC,
};
pub mod prelude {
pub use crate::component::{fc_to_builder, Fragment, Properties};
pub use crate::context::Context;
pub use crate::hooks::*;
pub use crate::innerlude::{DioxusElement, DomTree, LazyNodes, NodeFactory, FC};
pub use crate::nodes::VNode;
pub use crate::VirtualDom;
@ -36,6 +37,7 @@ pub(crate) mod innerlude {
pub use crate::events::*;
pub use crate::heuristics::*;
pub use crate::hooklist::*;
pub use crate::hooks::*;
pub use crate::nodes::*;
pub use crate::scope::*;
pub use crate::util::*;
@ -62,6 +64,7 @@ pub mod error;
pub mod events;
pub mod heuristics;
pub mod hooklist;
pub mod hooks;
pub mod nodes;
pub mod scope;
pub mod signals;

View file

@ -12,6 +12,7 @@ use std::{
cell::{Cell, RefCell},
fmt::{Arguments, Debug, Formatter},
marker::PhantomData,
mem::ManuallyDrop,
rc::Rc,
};
@ -127,6 +128,8 @@ pub struct VComponent<'src> {
pub(crate) comparator: Option<&'src dyn Fn(&VComponent) -> bool>,
pub(crate) drop_props: Option<&'src dyn FnOnce()>,
pub is_static: bool,
// a pointer into the bump arena (given by the 'src lifetime)
@ -335,15 +338,22 @@ impl<'a> NodeFactory<'a> {
// We don't want the fat part of the fat pointer
// This function does static dispatch so we don't need any VTable stuff
let props = self.bump().alloc(props);
let raw_props = props as *const P as *const ();
let raw_props = props as *mut P as *mut ();
let user_fc = component as *const ();
let comparator: Option<&dyn Fn(&VComponent) -> bool> = Some(self.bump().alloc_with(|| {
move |other: &VComponent| {
if user_fc == other.user_fc {
let real_other = unsafe { &*(other.raw_props as *const _ as *const P) };
let props_memoized = unsafe { props.memoize(&real_other) };
// Safety
// - We guarantee that FC<P> is the same by function pointer
// - Because FC<P> is the same, then P must be the same (even with generics)
// - Non-static P are autoderived to memoize as false
// - This comparator is only called on a corresponding set of bumpframes
let props_memoized = unsafe {
let real_other: &P = &*(other.raw_props as *const _ as *const P);
props.memoize(&real_other)
};
// It's only okay to memoize if there are no children and the props can be memoized
// Implementing memoize is unsafe and done automatically with the props trait
@ -357,6 +367,15 @@ impl<'a> NodeFactory<'a> {
}
}));
// create a closure to drop the props
let drop_props: Option<&dyn FnOnce()> = Some(self.bump().alloc_with(|| {
move || unsafe {
let real_other = raw_props as *mut _ as *mut P;
let b = BumpBox::from_raw(real_other);
std::mem::drop(b);
}
}));
let is_static = children.len() == 0 && P::IS_STATIC && key.is_none();
VNode {
@ -369,6 +388,7 @@ impl<'a> NodeFactory<'a> {
children,
caller: NodeFactory::create_component_caller(component, raw_props),
is_static,
drop_props,
ass_scope: Cell::new(None),
})),
}

View file

@ -19,6 +19,7 @@
//! This module includes just the barebones for a complete VirtualDOM API.
//! Additional functionality is defined in the respective files.
use crate::hooks::{SuspendedContext, SuspenseHook};
use crate::{arena::SharedResources, innerlude::*};
use std::any::Any;

View file

@ -31,7 +31,8 @@ const ENDPOINT: &str = "https://dog.ceo/api/breeds/image/random/";
static App: FC<()> = |cx| {
let state = use_state(cx, || 0);
let dog_node = cx.use_suspense(
let dog_node = use_suspense(
cx,
|| surf::get(ENDPOINT).recv_json::<DogApi>(),
|cx, res| match res {
Ok(res) => rsx!(in cx, img { src: "{res.message}" }),