Răsfoiți Sursa

Feat: add hooks

Jonathan Kelley 4 ani în urmă
părinte
comite
c1b990b

+ 68 - 0
packages/hooks/src/hooks/mod.rs

@@ -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 - 0
packages/hooks/src/hooks/use_context.rs

@@ -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 - 0
packages/hooks/src/hooks/use_effect.rs

@@ -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 - 0
packages/hooks/src/hooks/use_reducer.rs

@@ -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 - 0
packages/hooks/src/hooks/use_ref.rs

@@ -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 - 0
packages/hooks/src/hooks/use_state.rs

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