Feat: add hooks

This commit is contained in:
Jonathan Kelley 2021-01-19 08:56:22 -05:00
parent ae1b8bbede
commit c1b990b27c
6 changed files with 1300 additions and 0 deletions

View 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())
}

View 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"
);
}
}

View 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());
}
}

View 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");
}
}

View 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");
}
}

View 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");
}
}