mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-26 14:10:20 +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