diff --git a/docs/concepts/function-components/custom-hooks.md b/docs/concepts/function-components/custom-hooks.md index 835e9ea8cce..333a1e1983c 100644 --- a/docs/concepts/function-components/custom-hooks.md +++ b/docs/concepts/function-components/custom-hooks.md @@ -39,75 +39,13 @@ The `use_` prefix conventionally denotes that a function is a hook. This function will take no arguments and return `Rc>>`. ```rust fn use_subscribe() -> Rc>> { - // ... + todo!() } ``` -The hook's logic goes inside the `use_hook`'s callback. -`use_hook` is the handler function for custom Hooks. It takes in 2 arguments: `hook_runner` and `initial_state_producer`. - -`hook_runner` is where all the hook's logic goes. `use_hook` returns the value returned by this callback. -`hook_runner` itself takes 2 arguments: a mutable reference to the internal state of the hook and `hook_callback`. -`hook_callback` also takes in 2 arguments: a callback and, a bool indicating whether it is run post render of the component. -The callback takes in `internal_state` which is a mutable reference to the instance of the internal state and performs the actual mutations. -It returns `ShouldRender` bool. -`use_hook`'s second argument of `initial_state_producer` takes in a callback for creating an instance of the internal state. -The internal state is a struct which implements the `Hook` trait. - -Now let's create the state struct for our `use_subscribe` hook. -```rust -/// `use_subscribe` internal state -struct UseSubscribeState { - /// holds all the messages received - pub messages: Rc>>, -} - -impl Hook for UseSubscribeState {} -``` - -Now we'll modify `use_subscribe` to contain the actual logic. -```rust -fn use_subscribe() -> Rc>> { - use_hook( - // hook's handler. all the logic goes in here - |state: &mut UseSubscribeState, hook_callback| { - // calling other Hooks inside a hook - use_effect(move || { - let producer = EventBus::bridge(Callback::from(move |msg| { - hook_callback( - // where the mutations of state are performed - |state| { - (*state.messages).borrow_mut().deref_mut().push(msg); - true // should re-render - }, false // run post-render - ) - })); - - || drop(producer) - }); - - // return from hook - state.messages.clone() - }, - // initial state producer - || UseSubscribeState { messages: Rc::new(RefCell::new(vec![])) }, - ) -} -``` - -We can now use our custom hook like this: -```rust -#[function_component(ShowMessages)] -pub fn show_messages() -> Html { - let state = use_subscribe(); - let output = state.borrow().deref().into_iter().map(|it| html! {

{ it }

}); - - html! {
{ for output }
} -} -``` - -It's important to note that `use_hook` isn't necessarily required to create custom hooks -as they can just consist of other hooks. `use_hook` should generally be avoided. +This is a simple hook which can be created by combining other hooks. For this example, we'll two pre-defined hooks. +We'll use `use_state` hook to store the `Vec` for messages, so they persist between component re-renders. +We'll also use `use_effect` to subscribe to the `EventBus` `Agent` so the subscription can be tied to component's lifecycle. ```rust fn use_subscribe() -> Rc> { @@ -124,4 +62,11 @@ fn use_subscribe() -> Rc> { state } -``` +``` + +Although this approach works in almost all cases, it can't be used to write primitive hooks like the pre-defined hooks we've been using already + +### Writing primitive hooks + +`use_hook` function is used to write such hooks. View the docs on [docs.rs]() for the documentation +and `hooks` directory to see implementations of pre-defined hooks. diff --git a/docs/concepts/function-components/pre-defined-hooks.md b/docs/concepts/function-components/pre-defined-hooks.md index 107e0360aeb..ad0222a3ad7 100644 --- a/docs/concepts/function-components/pre-defined-hooks.md +++ b/docs/concepts/function-components/pre-defined-hooks.md @@ -3,56 +3,33 @@ title: Pre-defined Hooks description: The pre-defined Hooks that Yew comes with --- -:::note Why do Hooks return `Rc`? - -In most cases, you'll be cloning the values returned from the Hooks. -As it is generally expensive to clone such values, they're `Rc`ed, so they can be cloned relatively cheaply. - -The following example shows one of the most common cases which requires cloning the values: - -```rust -let (text, set_text) = use_state(|| "Hello".to_owned()); -let onclick = { - let text = Rc::clone(&text); - // Values must be moved into this closure so in order to use them later on, they must be cloned - Callback::from(move |_| set_text(format!("{} World", text))) -}; - -// If `text` wasn't cloned above, it would've been impossible to use it here -html! { text } -``` -::: - ## `use_state` -`use_state` is used to mange state in a function component. -It returns a `Rc` pointing to the value of the hook's state, and a setter function. +`use_state` is used to manage state in a function component. +It returns a `UseState` object which `Deref`s to the current value +and provides a `set` method to update the value. The hook takes a function as input which determines the initial state. This value remains up-to-date on subsequent renders. -The setter function is used to update the value and trigger a re-render. - ### Example ```rust #[function_component(UseState)] fn state() -> Html { - let ( - counter, // the returned state - set_counter // setter to update the state - ) = use_state(|| 0); + let counter = use_state(|| 0); let onclick = { - let counter = Rc::clone(&counter); - Callback::from(move |_| set_counter(*counter + 1)) + let counter = counter.clone(); + Callback::from(move |_| counter.set(*counter + 1)) }; + html! {

{ "Current value: " } - { counter } + { *counter }

} @@ -129,12 +106,7 @@ fn reducer() -> Html { 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( + let counter = use_reducer( // the reducer function |prev: Rc, action: Action| CounterState { counter: match action { @@ -146,11 +118,14 @@ fn reducer() -> Html { CounterState { counter: 1 }, ); - let double_onclick = { - let dispatch = Rc::clone(&dispatch); - Callback::from(move |_| dispatch(Action::Double)) + let double_onclick = { + let counter = counter.clone(); + Callback::from(move |_| counter.dispatch(Action::Double)) + }; + let square_onclick = { + let counter = counter.clone(); + Callback::from(move |_| counter.dispatch(Action::Square)) }; - let square_onclick = Callback::from(move |_| dispatch(Action::Square)); html! { <> @@ -172,7 +147,7 @@ This is useful for lazy initialization where it is beneficial not to perform exp computation up-front. ```rust -let (counter, dispatch) = use_reducer_with_init( +let counter = use_reducer_with_init( // reducer function |prev: Rc, action: i32| CounterState { counter: prev.counter + action, diff --git a/packages/yew-functional/Cargo.toml b/packages/yew-functional/Cargo.toml index b9a97d49d98..1c188b5036f 100644 --- a/packages/yew-functional/Cargo.toml +++ b/packages/yew-functional/Cargo.toml @@ -14,6 +14,8 @@ description = "A framework for making client-side single-page apps" wasm-bindgen-test = "0.3.9" web-sys = "0.3.36" yew = { path = "../yew" } +log = "0.4" +wasm-logger = "0.2" yew-services = { path = "../yew-services" } [dependencies] diff --git a/packages/yew-functional/src/hooks/mod.rs b/packages/yew-functional/src/hooks/mod.rs index a8519d8cbeb..ca140a94bc2 100644 --- a/packages/yew-functional/src/hooks/mod.rs +++ b/packages/yew-functional/src/hooks/mod.rs @@ -10,65 +10,62 @@ pub use use_reducer::*; pub use use_ref::*; pub use use_state::*; -use crate::CURRENT_HOOK; +use crate::{HookUpdater, CURRENT_HOOK}; use std::cell::RefCell; use std::ops::DerefMut; use std::rc::Rc; -pub trait Hook { - fn tear_down(&mut self) {} -} - -pub fn use_hook( - hook_runner: HookRunner, - initial_state_producer: InitialStateProvider, -) -> R -where - HookRunner: FnOnce(&mut InternalHookState, Box) -> R, - InternalHookState: Hook + 'static, - InitialStateProvider: FnOnce() -> InternalHookState, - HookUpdate: FnOnce(&mut InternalHookState) -> bool, -{ +/// Low level building block of creating hooks. +/// +/// It is used to created the pre-defined primitive hooks. +/// Generally, it isn't needed to create hooks and should be avoided as most custom hooks can be +/// created by combining other hooks as described in [Yew Docs]. +/// +/// The `initializer` callback is called once to create the initial state of the hook. +/// `runner` callback handles the logic of the hook. It is called when the hook function is called. +/// `destructor`, as the name implies, is called to cleanup the leftovers of the hook. +/// +/// See the pre-defined hooks for examples of how to use this function. +/// +/// [Yew Docs]: https://yew.rs/docs/en/next/concepts/function-components/custom-hooks +pub fn use_hook( + initializer: impl FnOnce() -> InternalHook, + runner: impl FnOnce(&mut InternalHook, HookUpdater) -> Output, + destructor: Tear, +) -> Output { // Extract current hook - let (hook, process_message) = CURRENT_HOOK.with(|hook_state| { + let updater = CURRENT_HOOK.with(|hook_state| { // 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(initial_state_producer())); + let initial_state = Rc::new(RefCell::new(initializer())); hook_state.hooks.push(initial_state.clone()); hook_state.destroy_listeners.push(Box::new(move || { - initial_state.borrow_mut().deref_mut().tear_down(); + destructor(initial_state.borrow_mut().deref_mut()); })); } - let hook = hook_state.hooks[hook_pos].clone(); + let hook = hook_state + .hooks + .get(hook_pos) + .expect("Not the same number of hooks. Hooks must not be called conditionally") + .clone(); - (hook, hook_state.process_message.clone()) + HookUpdater { + hook, + process_message: hook_state.process_message.clone(), + } }); - let hook: Rc> = hook - .downcast() - .expect("Incompatible hook type. Hooks must always be called in the same order"); - - let hook_callback = { - let hook = hook.clone(); - Box::new(move |update: HookUpdate, post_render| { - let hook = hook.clone(); - process_message( - Box::new(move || { - let mut hook = hook.borrow_mut(); - update(&mut hook) - }), - post_render, - ); - }) - }; - // 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 = hook.borrow_mut(); - hook_runner(&mut hook, hook_callback) + 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()) } diff --git a/packages/yew-functional/src/hooks/use_context.rs b/packages/yew-functional/src/hooks/use_context.rs index 0bc16e70c92..8e0f90066f0 100644 --- a/packages/yew-functional/src/hooks/use_context.rs +++ b/packages/yew-functional/src/hooks/use_context.rs @@ -1,5 +1,4 @@ -use super::{use_hook, Hook}; -use crate::get_current_scope; +use crate::{get_current_scope, use_hook}; use std::any::TypeId; use std::cell::RefCell; use std::rc::{Rc, Weak}; @@ -9,6 +8,60 @@ use yew::html::{AnyScope, Scope}; use yew::{Children, Component, ComponentLink, Html, Properties}; type ConsumerCallback = Box)>; +type UseContextOutput = Option>; + +struct UseContext { + provider_scope: Option>>, + current_context: Option>, + callback: Option>>, +} + +/// Hook for consuming context values in function components. +pub fn use_context() -> UseContextOutput { + 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::(&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| { + updater.callback(move |state: &mut UseContext| { + 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); + } + }, + ) +} /// Props for [`ContextProvider`] #[derive(Clone, PartialEq, Properties)] @@ -31,8 +84,8 @@ impl ContextProvider { /// 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>) { - let mut consumers = self.consumers.borrow_mut(); // 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); @@ -119,79 +172,3 @@ where .as_ref() .and_then(|scope| scope.get_component().map(|comp| f(&*comp))) } - -/// Hook for consuming context values in function components. -/// The context of the type passed as `T` is returned. If there is no such context in scope, `None` is returned. -/// A component which calls `use_context` will re-render when the data of the context changes. -/// -/// More information about contexts and how to define and consume them can be found on [Yew Docs](https://yew.rs). -/// -/// # Example -/// ```rust -/// # use yew_functional::{function_component, use_context}; -/// # use yew::prelude::*; -/// # use std::rc::Rc; -/// -/// # #[derive(Clone, Debug, PartialEq)] -/// # struct ThemeContext { -/// # foreground: String, -/// # background: String, -/// # } -/// #[function_component(ThemedButton)] -/// pub fn themed_button() -> Html { -/// let theme = use_context::>().expect("no ctx found"); -/// -/// html! { -/// -/// } -/// } -/// ``` -pub fn use_context() -> Option> { - struct UseContextState { - provider_scope: Option>>, - current_context: Option>, - callback: Option>>, - } - impl Hook for UseContextState { - fn tear_down(&mut self) { - if let Some(cb) = self.callback.take() { - drop(cb); - } - } - } - - let scope = get_current_scope() - .expect("No current Scope. `use_context` can only be called inside function components"); - - use_hook( - |state: &mut UseContextState, hook_callback| { - state.callback = Some(Rc::new(Box::new(move |ctx: Rc| { - hook_callback( - |state: &mut UseContextState| { - state.current_context = Some(ctx); - true - }, - false, // run pre render - ); - }))); - let weak_cb = Rc::downgrade(state.callback.as_ref().unwrap()); - with_provider_component(&state.provider_scope, |comp| { - comp.subscribe_consumer(weak_cb) - }); - - state.current_context.clone() - }, - move || { - let provider_scope = find_context_provider_scope::(&scope); - let current_context = - with_provider_component(&provider_scope, |comp| Rc::clone(&comp.context)); - UseContextState { - provider_scope, - current_context, - callback: None, - } - }, - ) -} diff --git a/packages/yew-functional/src/hooks/use_effect.rs b/packages/yew-functional/src/hooks/use_effect.rs index 46c6b58af84..fe154a17fd6 100644 --- a/packages/yew-functional/src/hooks/use_effect.rs +++ b/packages/yew-functional/src/hooks/use_effect.rs @@ -1,6 +1,10 @@ -use super::{use_hook, Hook}; +use crate::use_hook; use std::{borrow::Borrow, rc::Rc}; +struct UseEffect { + destructor: Option>, +} + /// This hook is used for hooking into the component's lifecycle. /// /// # Example @@ -11,61 +15,59 @@ use std::{borrow::Borrow, rc::Rc}; /// # /// #[function_component(UseEffect)] /// fn effect() -> Html { -/// let (counter, set_counter) = use_state(|| 0); +/// let 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)); +/// 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)) +/// let counter = counter.clone(); +/// Callback::from(move |_| counter.set(*counter + 1)) /// }; /// /// html! { -/// +/// /// } /// } /// ``` -pub fn use_effect(callback: F) +pub fn use_effect(callback: impl FnOnce() -> Destructor + 'static) where - F: FnOnce() -> Destructor + 'static, Destructor: FnOnce() + 'static, { - struct UseEffectState { - destructor: Option>, - } - impl Hook for UseEffectState { - fn tear_down(&mut self) { - if let Some(destructor) = self.destructor.take() { - destructor() - } - } - } - let callback = Box::new(callback); - use_hook( - |_: &mut UseEffectState, hook_callback| { - hook_callback( - move |state: &mut UseEffectState| { - if let Some(de) = state.destructor.take() { - de(); - } - let new_destructor = callback(); - state.destructor.replace(Box::new(new_destructor)); - false - }, - true, // run post render - ); + move || { + let effect: UseEffect = UseEffect { destructor: None }; + effect }, - || UseEffectState { destructor: None }, - ); + |_, updater| { + // Run on every render + updater.post_render(move |state: &mut UseEffect| { + 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: Option>, + deps: Rc, } /// This hook is similar to [`use_effect`] but it accepts dependencies. @@ -73,51 +75,44 @@ where /// 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: F, deps: Dependents) +pub fn use_effect_with_deps(callback: Callback, deps: Dependents) where - F: FnOnce(&Dependents) -> Destructor + 'static, + Callback: FnOnce(&Dependents) -> Destructor + 'static, Destructor: FnOnce() + 'static, Dependents: PartialEq + 'static, { - struct UseEffectState { - deps: Rc, - destructor: Option>, - } - impl Hook for UseEffectState { - fn tear_down(&mut self) { - if let Some(destructor) = self.destructor.take() { - destructor() - } - } - } - let deps = Rc::new(deps); let deps_c = deps.clone(); use_hook( - move |_state: &mut UseEffectState, hook_callback| { - hook_callback( - move |state: &mut UseEffectState| { - 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()))); + move || { + let destructor: Option> = None; + UseEffectDeps { + destructor, + deps: deps_c, + } + }, + move |_, updater| { + updater.post_render(move |state: &mut UseEffectDeps| { + if state.deps != deps { + if let Some(de) = state.destructor.take() { + de(); } - false - }, - true, // run post render - ); + 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 + }); }, - || UseEffectState { - deps: deps_c, - destructor: None, + |hook| { + if let Some(destructor) = hook.destructor.take() { + destructor() + } }, ); } diff --git a/packages/yew-functional/src/hooks/use_reducer.rs b/packages/yew-functional/src/hooks/use_reducer.rs index 35809b9591d..c2df4640208 100644 --- a/packages/yew-functional/src/hooks/use_reducer.rs +++ b/packages/yew-functional/src/hooks/use_reducer.rs @@ -1,7 +1,12 @@ -use super::{use_hook, Hook}; +use crate::use_hook; +use std::ops::Deref; use std::rc::Rc; -/// This hook is an alternative to [`use_state`]. It is used to handle component's state and is used +struct UseReducer { + current_state: Rc, +} + +/// This hook is an alternative to [`use_state`](super::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. @@ -26,12 +31,7 @@ use std::rc::Rc; /// 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( +/// let counter = use_reducer( /// // the reducer function /// |prev: Rc, action: Action| CounterState { /// counter: match action { @@ -44,10 +44,13 @@ use std::rc::Rc; /// ); /// /// let double_onclick = { -/// let dispatch = Rc::clone(&dispatch); -/// Callback::from(move |_| dispatch(Action::Double)) +/// let counter = counter.clone(); +/// Callback::from(move |_| counter.dispatch(Action::Double)) +/// }; +/// let square_onclick = { +/// let counter = counter.clone(); +/// Callback::from(move |_| counter.dispatch(Action::Square)) /// }; -/// let square_onclick = Callback::from(move |_| dispatch(Action::Square)); /// /// html! { /// <> @@ -59,12 +62,14 @@ use std::rc::Rc; /// } /// } /// ``` -pub fn use_reducer( +pub fn use_reducer( reducer: Reducer, initial_state: State, -) -> (Rc, Rc) +) -> UseReducerHandle where + Action: 'static, Reducer: Fn(Rc, Action) -> State + 'static, + State: 'static, { use_reducer_with_init(reducer, initial_state, |a| a) } @@ -85,7 +90,7 @@ where /// struct CounterState { /// counter: i32, /// } -/// let (counter, dispatch) = use_reducer_with_init( +/// let counter = use_reducer_with_init( /// |prev: Rc, action: i32| CounterState { /// counter: prev.counter + action, /// }, @@ -99,47 +104,75 @@ where /// <> ///
{counter.counter}
/// -/// +/// /// /// } /// } /// ``` -pub fn use_reducer_with_init( +pub fn use_reducer_with_init( reducer: Reducer, initial_state: InitialState, init: InitFn, -) -> (Rc, Rc) +) -> UseReducerHandle where Reducer: Fn(Rc, Action) -> State + 'static, - InitFn: Fn(InitialState) -> State, + Action: 'static, + State: 'static, + InitialState: 'static, + InitFn: Fn(InitialState) -> State + 'static, { - struct UseReducerState { - current_state: Rc, - } - impl Hook for UseReducerState {} let init = Box::new(init); let reducer = Rc::new(reducer); use_hook( - |internal_hook_change: &mut UseReducerState, hook_callback| { - ( - internal_hook_change.current_state.clone(), - Rc::new(move |action: Action| { - let reducer = reducer.clone(); - hook_callback( - move |internal_hook_change: &mut UseReducerState| { - internal_hook_change.current_state = Rc::new((reducer)( - internal_hook_change.current_state.clone(), - action, - )); - true - }, - false, // run pre render - ); - }), - ) - }, - move || UseReducerState { + move || UseReducer { current_state: Rc::new(init(initial_state)), }, + |s, updater| { + let setter: Rc = 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| { + let new_state = reducer(state.current_state.clone(), action); + state.current_state = Rc::new(new_state); + true + }); + }); + + UseReducerHandle { + value: Rc::clone(&s.current_state), + setter, + } + }, + |_| {}, ) } + +/// State handle for [`use_reducer`] hook +pub struct UseReducerHandle { + value: Rc, + setter: Rc, +} + +impl UseReducerHandle { + pub fn dispatch(&self, value: Action) { + (self.setter)(value) + } +} + +impl Deref for UseReducerHandle { + type Target = State; + + fn deref(&self) -> &Self::Target { + &*self.value + } +} + +impl Clone for UseReducerHandle { + fn clone(&self) -> Self { + Self { + value: Rc::clone(&self.value), + setter: Rc::clone(&self.setter), + } + } +} diff --git a/packages/yew-functional/src/hooks/use_ref.rs b/packages/yew-functional/src/hooks/use_ref.rs index ca6e0f18702..865ab774ac5 100644 --- a/packages/yew-functional/src/hooks/use_ref.rs +++ b/packages/yew-functional/src/hooks/use_ref.rs @@ -1,12 +1,11 @@ -use super::{use_hook, Hook}; -use std::cell::RefCell; -use std::rc::Rc; +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`]. +/// If you need the component to be re-rendered on state change, consider using [`use_state`](super::use_state()). /// /// # Example /// ```rust @@ -18,7 +17,7 @@ use std::rc::Rc; /// # /// #[function_component(UseRef)] /// fn ref_hook() -> Html { -/// let (message, set_message) = use_state(|| "".to_string()); +/// let message = use_state(|| "".to_string()); /// let message_count = use_ref(|| 0); /// /// let onclick = Callback::from(move |e| { @@ -32,34 +31,27 @@ use std::rc::Rc; /// } /// }); /// -/// let onchange = Callback::from(move |e| { -/// if let ChangeData::Value(value) = e { -/// set_message(value) -/// } -/// }); +/// let onchange = { +/// let message = message.clone(); +/// Callback::from(move |e| { +/// if let ChangeData::Value(value) = e { +/// message.set(value) +/// } +/// }) +/// }; /// /// html! { ///
-/// +/// /// ///
/// } /// } /// ``` -pub fn use_ref(initial_value: InitialProvider) -> Rc> -where - InitialProvider: FnOnce() -> T, -{ - #[derive(Clone)] - struct UseRefState(Rc>); - impl Hook for UseRefState {} - +pub fn use_ref(initial_value: impl FnOnce() -> T + 'static) -> Rc> { use_hook( - |state: &mut UseRefState, hook_callback| { - // we need it to be a specific closure type, even if we never use it - let _ignored = || hook_callback(|_| false, false); - state.0.clone() - }, - move || UseRefState(Rc::new(RefCell::new(initial_value()))), + || Rc::new(RefCell::new(initial_value())), + |state, _| state.clone(), + |_| {}, ) } diff --git a/packages/yew-functional/src/hooks/use_state.rs b/packages/yew-functional/src/hooks/use_state.rs index 99cbc102a05..b5beb04d2dd 100644 --- a/packages/yew-functional/src/hooks/use_state.rs +++ b/packages/yew-functional/src/hooks/use_state.rs @@ -1,6 +1,11 @@ -use super::{use_hook, Hook}; +use crate::use_hook; +use std::ops::Deref; use std::rc::Rc; +struct UseState { + current: Rc, +} + /// This hook is used to mange state in a function component. /// /// # Example @@ -11,53 +16,75 @@ 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 counter = use_state(|| 0); /// let onclick = { -/// let counter = Rc::clone(&counter); -/// Callback::from(move |_| set_counter(*counter + 1)) +/// let counter = counter.clone(); +/// Callback::from(move |_| counter.set(*counter + 1)) /// }; /// +/// /// html! { ///
/// ///

/// { "Current value: " } -/// { counter } +/// { *counter } ///

///
/// } /// } /// ``` -pub fn use_state(initial_state_fn: F) -> (Rc, Rc) -where - F: FnOnce() -> T, - T: 'static, -{ - struct UseStateState { - current: Rc, - } - impl Hook for UseStateState {} +pub fn use_state T + 'static>(initial_state_fn: F) -> UseStateHandle { use_hook( - |prev: &mut UseStateState, hook_callback| { - let current = prev.current.clone(); - ( - current, - Rc::new(move |o: T| { - hook_callback( - |state: &mut UseStateState| { - state.current = Rc::new(o); - true - }, - false, // run pre render - ) - }), - ) - }, - move || UseStateState { + // 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| { + st.current = Rc::new(new_val); + true + }) + }); + + let current = hook.current.clone(); + UseStateHandle { + value: current, + setter, + } + }, + // Destructor + |_| {}, ) } + +/// State handle for the [`use_state`] hook. +pub struct UseStateHandle { + value: Rc, + setter: Rc, +} + +impl UseStateHandle { + pub fn set(&self, value: T) { + (self.setter)(value) + } +} + +impl Deref for UseStateHandle { + type Target = T; + + fn deref(&self) -> &Self::Target { + &(*self.value) + } +} + +impl Clone for UseStateHandle { + fn clone(&self) -> Self { + Self { + value: Rc::clone(&self.value), + setter: Rc::clone(&self.setter), + } + } +} diff --git a/packages/yew-functional/src/lib.rs b/packages/yew-functional/src/lib.rs index b659e6c8e39..fcb7a02d26c 100644 --- a/packages/yew-functional/src/lib.rs +++ b/packages/yew-functional/src/lib.rs @@ -61,7 +61,7 @@ struct HookState { counter: usize, scope: AnyScope, process_message: ProcessMessage, - hooks: Vec>, + hooks: Vec>>, destroy_listeners: Vec>, } @@ -70,24 +70,11 @@ pub trait FunctionProvider { fn run(props: &Self::TProps) -> Html; } -#[derive(Clone, Default)] -struct MsgQueue(Rc>>); - -impl MsgQueue { - fn push(&self, msg: Msg) { - self.0.borrow_mut().push(msg); - } - - fn drain(&self) -> Vec { - self.0.borrow_mut().drain(..).collect() - } -} - pub struct FunctionComponent { _never: std::marker::PhantomData, props: T::TProps, - link: ComponentLink, hook_state: RefCell, + link: ComponentLink, message_queue: MsgQueue, } @@ -112,6 +99,7 @@ where fn create(props: Self::Properties, link: ComponentLink) -> Self { let scope = AnyScope::from(link.clone()); let message_queue = MsgQueue::default(); + Self { _never: std::marker::PhantomData::default(), props, @@ -168,3 +156,77 @@ pub(crate) fn get_current_scope() -> Option { None } } + +#[derive(Clone, Default)] +struct MsgQueue(Rc>>); + +impl MsgQueue { + fn push(&self, msg: Msg) { + self.0.borrow_mut().push(msg); + } + + fn drain(&self) -> Vec { + self.0.borrow_mut().drain(..).collect() + } +} + +/// The `HookUpdater` provides a convenient interface for hooking into the lifecycle of +/// the underlying Yew Component that backs the function component. +/// +/// Two interfaces are provided - callback and post_render. +/// - `callback` allows the creation of regular yew callbacks on the host component. +/// - `post_render` allows the creation of events that happen after a render is complete. +/// +/// See [`use_effect`](hooks::use_effect()) and [`use_context`](hooks::use_context()) +/// for more details on how to use the hook updater to provide function components +/// the necessary callbacks to update the underlying state. +#[derive(Clone)] +pub struct HookUpdater { + hook: Rc>, + process_message: ProcessMessage, +} +impl HookUpdater { + pub fn callback(&self, cb: F) + where + F: FnOnce(&mut T) -> bool + 'static, + { + let internal_hook_state = self.hook.clone(); + let process_message = self.process_message.clone(); + + // Update the component + // We're calling "link.send_message", so we're not calling it post-render + let post_render = false; + process_message( + Box::new(move || { + let mut r = internal_hook_state.borrow_mut(); + let hook: &mut T = r + .downcast_mut() + .expect("internal error: hook downcasted to wrong type"); + cb(hook) + }), + post_render, + ); + } + + pub fn post_render(&self, cb: F) + where + F: FnOnce(&mut T) -> bool + 'static, + { + let internal_hook_state = self.hook.clone(); + let process_message = self.process_message.clone(); + + // Update the component + // We're calling "messag_equeue.push", so not calling it post-render + let post_render = true; + process_message( + Box::new(move || { + let mut hook = internal_hook_state.borrow_mut(); + let hook: &mut T = hook + .downcast_mut() + .expect("internal error: hook downcasted to wrong type"); + cb(hook) + }), + post_render, + ); + } +} diff --git a/packages/yew-functional/tests/use_context.rs b/packages/yew-functional/tests/use_context.rs index f4fbe39f1f7..d4c83ab3dda 100644 --- a/packages/yew-functional/tests/use_context.rs +++ b/packages/yew-functional/tests/use_context.rs @@ -233,36 +233,37 @@ fn use_context_update_works() { fn run(_props: &Self::TProps) -> Html { type MyContextProvider = ContextProvider>; - let (ctx, set_ctx) = use_state(|| MyContext("hello".into())); + let 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_rc = 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; - } - _ => (), - }; - || {} - }); - + { + let ctx = ctx.clone(); + use_effect(move || { + let count = *rendered.borrow(); + match count { + 0 => { + ctx.set(MyContext("world".into())); + *rendered.borrow_mut() += 1; + } + 1 => { + // force test-2 to re-render. + magic_rc.set(1); + *rendered.borrow_mut() += 1; + } + 2 => { + ctx.set(MyContext("hello world!".into())); + *rendered.borrow_mut() += 1; + } + _ => (), + }; + || {} + }); + } return html! { - + diff --git a/packages/yew-functional/tests/use_effect.rs b/packages/yew-functional/tests/use_effect.rs index 80f3a549af9..53414c7a68d 100644 --- a/packages/yew-functional/tests/use_effect.rs +++ b/packages/yew-functional/tests/use_effect.rs @@ -56,16 +56,16 @@ fn use_effect_destroys_on_component_drop() { type TProps = WrapperProps; fn run(props: &Self::TProps) -> Html { - let (show, set_show) = use_state(|| true); + let show = use_state(|| true); if *show { - let effect_called: Rc = Rc::new(move || set_show(false)); - return html! { + let effect_called: Rc = { Rc::new(move || show.set(false)) }; + html! { - }; + } } else { - return html! { -
{"EMPTY"}
- }; + html! { +
{ "EMPTY" }
+ } } } } @@ -88,13 +88,13 @@ fn use_effect_works_many_times() { type TProps = (); fn run(_: &Self::TProps) -> Html { - let (counter, set_counter) = use_state(|| 0); + let counter = use_state(|| 0); let counter_clone = counter.clone(); use_effect_with_deps( move |_| { if *counter_clone < 4 { - set_counter(*counter_clone + 1); + counter_clone.set(*counter_clone + 1); } || {} }, @@ -103,9 +103,9 @@ fn use_effect_works_many_times() { return html! {
- {"The test result is"} -
{counter}
- {"\n"} + { "The test result is" } +
{ *counter }
+ { "\n" }
}; } @@ -125,12 +125,12 @@ fn use_effect_works_once() { type TProps = (); fn run(_: &Self::TProps) -> Html { - let (counter, set_counter) = use_state(|| 0); + let counter = use_state(|| 0); let counter_clone = counter.clone(); use_effect_with_deps( move |_| { - set_counter(*counter_clone + 1); + counter_clone.set(*counter_clone + 1); || panic!("Destructor should not have been called") }, (), @@ -138,9 +138,9 @@ fn use_effect_works_once() { return html! {
- {"The test result is"} -
{counter}
- {"\n"} + { "The test result is" } +
{ *counter }
+ { "\n" }
}; } @@ -164,7 +164,7 @@ fn use_effect_refires_on_dependency_change() { 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); + let counter = use_state(|| 0); use_effect_with_deps( move |dep| { let mut ref_mut = number_ref_c.borrow_mut(); @@ -175,9 +175,9 @@ fn use_effect_refires_on_dependency_change() { } else { assert_eq!(dep, &1); } - set_counter(10); // we just need to make sure it does not panic + counter.set(10); // we just need to make sure it does not panic move || { - set_counter(11); + counter.set(11); *number_ref2_c.borrow_mut().deref_mut() += 1; } }, diff --git a/packages/yew-functional/tests/use_reducer.rs b/packages/yew-functional/tests/use_reducer.rs index d6697812520..41f9301d5fd 100644 --- a/packages/yew-functional/tests/use_reducer.rs +++ b/packages/yew-functional/tests/use_reducer.rs @@ -18,7 +18,7 @@ fn use_reducer_works() { struct CounterState { counter: i32, } - let (counter, dispatch) = use_reducer_with_init( + let counter = use_reducer_with_init( |prev: std::rc::Rc, action: i32| CounterState { counter: prev.counter + action, }, @@ -28,9 +28,10 @@ fn use_reducer_works() { }, ); + let counter_clone = counter.clone(); use_effect_with_deps( move |_| { - dispatch(1); + counter_clone.dispatch(1); || {} }, (), diff --git a/packages/yew-functional/tests/use_ref.rs b/packages/yew-functional/tests/use_ref.rs index 3e2f45511b1..c562b226fc9 100644 --- a/packages/yew-functional/tests/use_ref.rs +++ b/packages/yew-functional/tests/use_ref.rs @@ -17,9 +17,9 @@ fn use_ref_works() { 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); + let counter = use_state(|| 0); if *counter < 5 { - set_counter(*counter + 1) + counter.set(*counter + 1) } return html! {
diff --git a/packages/yew-functional/tests/use_state.rs b/packages/yew-functional/tests/use_state.rs index 8ac4523c16e..113537ba9f5 100644 --- a/packages/yew-functional/tests/use_state.rs +++ b/packages/yew-functional/tests/use_state.rs @@ -14,9 +14,9 @@ fn use_state_works() { type TProps = (); fn run(_: &Self::TProps) -> Html { - let (counter, set_counter) = use_state(|| 0); + let counter = use_state(|| 0); if *counter < 5 { - set_counter(*counter + 1) + counter.set(*counter + 1) } return html! {
@@ -41,31 +41,32 @@ fn multiple_use_state_setters() { 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(); + let counter = use_state(|| 0); + let counter_clone = counter.clone(); use_effect_with_deps( move |_| { // 1st location - set_counter_in_use_effect(counter + 1); + counter_clone.set(*counter_clone + 1); || {} }, (), ); - let another_scope = move || { - if counter < 11 { - // 2nd location - set_counter_in_another_scope(counter + 10) + let another_scope = { + let counter = counter.clone(); + move || { + if *counter < 11 { + // 2nd location + counter.set(*counter + 10) + } } }; another_scope(); return html! {
- {"Test Output: "} + { "Test Output: " } // expected output -
{counter}
- {"\n"} +
{ *counter }
+ { "\n" }
}; }