mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-22 20:23:09 +00:00
Feat: add hooks
This commit is contained in:
parent
ae1b8bbede
commit
c1b990b27c
6 changed files with 1300 additions and 0 deletions
68
packages/hooks/src/hooks/mod.rs
Normal file
68
packages/hooks/src/hooks/mod.rs
Normal file
|
@ -0,0 +1,68 @@
|
|||
mod use_context;
|
||||
mod use_effect;
|
||||
mod use_reducer;
|
||||
mod use_ref;
|
||||
mod use_state;
|
||||
|
||||
pub use use_context::*;
|
||||
pub use use_effect::*;
|
||||
pub use use_reducer::*;
|
||||
pub use use_ref::*;
|
||||
pub use use_state::*;
|
||||
|
||||
use crate::{HookUpdater, CURRENT_HOOK};
|
||||
use std::cell::RefCell;
|
||||
use std::ops::DerefMut;
|
||||
use std::rc::Rc;
|
||||
|
||||
pub fn use_hook<InternalHook: 'static, Output, Tear: FnOnce(&mut InternalHook) -> () + 'static>(
|
||||
initializer: impl FnOnce() -> InternalHook,
|
||||
runner: impl FnOnce(&mut InternalHook, HookUpdater) -> Output,
|
||||
tear_down: Tear,
|
||||
) -> Output {
|
||||
// Extract current hook
|
||||
let updater = CURRENT_HOOK.with(|hook_state_holder| {
|
||||
let mut hook_state_holder = hook_state_holder
|
||||
.try_borrow_mut()
|
||||
.expect("Nested hooks not supported");
|
||||
|
||||
let mut hook_state = hook_state_holder
|
||||
.as_mut()
|
||||
.expect("No current hook. Hooks can only be called inside function components");
|
||||
|
||||
// Determine which hook position we're at and increment for the next hook
|
||||
let hook_pos = hook_state.counter;
|
||||
hook_state.counter += 1;
|
||||
|
||||
// Initialize hook if this is the first call
|
||||
if hook_pos >= hook_state.hooks.len() {
|
||||
let initial_state = Rc::new(RefCell::new(initializer()));
|
||||
hook_state.hooks.push(initial_state.clone());
|
||||
hook_state.destroy_listeners.push(Box::new(move || {
|
||||
let mut is = initial_state.borrow_mut();
|
||||
let ihook = is.deref_mut();
|
||||
tear_down(ihook);
|
||||
}));
|
||||
}
|
||||
|
||||
let hook = hook_state
|
||||
.hooks
|
||||
.get(hook_pos)
|
||||
.expect("Not the same number of hooks. Hooks must not be called conditionally")
|
||||
.clone();
|
||||
|
||||
HookUpdater {
|
||||
hook,
|
||||
process_message: hook_state.process_message.clone(),
|
||||
}
|
||||
});
|
||||
|
||||
// Execute the actual hook closure we were given. Let it mutate the hook state and let
|
||||
// it create a callback that takes the mutable hook state.
|
||||
let mut hook = updater.hook.borrow_mut();
|
||||
let hook: &mut InternalHook = hook
|
||||
.downcast_mut()
|
||||
.expect("Incompatible hook type. Hooks must always be called in the same order");
|
||||
|
||||
runner(hook, updater.clone())
|
||||
}
|
469
packages/hooks/src/hooks/use_context.rs
Normal file
469
packages/hooks/src/hooks/use_context.rs
Normal file
|
@ -0,0 +1,469 @@
|
|||
// Naming this file use_context could be confusing. Not least to the IDE.
|
||||
use crate::{get_current_scope, use_hook};
|
||||
use std::any::TypeId;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::{Rc, Weak};
|
||||
use std::{iter, mem};
|
||||
use yew::html;
|
||||
use yew::html::{AnyScope, Scope};
|
||||
use yew::{Children, Component, ComponentLink, Html, Properties};
|
||||
|
||||
type ConsumerCallback<T> = Box<dyn Fn(Rc<T>)>;
|
||||
type UseContextOutput<T> = Option<Rc<T>>;
|
||||
|
||||
struct UseContext<T2: Clone + PartialEq + 'static> {
|
||||
provider_scope: Option<Scope<ContextProvider<T2>>>,
|
||||
current_context: Option<Rc<T2>>,
|
||||
callback: Option<Rc<ConsumerCallback<T2>>>,
|
||||
}
|
||||
|
||||
pub fn use_context<T: Clone + PartialEq + 'static>() -> UseContextOutput<T> {
|
||||
let scope = get_current_scope()
|
||||
.expect("No current Scope. `use_context` can only be called inside function components");
|
||||
|
||||
use_hook(
|
||||
// Initializer
|
||||
move || {
|
||||
let provider_scope = find_context_provider_scope::<T>(&scope);
|
||||
let current_context =
|
||||
with_provider_component(&provider_scope, |comp| Rc::clone(&comp.context));
|
||||
|
||||
UseContext {
|
||||
provider_scope,
|
||||
current_context,
|
||||
callback: None,
|
||||
}
|
||||
},
|
||||
// Runner
|
||||
|hook, updater| {
|
||||
// setup a listener for the context provider to update us
|
||||
let listener = move |ctx: Rc<T>| {
|
||||
updater.callback(move |state: &mut UseContext<T>| {
|
||||
state.current_context = Some(ctx);
|
||||
true
|
||||
});
|
||||
};
|
||||
hook.callback = Some(Rc::new(Box::new(listener)));
|
||||
|
||||
// Subscribe to the context provider with our callback
|
||||
let weak_cb = Rc::downgrade(hook.callback.as_ref().unwrap());
|
||||
with_provider_component(&hook.provider_scope, |comp| {
|
||||
comp.subscribe_consumer(weak_cb)
|
||||
});
|
||||
|
||||
// Return the current state
|
||||
hook.current_context.clone()
|
||||
},
|
||||
// Cleanup
|
||||
|hook| {
|
||||
if let Some(cb) = hook.callback.take() {
|
||||
drop(cb);
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Properties)]
|
||||
pub struct ContextProviderProps<T: Clone + PartialEq> {
|
||||
pub context: T,
|
||||
pub children: Children,
|
||||
}
|
||||
|
||||
pub struct ContextProvider<T: Clone + PartialEq + 'static> {
|
||||
context: Rc<T>,
|
||||
children: Children,
|
||||
consumers: RefCell<Vec<Weak<ConsumerCallback<T>>>>,
|
||||
}
|
||||
|
||||
impl<T: Clone + PartialEq> ContextProvider<T> {
|
||||
/// Add the callback to the subscriber list to be called whenever the context changes.
|
||||
/// The consumer is unsubscribed as soon as the callback is dropped.
|
||||
fn subscribe_consumer(&self, mut callback: Weak<ConsumerCallback<T>>) {
|
||||
// consumers re-subscribe on every render. Try to keep the subscriber list small by reusing dead slots.
|
||||
let mut consumers = self.consumers.borrow_mut();
|
||||
for cb in consumers.iter_mut() {
|
||||
if cb.strong_count() == 0 {
|
||||
mem::swap(cb, &mut callback);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// no slot to reuse, this is a new consumer
|
||||
consumers.push(callback);
|
||||
}
|
||||
|
||||
/// Notify all subscribed consumers and remove dropped consumers from the list.
|
||||
fn notify_consumers(&mut self) {
|
||||
let context = &self.context;
|
||||
self.consumers.borrow_mut().retain(|cb| {
|
||||
if let Some(cb) = cb.upgrade() {
|
||||
cb(Rc::clone(context));
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + PartialEq + 'static> Component for ContextProvider<T> {
|
||||
type Message = ();
|
||||
type Properties = ContextProviderProps<T>;
|
||||
|
||||
fn create(props: Self::Properties, _link: ComponentLink<Self>) -> Self {
|
||||
Self {
|
||||
children: props.children,
|
||||
context: Rc::new(props.context),
|
||||
consumers: RefCell::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, _msg: Self::Message) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn change(&mut self, props: Self::Properties) -> bool {
|
||||
let should_render = if self.children == props.children {
|
||||
false
|
||||
} else {
|
||||
self.children = props.children;
|
||||
true
|
||||
};
|
||||
|
||||
let new_context = Rc::new(props.context);
|
||||
if self.context != new_context {
|
||||
self.context = new_context;
|
||||
self.notify_consumers();
|
||||
}
|
||||
|
||||
should_render
|
||||
}
|
||||
|
||||
fn view(&self) -> Html {
|
||||
html! { <>{ self.children.clone() }</> }
|
||||
}
|
||||
}
|
||||
|
||||
fn find_context_provider_scope<T: Clone + PartialEq + 'static>(
|
||||
scope: &AnyScope,
|
||||
) -> Option<Scope<ContextProvider<T>>> {
|
||||
let expected_type_id = TypeId::of::<ContextProvider<T>>();
|
||||
iter::successors(Some(scope), |scope| scope.get_parent())
|
||||
.filter(|scope| scope.get_type_id() == &expected_type_id)
|
||||
.cloned()
|
||||
.map(AnyScope::downcast::<ContextProvider<T>>)
|
||||
.next()
|
||||
}
|
||||
|
||||
fn with_provider_component<T, F, R>(
|
||||
provider_scope: &Option<Scope<ContextProvider<T>>>,
|
||||
f: F,
|
||||
) -> Option<R>
|
||||
where
|
||||
T: Clone + PartialEq,
|
||||
F: FnOnce(&ContextProvider<T>) -> R,
|
||||
{
|
||||
provider_scope
|
||||
.as_ref()
|
||||
.and_then(|scope| scope.get_component().map(|comp| f(&*comp)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::hooks::{use_effect, use_ref, use_state};
|
||||
use crate::util::*;
|
||||
use crate::{FunctionComponent, FunctionProvider};
|
||||
use wasm_bindgen_test::*;
|
||||
use yew::prelude::*;
|
||||
|
||||
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn use_context_scoping_works() {
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct ExampleContext(String);
|
||||
struct UseContextFunctionOuter {}
|
||||
struct UseContextFunctionInner {}
|
||||
struct ExpectNoContextFunction {}
|
||||
type UseContextComponent = FunctionComponent<UseContextFunctionOuter>;
|
||||
type UseContextComponentInner = FunctionComponent<UseContextFunctionInner>;
|
||||
type ExpectNoContextComponent = FunctionComponent<ExpectNoContextFunction>;
|
||||
impl FunctionProvider for ExpectNoContextFunction {
|
||||
type TProps = ();
|
||||
|
||||
fn run(_props: &Self::TProps) -> Html {
|
||||
if use_context::<ExampleContext>().is_some() {
|
||||
yew::services::ConsoleService::log(&format!(
|
||||
"Context should be None here, but was {:?}!",
|
||||
use_context::<ExampleContext>().unwrap()
|
||||
));
|
||||
};
|
||||
return html! {
|
||||
<div></div>
|
||||
};
|
||||
}
|
||||
}
|
||||
impl FunctionProvider for UseContextFunctionOuter {
|
||||
type TProps = ();
|
||||
|
||||
fn run(_props: &Self::TProps) -> Html {
|
||||
type ExampleContextProvider = ContextProvider<ExampleContext>;
|
||||
return html! {
|
||||
<div>
|
||||
<ExampleContextProvider context=ExampleContext("wrong1".into())>
|
||||
<div>{"ignored"}</div>
|
||||
</ExampleContextProvider>
|
||||
<ExampleContextProvider context=ExampleContext("wrong2".into())>
|
||||
<ExampleContextProvider context=ExampleContext("correct".into())>
|
||||
<ExampleContextProvider context=ExampleContext("wrong1".into())>
|
||||
<div>{"ignored"}</div>
|
||||
</ExampleContextProvider>
|
||||
<UseContextComponentInner />
|
||||
</ExampleContextProvider>
|
||||
</ExampleContextProvider>
|
||||
<ExampleContextProvider context=ExampleContext("wrong3".into())>
|
||||
<div>{"ignored"}</div>
|
||||
</ExampleContextProvider>
|
||||
<ExpectNoContextComponent />
|
||||
</div>
|
||||
};
|
||||
}
|
||||
}
|
||||
impl FunctionProvider for UseContextFunctionInner {
|
||||
type TProps = ();
|
||||
|
||||
fn run(_props: &Self::TProps) -> Html {
|
||||
let context = use_context::<ExampleContext>();
|
||||
return html! {
|
||||
<div id="result">{ &context.unwrap().0 }</div>
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let app: App<UseContextComponent> = yew::App::new();
|
||||
app.mount(yew::utils::document().get_element_by_id("output").unwrap());
|
||||
let result: String = obtain_result_by_id("result");
|
||||
assert_eq!("correct", result);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn use_context_works_with_multiple_types() {
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct ContextA(u32);
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct ContextB(u32);
|
||||
|
||||
struct Test1Function;
|
||||
impl FunctionProvider for Test1Function {
|
||||
type TProps = ();
|
||||
|
||||
fn run(_props: &Self::TProps) -> Html {
|
||||
assert_eq!(use_context::<ContextA>(), Some(Rc::new(ContextA(2))));
|
||||
assert_eq!(use_context::<ContextB>(), Some(Rc::new(ContextB(1))));
|
||||
|
||||
return html! {};
|
||||
}
|
||||
}
|
||||
type Test1 = FunctionComponent<Test1Function>;
|
||||
|
||||
struct Test2Function;
|
||||
impl FunctionProvider for Test2Function {
|
||||
type TProps = ();
|
||||
|
||||
fn run(_props: &Self::TProps) -> Html {
|
||||
assert_eq!(use_context::<ContextA>(), Some(Rc::new(ContextA(0))));
|
||||
assert_eq!(use_context::<ContextB>(), Some(Rc::new(ContextB(1))));
|
||||
|
||||
return html! {};
|
||||
}
|
||||
}
|
||||
type Test2 = FunctionComponent<Test2Function>;
|
||||
|
||||
struct Test3Function;
|
||||
impl FunctionProvider for Test3Function {
|
||||
type TProps = ();
|
||||
|
||||
fn run(_props: &Self::TProps) -> Html {
|
||||
assert_eq!(use_context::<ContextA>(), Some(Rc::new(ContextA(0))));
|
||||
assert_eq!(use_context::<ContextB>(), None);
|
||||
|
||||
return html! {};
|
||||
}
|
||||
}
|
||||
type Test3 = FunctionComponent<Test3Function>;
|
||||
|
||||
struct Test4Function;
|
||||
impl FunctionProvider for Test4Function {
|
||||
type TProps = ();
|
||||
|
||||
fn run(_props: &Self::TProps) -> Html {
|
||||
assert_eq!(use_context::<ContextA>(), None);
|
||||
assert_eq!(use_context::<ContextB>(), None);
|
||||
|
||||
return html! {};
|
||||
}
|
||||
}
|
||||
type Test4 = FunctionComponent<Test4Function>;
|
||||
|
||||
struct TestFunction;
|
||||
impl FunctionProvider for TestFunction {
|
||||
type TProps = ();
|
||||
|
||||
fn run(_props: &Self::TProps) -> Html {
|
||||
type ContextAProvider = ContextProvider<ContextA>;
|
||||
type ContextBProvider = ContextProvider<ContextB>;
|
||||
|
||||
return html! {
|
||||
<div>
|
||||
<ContextAProvider context=ContextA(0)>
|
||||
<ContextBProvider context=ContextB(1)>
|
||||
<ContextAProvider context=ContextA(2)>
|
||||
<Test1/>
|
||||
</ContextAProvider>
|
||||
<Test2/>
|
||||
</ContextBProvider>
|
||||
<Test3/>
|
||||
</ContextAProvider>
|
||||
<Test4 />
|
||||
</div>
|
||||
};
|
||||
}
|
||||
}
|
||||
type TestComponent = FunctionComponent<TestFunction>;
|
||||
|
||||
let app: App<TestComponent> = yew::App::new();
|
||||
app.mount(yew::utils::document().get_element_by_id("output").unwrap());
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn use_context_update_works() {
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct MyContext(String);
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Properties)]
|
||||
struct RenderCounterProps {
|
||||
id: String,
|
||||
children: Children,
|
||||
}
|
||||
|
||||
struct RenderCounterFunction;
|
||||
impl FunctionProvider for RenderCounterFunction {
|
||||
type TProps = RenderCounterProps;
|
||||
|
||||
fn run(props: &Self::TProps) -> Html {
|
||||
let counter = use_ref(|| 0);
|
||||
*counter.borrow_mut() += 1;
|
||||
log::info!("Render counter {:?}", counter);
|
||||
return html! {
|
||||
<>
|
||||
<div id=props.id.clone()>
|
||||
{ format!("total: {}", counter.borrow()) }
|
||||
</div>
|
||||
{ props.children.clone() }
|
||||
</>
|
||||
};
|
||||
}
|
||||
}
|
||||
type RenderCounter = FunctionComponent<RenderCounterFunction>;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Properties)]
|
||||
struct ContextOutletProps {
|
||||
id: String,
|
||||
#[prop_or_default]
|
||||
magic: usize,
|
||||
}
|
||||
struct ContextOutletFunction;
|
||||
impl FunctionProvider for ContextOutletFunction {
|
||||
type TProps = ContextOutletProps;
|
||||
|
||||
fn run(props: &Self::TProps) -> Html {
|
||||
let counter = use_ref(|| 0);
|
||||
*counter.borrow_mut() += 1;
|
||||
|
||||
let ctx = use_context::<Rc<MyContext>>().expect("context not passed down");
|
||||
log::info!("============");
|
||||
log::info!("ctx is {:#?}", ctx);
|
||||
log::info!("magic is {:#?}", props.magic);
|
||||
log::info!("outlet counter is {:#?}", ctx);
|
||||
log::info!("============");
|
||||
|
||||
return html! {
|
||||
<>
|
||||
<div>{ format!("magic: {}\n", props.magic) }</div>
|
||||
<div id=props.id.clone()>
|
||||
{ format!("current: {}, total: {}", ctx.0, counter.borrow()) }
|
||||
</div>
|
||||
</>
|
||||
};
|
||||
}
|
||||
}
|
||||
type ContextOutlet = FunctionComponent<ContextOutletFunction>;
|
||||
|
||||
struct TestFunction;
|
||||
impl FunctionProvider for TestFunction {
|
||||
type TProps = ();
|
||||
|
||||
fn run(_props: &Self::TProps) -> Html {
|
||||
type MyContextProvider = ContextProvider<Rc<MyContext>>;
|
||||
|
||||
let (ctx, set_ctx) = use_state(|| MyContext("hello".into()));
|
||||
let rendered = use_ref(|| 0);
|
||||
|
||||
// this is used to force an update specific to test-2
|
||||
let (magic_rc, set_magic) = use_state(|| 0);
|
||||
let magic: usize = *magic_rc;
|
||||
|
||||
use_effect(move || {
|
||||
let count = *rendered.borrow();
|
||||
match count {
|
||||
0 => {
|
||||
set_ctx(MyContext("world".into()));
|
||||
*rendered.borrow_mut() += 1;
|
||||
}
|
||||
1 => {
|
||||
// force test-2 to re-render.
|
||||
set_magic(1);
|
||||
*rendered.borrow_mut() += 1;
|
||||
}
|
||||
2 => {
|
||||
set_ctx(MyContext("hello world!".into()));
|
||||
*rendered.borrow_mut() += 1;
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
|| {}
|
||||
});
|
||||
|
||||
return html! {
|
||||
<MyContextProvider context=ctx>
|
||||
<RenderCounter id="test-0">
|
||||
<ContextOutlet id="test-1"/>
|
||||
<ContextOutlet id="test-2" magic=magic/>
|
||||
</RenderCounter>
|
||||
</MyContextProvider>
|
||||
};
|
||||
}
|
||||
}
|
||||
type TestComponent = FunctionComponent<TestFunction>;
|
||||
|
||||
wasm_logger::init(wasm_logger::Config::default());
|
||||
let app: App<TestComponent> = yew::App::new();
|
||||
app.mount(yew::utils::document().get_element_by_id("output").unwrap());
|
||||
|
||||
// 1 initial render + 3 update steps
|
||||
assert_eq!(obtain_result_by_id("test-0"), "total: 4");
|
||||
|
||||
// 1 initial + 2 context update
|
||||
assert_eq!(
|
||||
obtain_result_by_id("test-1"),
|
||||
"current: hello world!, total: 3"
|
||||
);
|
||||
|
||||
// 1 initial + 1 context update + 1 magic update + 1 context update
|
||||
assert_eq!(
|
||||
obtain_result_by_id("test-2"),
|
||||
"current: hello world!, total: 4"
|
||||
);
|
||||
}
|
||||
}
|
320
packages/hooks/src/hooks/use_effect.rs
Normal file
320
packages/hooks/src/hooks/use_effect.rs
Normal file
|
@ -0,0 +1,320 @@
|
|||
use crate::use_hook;
|
||||
use std::{borrow::Borrow, rc::Rc};
|
||||
|
||||
struct UseEffect<Destructor> {
|
||||
destructor: Option<Box<Destructor>>,
|
||||
}
|
||||
|
||||
/// This hook is used for hooking into the component's lifecycle.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// # use yew_functional::{function_component, use_effect, use_state};
|
||||
/// # use yew::prelude::*;
|
||||
/// # use std::rc::Rc;
|
||||
/// #
|
||||
/// #[function_component(UseEffect)]
|
||||
/// fn effect() -> Html {
|
||||
/// let (counter, set_counter) = use_state(|| 0);
|
||||
///
|
||||
/// let counter_one = counter.clone();
|
||||
/// use_effect(move || {
|
||||
/// // Make a call to DOM API after component is rendered
|
||||
/// yew::utils::document().set_title(&format!("You clicked {} times", counter_one));
|
||||
///
|
||||
/// // Perform the cleanup
|
||||
/// || yew::utils::document().set_title(&format!("You clicked 0 times"))
|
||||
/// });
|
||||
///
|
||||
/// let onclick = {
|
||||
/// let counter = Rc::clone(&counter);
|
||||
/// Callback::from(move |_| set_counter(*counter + 1))
|
||||
/// };
|
||||
///
|
||||
/// html! {
|
||||
/// <button onclick=onclick>{ format!("Increment to {}", counter) }</button>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn use_effect<Destructor>(callback: impl FnOnce() -> Destructor + 'static)
|
||||
where
|
||||
Destructor: FnOnce() + 'static,
|
||||
{
|
||||
let callback = Box::new(callback);
|
||||
use_hook(
|
||||
move || {
|
||||
let effect: UseEffect<Destructor> = UseEffect { destructor: None };
|
||||
effect
|
||||
},
|
||||
|_, updater| {
|
||||
// Run on every render
|
||||
updater.post_render(move |state: &mut UseEffect<Destructor>| {
|
||||
if let Some(de) = state.destructor.take() {
|
||||
de();
|
||||
}
|
||||
let new_destructor = callback();
|
||||
state.destructor.replace(Box::new(new_destructor));
|
||||
false
|
||||
});
|
||||
},
|
||||
|hook| {
|
||||
if let Some(destructor) = hook.destructor.take() {
|
||||
destructor()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
struct UseEffectDeps<Destructor, Dependents> {
|
||||
destructor: Option<Box<Destructor>>,
|
||||
deps: Rc<Dependents>,
|
||||
}
|
||||
|
||||
/// This hook is similar to [`use_effect`] but it accepts dependencies.
|
||||
///
|
||||
/// Whenever the dependencies are changed, the effect callback is called again.
|
||||
/// To detect changes, dependencies must implement `PartialEq`.
|
||||
/// Note that the destructor also runs when dependencies change.
|
||||
pub fn use_effect_with_deps<Callback, Destructor, Dependents>(callback: Callback, deps: Dependents)
|
||||
where
|
||||
Callback: FnOnce(&Dependents) -> Destructor + 'static,
|
||||
Destructor: FnOnce() + 'static,
|
||||
Dependents: PartialEq + 'static,
|
||||
{
|
||||
let deps = Rc::new(deps);
|
||||
let deps_c = deps.clone();
|
||||
|
||||
use_hook(
|
||||
move || {
|
||||
let destructor: Option<Box<Destructor>> = None;
|
||||
UseEffectDeps {
|
||||
destructor,
|
||||
deps: deps_c,
|
||||
}
|
||||
},
|
||||
move |_, updater| {
|
||||
updater.post_render(move |state: &mut UseEffectDeps<Destructor, Dependents>| {
|
||||
if state.deps != deps {
|
||||
if let Some(de) = state.destructor.take() {
|
||||
de();
|
||||
}
|
||||
let new_destructor = callback(deps.borrow());
|
||||
state.deps = deps;
|
||||
state.destructor.replace(Box::new(new_destructor));
|
||||
} else if state.destructor.is_none() {
|
||||
state
|
||||
.destructor
|
||||
.replace(Box::new(callback(state.deps.borrow())));
|
||||
}
|
||||
false
|
||||
});
|
||||
},
|
||||
|hook| {
|
||||
if let Some(destructor) = hook.destructor.take() {
|
||||
destructor()
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::hooks::{use_effect_with_deps, use_ref, use_state};
|
||||
use crate::util::*;
|
||||
use crate::{FunctionComponent, FunctionProvider};
|
||||
use std::ops::Deref;
|
||||
use std::ops::DerefMut;
|
||||
use std::rc::Rc;
|
||||
use wasm_bindgen_test::*;
|
||||
use yew::prelude::*;
|
||||
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn use_effect_works_many_times() {
|
||||
struct UseEffectFunction {}
|
||||
impl FunctionProvider for UseEffectFunction {
|
||||
type TProps = ();
|
||||
|
||||
fn run(_: &Self::TProps) -> Html {
|
||||
let (counter, set_counter) = use_state(|| 0);
|
||||
let counter_clone = counter.clone();
|
||||
|
||||
use_effect_with_deps(
|
||||
move |_| {
|
||||
if *counter_clone < 4 {
|
||||
set_counter(*counter_clone + 1);
|
||||
}
|
||||
|| {}
|
||||
},
|
||||
*counter,
|
||||
);
|
||||
|
||||
return html! {
|
||||
<div>
|
||||
{"The test result is"}
|
||||
<div id="result">{counter}</div>
|
||||
{"\n"}
|
||||
</div>
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
type UseEffectComponent = FunctionComponent<UseEffectFunction>;
|
||||
let app: App<UseEffectComponent> = yew::App::new();
|
||||
app.mount(yew::utils::document().get_element_by_id("output").unwrap());
|
||||
let result = obtain_result();
|
||||
assert_eq!(result.as_str(), "4");
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn use_effect_works_once() {
|
||||
struct UseEffectFunction {}
|
||||
impl FunctionProvider for UseEffectFunction {
|
||||
type TProps = ();
|
||||
|
||||
fn run(_: &Self::TProps) -> Html {
|
||||
let (counter, set_counter) = use_state(|| 0);
|
||||
let counter_clone = counter.clone();
|
||||
|
||||
use_effect_with_deps(
|
||||
move |_| {
|
||||
set_counter(*counter_clone + 1);
|
||||
|| panic!("Destructor should not have been called")
|
||||
},
|
||||
(),
|
||||
);
|
||||
|
||||
return html! {
|
||||
<div>
|
||||
{"The test result is"}
|
||||
<div id="result">{counter}</div>
|
||||
{"\n"}
|
||||
</div>
|
||||
};
|
||||
}
|
||||
}
|
||||
type UseEffectComponent = FunctionComponent<UseEffectFunction>;
|
||||
let app: App<UseEffectComponent> = yew::App::new();
|
||||
app.mount(yew::utils::document().get_element_by_id("output").unwrap());
|
||||
let result = obtain_result();
|
||||
assert_eq!(result.as_str(), "1");
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn use_effect_refires_on_dependency_change() {
|
||||
struct UseEffectFunction {}
|
||||
impl FunctionProvider for UseEffectFunction {
|
||||
type TProps = ();
|
||||
|
||||
fn run(_: &Self::TProps) -> Html {
|
||||
let number_ref = use_ref(|| 0);
|
||||
let number_ref_c = number_ref.clone();
|
||||
let number_ref2 = use_ref(|| 0);
|
||||
let number_ref2_c = number_ref2.clone();
|
||||
let arg = *number_ref.borrow_mut().deref_mut();
|
||||
let (_, set_counter) = use_state(|| 0);
|
||||
use_effect_with_deps(
|
||||
move |dep| {
|
||||
let mut ref_mut = number_ref_c.borrow_mut();
|
||||
let inner_ref_mut = ref_mut.deref_mut();
|
||||
if *inner_ref_mut < 1 {
|
||||
*inner_ref_mut += 1;
|
||||
assert_eq!(dep, &0);
|
||||
} else {
|
||||
assert_eq!(dep, &1);
|
||||
}
|
||||
set_counter(10); // we just need to make sure it does not panic
|
||||
move || {
|
||||
set_counter(11);
|
||||
*number_ref2_c.borrow_mut().deref_mut() += 1;
|
||||
}
|
||||
},
|
||||
arg,
|
||||
);
|
||||
return html! {
|
||||
<div>
|
||||
{"The test result is"}
|
||||
<div id="result">{*number_ref.borrow_mut().deref_mut()}{*number_ref2.borrow_mut().deref_mut()}</div>
|
||||
{"\n"}
|
||||
</div>
|
||||
};
|
||||
}
|
||||
}
|
||||
type UseEffectComponent = FunctionComponent<UseEffectFunction>;
|
||||
let app: App<UseEffectComponent> = yew::App::new();
|
||||
app.mount(yew::utils::document().get_element_by_id("output").unwrap());
|
||||
let result: String = obtain_result();
|
||||
|
||||
assert_eq!(result.as_str(), "11");
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn use_effect_destroys_on_component_drop() {
|
||||
struct UseEffectFunction {}
|
||||
struct UseEffectWrapper {}
|
||||
#[derive(Properties, Clone)]
|
||||
struct WrapperProps {
|
||||
destroy_called: Rc<dyn Fn()>,
|
||||
}
|
||||
impl PartialEq for WrapperProps {
|
||||
fn eq(&self, _other: &Self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
#[derive(Properties, Clone)]
|
||||
struct FunctionProps {
|
||||
effect_called: Rc<dyn Fn()>,
|
||||
destroy_called: Rc<dyn Fn()>,
|
||||
}
|
||||
impl PartialEq for FunctionProps {
|
||||
fn eq(&self, _other: &Self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
type UseEffectComponent = FunctionComponent<UseEffectFunction>;
|
||||
type UseEffectWrapperComponent = FunctionComponent<UseEffectWrapper>;
|
||||
impl FunctionProvider for UseEffectFunction {
|
||||
type TProps = FunctionProps;
|
||||
|
||||
fn run(props: &Self::TProps) -> Html {
|
||||
let effect_called = props.effect_called.clone();
|
||||
let destroy_called = props.destroy_called.clone();
|
||||
use_effect_with_deps(
|
||||
move |_| {
|
||||
effect_called();
|
||||
move || destroy_called()
|
||||
},
|
||||
(),
|
||||
);
|
||||
return html! {};
|
||||
}
|
||||
}
|
||||
impl FunctionProvider for UseEffectWrapper {
|
||||
type TProps = WrapperProps;
|
||||
|
||||
fn run(props: &Self::TProps) -> Html {
|
||||
let (show, set_show) = use_state(|| true);
|
||||
if *show {
|
||||
let effect_called: Rc<dyn Fn()> = Rc::new(move || set_show(false));
|
||||
return html! {
|
||||
<UseEffectComponent destroy_called=props.destroy_called.clone() effect_called=effect_called />
|
||||
};
|
||||
} else {
|
||||
return html! {
|
||||
<div>{"EMPTY"}</div>
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
let app: App<UseEffectWrapperComponent> = yew::App::new();
|
||||
let destroy_counter = Rc::new(std::cell::RefCell::new(0));
|
||||
let destroy_counter_c = destroy_counter.clone();
|
||||
app.mount_with_props(
|
||||
yew::utils::document().get_element_by_id("output").unwrap(),
|
||||
WrapperProps {
|
||||
destroy_called: Rc::new(move || *destroy_counter_c.borrow_mut().deref_mut() += 1),
|
||||
},
|
||||
);
|
||||
assert_eq!(1, *destroy_counter.borrow().deref());
|
||||
}
|
||||
}
|
203
packages/hooks/src/hooks/use_reducer.rs
Normal file
203
packages/hooks/src/hooks/use_reducer.rs
Normal file
|
@ -0,0 +1,203 @@
|
|||
use crate::use_hook;
|
||||
use std::rc::Rc;
|
||||
|
||||
struct UseReducer<State> {
|
||||
current_state: Rc<State>,
|
||||
}
|
||||
|
||||
/// This hook is an alternative to [`use_state`]. It is used to handle component's state and is used
|
||||
/// when complex actions needs to be performed on said state.
|
||||
///
|
||||
/// For lazy initialization, consider using [`use_reducer_with_init`] instead.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// # use yew_functional::{function_component, use_reducer};
|
||||
/// # use yew::prelude::*;
|
||||
/// # use std::rc::Rc;
|
||||
/// # use std::ops::DerefMut;
|
||||
/// #
|
||||
/// #[function_component(UseReducer)]
|
||||
/// fn reducer() -> Html {
|
||||
/// /// reducer's Action
|
||||
/// enum Action {
|
||||
/// Double,
|
||||
/// Square,
|
||||
/// }
|
||||
///
|
||||
/// /// reducer's State
|
||||
/// struct CounterState {
|
||||
/// counter: i32,
|
||||
/// }
|
||||
///
|
||||
/// let (
|
||||
/// counter, // the state
|
||||
/// // function to update the state
|
||||
/// // as the same suggests, it dispatches the values to the reducer function
|
||||
/// dispatch
|
||||
/// ) = use_reducer(
|
||||
/// // the reducer function
|
||||
/// |prev: Rc<CounterState>, action: Action| CounterState {
|
||||
/// counter: match action {
|
||||
/// Action::Double => prev.counter * 2,
|
||||
/// Action::Square => prev.counter * prev.counter,
|
||||
/// }
|
||||
/// },
|
||||
/// // initial state
|
||||
/// CounterState { counter: 1 },
|
||||
/// );
|
||||
///
|
||||
/// let double_onclick = {
|
||||
/// let dispatch = Rc::clone(&dispatch);
|
||||
/// Callback::from(move |_| dispatch(Action::Double))
|
||||
/// };
|
||||
/// let square_onclick = Callback::from(move |_| dispatch(Action::Square));
|
||||
///
|
||||
/// html! {
|
||||
/// <>
|
||||
/// <div id="result">{ counter.counter }</div>
|
||||
///
|
||||
/// <button onclick=double_onclick>{ "Double" }</button>
|
||||
/// <button onclick=square_onclick>{ "Square" }</button>
|
||||
/// </>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn use_reducer<Action: 'static, Reducer, State: 'static>(
|
||||
reducer: Reducer,
|
||||
initial_state: State,
|
||||
) -> (Rc<State>, Rc<dyn Fn(Action)>)
|
||||
where
|
||||
Reducer: Fn(Rc<State>, Action) -> State + 'static,
|
||||
{
|
||||
use_reducer_with_init(reducer, initial_state, |a| a)
|
||||
}
|
||||
|
||||
/// [`use_reducer`] but with init argument.
|
||||
///
|
||||
/// This is useful for lazy initialization where it is beneficial not to perform expensive
|
||||
/// computation up-front
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// # use yew_functional::{function_component, use_reducer_with_init};
|
||||
/// # use yew::prelude::*;
|
||||
/// # use std::rc::Rc;
|
||||
/// #
|
||||
/// #[function_component(UseReducerWithInit)]
|
||||
/// fn reducer_with_init() -> Html {
|
||||
/// struct CounterState {
|
||||
/// counter: i32,
|
||||
/// }
|
||||
/// let (counter, dispatch) = use_reducer_with_init(
|
||||
/// |prev: Rc<CounterState>, action: i32| CounterState {
|
||||
/// counter: prev.counter + action,
|
||||
/// },
|
||||
/// 0,
|
||||
/// |initial: i32| CounterState {
|
||||
/// counter: initial + 10,
|
||||
/// },
|
||||
/// );
|
||||
///
|
||||
/// html! {
|
||||
/// <>
|
||||
/// <div id="result">{counter.counter}</div>
|
||||
///
|
||||
/// <button onclick=Callback::from(move |_| dispatch(10))>{"Increment by 10"}</button>
|
||||
/// </>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn use_reducer_with_init<
|
||||
Reducer,
|
||||
Action: 'static,
|
||||
State: 'static,
|
||||
InitialState: 'static,
|
||||
InitFn: 'static,
|
||||
>(
|
||||
reducer: Reducer,
|
||||
initial_state: InitialState,
|
||||
init: InitFn,
|
||||
) -> (Rc<State>, Rc<dyn Fn(Action)>)
|
||||
where
|
||||
Reducer: Fn(Rc<State>, Action) -> State + 'static,
|
||||
InitFn: Fn(InitialState) -> State,
|
||||
{
|
||||
let init = Box::new(init);
|
||||
let reducer = Rc::new(reducer);
|
||||
use_hook(
|
||||
move || UseReducer {
|
||||
current_state: Rc::new(init(initial_state)),
|
||||
},
|
||||
|s, updater| {
|
||||
let setter: Rc<dyn Fn(Action)> = Rc::new(move |action: Action| {
|
||||
let reducer = reducer.clone();
|
||||
// We call the callback, consumer the updater
|
||||
// Required to put the type annotations on Self so the method knows how to downcast
|
||||
updater.callback(move |state: &mut UseReducer<State>| {
|
||||
let new_state = reducer(state.current_state.clone(), action);
|
||||
state.current_state = Rc::new(new_state);
|
||||
true
|
||||
});
|
||||
});
|
||||
|
||||
let current = s.current_state.clone();
|
||||
(current, setter)
|
||||
},
|
||||
|_| {},
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::hooks::use_effect_with_deps;
|
||||
use crate::util::*;
|
||||
use crate::{FunctionComponent, FunctionProvider};
|
||||
use wasm_bindgen_test::*;
|
||||
use yew::prelude::*;
|
||||
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn use_reducer_works() {
|
||||
struct UseReducerFunction {}
|
||||
impl FunctionProvider for UseReducerFunction {
|
||||
type TProps = ();
|
||||
fn run(_: &Self::TProps) -> Html {
|
||||
struct CounterState {
|
||||
counter: i32,
|
||||
}
|
||||
let (counter, dispatch) = use_reducer_with_init(
|
||||
|prev: std::rc::Rc<CounterState>, action: i32| CounterState {
|
||||
counter: prev.counter + action,
|
||||
},
|
||||
0,
|
||||
|initial: i32| CounterState {
|
||||
counter: initial + 10,
|
||||
},
|
||||
);
|
||||
|
||||
use_effect_with_deps(
|
||||
move |_| {
|
||||
dispatch(1);
|
||||
|| {}
|
||||
},
|
||||
(),
|
||||
);
|
||||
return html! {
|
||||
<div>
|
||||
{"The test result is"}
|
||||
<div id="result">{counter.counter}</div>
|
||||
{"\n"}
|
||||
</div>
|
||||
};
|
||||
}
|
||||
}
|
||||
type UseReducerComponent = FunctionComponent<UseReducerFunction>;
|
||||
let app: App<UseReducerComponent> = yew::App::new();
|
||||
app.mount(yew::utils::document().get_element_by_id("output").unwrap());
|
||||
let result = obtain_result();
|
||||
|
||||
assert_eq!(result.as_str(), "11");
|
||||
}
|
||||
}
|
98
packages/hooks/src/hooks/use_ref.rs
Normal file
98
packages/hooks/src/hooks/use_ref.rs
Normal file
|
@ -0,0 +1,98 @@
|
|||
use crate::use_hook;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
/// This hook is used for obtaining a mutable reference to a stateful value.
|
||||
/// Its state persists across renders.
|
||||
///
|
||||
/// It is important to note that you do not get notified of state changes.
|
||||
/// If you need the component to be re-rendered on state change, consider using [`use_state`].
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// # use yew_functional::{function_component, use_state, use_ref};
|
||||
/// # use yew::prelude::*;
|
||||
/// # use std::rc::Rc;
|
||||
/// # use std::cell::RefCell;
|
||||
/// # use std::ops::{Deref, DerefMut};
|
||||
/// #
|
||||
/// #[function_component(UseRef)]
|
||||
/// fn ref_hook() -> Html {
|
||||
/// let (message, set_message) = use_state(|| "".to_string());
|
||||
/// let message_count = use_ref(|| 0);
|
||||
///
|
||||
/// let onclick = Callback::from(move |e| {
|
||||
/// let window = yew::utils::window();
|
||||
///
|
||||
/// if *message_count.borrow_mut() > 3 {
|
||||
/// window.alert_with_message("Message limit reached");
|
||||
/// } else {
|
||||
/// *message_count.borrow_mut() += 1;
|
||||
/// window.alert_with_message("Message sent");
|
||||
/// }
|
||||
/// });
|
||||
///
|
||||
/// let onchange = Callback::from(move |e| {
|
||||
/// if let ChangeData::Value(value) = e {
|
||||
/// set_message(value)
|
||||
/// }
|
||||
/// });
|
||||
///
|
||||
/// html! {
|
||||
/// <div>
|
||||
/// <input onchange=onchange value=message />
|
||||
/// <button onclick=onclick>{ "Send" }</button>
|
||||
/// </div>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn use_ref<T: 'static>(initial_value: impl FnOnce() -> T + 'static) -> Rc<RefCell<T>> {
|
||||
use_hook(
|
||||
|| Rc::new(RefCell::new(initial_value())),
|
||||
|state, _| state.clone(),
|
||||
|_| {},
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
hooks::use_state,
|
||||
util::*,
|
||||
{FunctionComponent, FunctionProvider},
|
||||
};
|
||||
use std::ops::DerefMut;
|
||||
use wasm_bindgen_test::*;
|
||||
use yew::prelude::*;
|
||||
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn use_ref_works() {
|
||||
struct UseRefFunction {}
|
||||
impl FunctionProvider for UseRefFunction {
|
||||
type TProps = ();
|
||||
|
||||
fn run(_: &Self::TProps) -> Html {
|
||||
let ref_example = use_ref(|| 0);
|
||||
*ref_example.borrow_mut().deref_mut() += 1;
|
||||
let (counter, set_counter) = use_state(|| 0);
|
||||
if *counter < 5 {
|
||||
set_counter(*counter + 1)
|
||||
}
|
||||
return html! {
|
||||
<div>
|
||||
{"The test output is: "}
|
||||
<div id="result">{*ref_example.borrow_mut().deref_mut() > 4}</div>
|
||||
{"\n"}
|
||||
</div>
|
||||
};
|
||||
}
|
||||
}
|
||||
type UseRefComponent = FunctionComponent<UseRefFunction>;
|
||||
let app: App<UseRefComponent> = yew::App::new();
|
||||
app.mount(yew::utils::document().get_element_by_id("output").unwrap());
|
||||
|
||||
let result = obtain_result();
|
||||
assert_eq!(result.as_str(), "true");
|
||||
}
|
||||
}
|
142
packages/hooks/src/hooks/use_state.rs
Normal file
142
packages/hooks/src/hooks/use_state.rs
Normal file
|
@ -0,0 +1,142 @@
|
|||
use crate::use_hook;
|
||||
use std::rc::Rc;
|
||||
|
||||
struct UseState<T2> {
|
||||
current: Rc<T2>,
|
||||
}
|
||||
|
||||
/// This hook is used to mange state in a function component.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// # use yew_functional::{function_component, use_state, use_ref};
|
||||
/// # use yew::prelude::*;
|
||||
/// # use std::rc::Rc;
|
||||
/// #
|
||||
/// #[function_component(UseState)]
|
||||
/// fn state() -> Html {
|
||||
/// let (
|
||||
/// counter, // the returned state
|
||||
/// set_counter // setter to update the state
|
||||
/// ) = use_state(|| 0);
|
||||
/// let onclick = {
|
||||
/// let counter = Rc::clone(&counter);
|
||||
/// Callback::from(move |_| set_counter(*counter + 1))
|
||||
/// };
|
||||
///
|
||||
/// html! {
|
||||
/// <div>
|
||||
/// <button onclick=onclick>{ "Increment value" }</button>
|
||||
/// <p>
|
||||
/// <b>{ "Current value: " }</b>
|
||||
/// { counter }
|
||||
/// </p>
|
||||
/// </div>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn use_state<T: 'static, F: FnOnce() -> T + 'static>(
|
||||
initial_state_fn: F,
|
||||
) -> (Rc<T>, Rc<dyn Fn(T)>) {
|
||||
use_hook(
|
||||
// Initializer
|
||||
move || UseState {
|
||||
current: Rc::new(initial_state_fn()),
|
||||
},
|
||||
// Runner
|
||||
move |hook, updater| {
|
||||
let setter: Rc<(dyn Fn(T))> = Rc::new(move |new_val: T| {
|
||||
updater.callback(move |st: &mut UseState<T>| {
|
||||
st.current = Rc::new(new_val);
|
||||
true
|
||||
})
|
||||
});
|
||||
|
||||
let current = hook.current.clone();
|
||||
(current, setter)
|
||||
},
|
||||
// Teardown
|
||||
|_| {},
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::hooks::use_effect_with_deps;
|
||||
use crate::util::*;
|
||||
use crate::{FunctionComponent, FunctionProvider};
|
||||
use wasm_bindgen_test::*;
|
||||
use yew::prelude::*;
|
||||
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn use_state_works() {
|
||||
struct UseStateFunction {}
|
||||
impl FunctionProvider for UseStateFunction {
|
||||
type TProps = ();
|
||||
|
||||
fn run(_: &Self::TProps) -> Html {
|
||||
let (counter, set_counter) = use_state(|| 0);
|
||||
if *counter < 5 {
|
||||
set_counter(*counter + 1)
|
||||
}
|
||||
return html! {
|
||||
<div>
|
||||
{"Test Output: "}
|
||||
<div id="result">{*counter}</div>
|
||||
{"\n"}
|
||||
</div>
|
||||
};
|
||||
}
|
||||
}
|
||||
type UseComponent = FunctionComponent<UseStateFunction>;
|
||||
let app: App<UseComponent> = yew::App::new();
|
||||
app.mount(yew::utils::document().get_element_by_id("output").unwrap());
|
||||
let result = obtain_result();
|
||||
assert_eq!(result.as_str(), "5");
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn multiple_use_state_setters() {
|
||||
struct UseStateFunction {}
|
||||
impl FunctionProvider for UseStateFunction {
|
||||
type TProps = ();
|
||||
|
||||
fn run(_: &Self::TProps) -> Html {
|
||||
let (counter, set_counter_in_use_effect) = use_state(|| 0);
|
||||
let counter = *counter;
|
||||
// clone without manually wrapping with Rc
|
||||
let set_counter_in_another_scope = set_counter_in_use_effect.clone();
|
||||
use_effect_with_deps(
|
||||
move |_| {
|
||||
// 1st location
|
||||
set_counter_in_use_effect(counter + 1);
|
||||
|| {}
|
||||
},
|
||||
(),
|
||||
);
|
||||
let another_scope = move || {
|
||||
if counter < 11 {
|
||||
// 2nd location
|
||||
set_counter_in_another_scope(counter + 10)
|
||||
}
|
||||
};
|
||||
another_scope();
|
||||
return html! {
|
||||
<div>
|
||||
{"Test Output: "}
|
||||
// expected output
|
||||
<div id="result">{counter}</div>
|
||||
{"\n"}
|
||||
</div>
|
||||
};
|
||||
}
|
||||
}
|
||||
type UseComponent = FunctionComponent<UseStateFunction>;
|
||||
let app: App<UseComponent> = yew::App::new();
|
||||
app.mount(yew::utils::document().get_element_by_id("output").unwrap());
|
||||
let result = obtain_result();
|
||||
assert_eq!(result.as_str(), "11");
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue