Skip to content

Commit

Permalink
Refactor use ref hooks (yewstack#2093)
Browse files Browse the repository at this point in the history
* Refactor use ref hooks

`use_ref` has been renamed to `use_mut_ref` and `use_ref` has become a
similar hook for immutable reference.

This is different from React but I think lines up nicely with Rust as a
reference is immutable unless specified to be a mut ref.

* fix CI

Co-authored-by: Hamza <muhammadhamza1311@gmail.com>

(cherry picked from commit 071f1b2)
  • Loading branch information
mc1098 authored and Madoshakalaka committed Nov 23, 2021
1 parent 81437fd commit 4efd980
Show file tree
Hide file tree
Showing 7 changed files with 353 additions and 35 deletions.
64 changes: 64 additions & 0 deletions packages/yew-agent/src/hooks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use std::cell::RefCell;
use std::rc::Rc;

use crate::*;
use yew::prelude::*;

/// State handle for [`use_bridge`] hook
pub struct UseBridgeHandle<T>
where
T: Bridged,
{
inner: Rc<RefCell<Box<dyn Bridge<T>>>>,
}

impl<T> UseBridgeHandle<T>
where
T: Bridged,
{
/// Send a message to an agent.
pub fn send(&self, msg: T::Input) {
let mut bridge = self.inner.borrow_mut();
bridge.send(msg);
}
}

/// A hook to bridge to an Agent.
///
/// This hooks will only bridge the agent once over the entire component lifecycle.
///
/// Takes a callback as the only argument. The callback will be updated on every render to make
/// sure captured values (if any) are up to date.
pub fn use_bridge<T, F>(on_output: F) -> UseBridgeHandle<T>
where
T: Bridged,
F: Fn(T::Output) + 'static,
{
let on_output = Rc::new(on_output);

let on_output_clone = on_output.clone();
let on_output_ref = use_mut_ref(move || on_output_clone);

// Refresh the callback on every render.
{
let mut on_output_ref = on_output_ref.borrow_mut();
*on_output_ref = on_output;
}

let bridge = use_mut_ref(move || {
T::bridge(Callback::from(move |output| {
let on_output = on_output_ref.borrow().clone();
on_output(output);
}))
});

UseBridgeHandle { inner: bridge }
}

impl<T: Agent> Clone for UseBridgeHandle<T> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
}
}
}
79 changes: 76 additions & 3 deletions packages/yew/src/functional/hooks/use_ref.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::functional::use_hook;
use crate::{functional::use_hook, NodeRef};
use std::{cell::RefCell, rc::Rc};

/// This hook is used for obtaining a mutable reference to a stateful value.
Expand All @@ -18,7 +18,7 @@ use std::{cell::RefCell, rc::Rc};
/// #[function_component(UseRef)]
/// fn ref_hook() -> Html {
/// let message = use_state(|| "".to_string());
/// let message_count = use_ref(|| 0);
/// let message_count = use_mut_ref(|| 0);
///
/// let onclick = Callback::from(move |e| {
/// let window = yew::utils::window();
Expand Down Expand Up @@ -47,10 +47,83 @@ use std::{cell::RefCell, rc::Rc};
/// }
/// }
/// ```
pub fn use_ref<T: 'static>(initial_value: impl FnOnce() -> T) -> Rc<RefCell<T>> {
pub fn use_mut_ref<T: 'static>(initial_value: impl FnOnce() -> T) -> Rc<RefCell<T>> {
use_hook(
|| Rc::new(RefCell::new(initial_value())),
|state, _| state.clone(),
|_| {},
)
}

/// This hook is used for obtaining a immutable reference to a stateful value.
/// Its state persists across renders.
///
/// If you need a mutable reference, consider using [`use_mut_ref`](super::use_mut_ref).
/// If you need the component to be re-rendered on state change, consider using [`use_state`](super::use_state()).
pub fn use_ref<T: 'static>(initial_value: impl FnOnce() -> T) -> Rc<T> {
use_hook(
|| Rc::new(initial_value()),
|state, _| Rc::clone(state),
|_| {},
)
}

/// This hook is used for obtaining a [`NodeRef`].
/// It persists across renders.
///
/// It is important to note that you do not get notified of state changes.
///
/// # Example
/// ```rust
/// # use wasm_bindgen::{prelude::Closure, JsCast};
/// # use yew::{
/// # function_component, html, use_effect_with_deps, use_node_ref
/// # };
/// # use web_sys::{Event, HtmlElement};
///
/// #[function_component(UseNodeRef)]
/// pub fn node_ref_hook() -> Html {
/// let div_ref = use_node_ref();
///
/// {
/// let div_ref = div_ref.clone();
///
/// use_effect_with_deps(
/// |div_ref| {
/// let div = div_ref
/// .cast::<HtmlElement>()
/// .expect("div_ref not attached to div element");
///
/// let listener = Closure::<dyn Fn(Event)>::wrap(Box::new(|_| {
/// web_sys::console::log_1(&"Clicked!".into());
/// }));
///
/// div.add_event_listener_with_callback(
/// "click",
/// listener.as_ref().unchecked_ref(),
/// )
/// .unwrap();
///
/// move || {
/// div.remove_event_listener_with_callback(
/// "click",
/// listener.as_ref().unchecked_ref(),
/// )
/// .unwrap();
/// }
/// },
/// div_ref,
/// );
/// }
///
/// html! {
/// <div ref={div_ref}>
/// { "Click me and watch the console log!" }
/// </div>
/// }
/// }
///
/// ```
pub fn use_node_ref() -> NodeRef {
use_hook(NodeRef::default, |state, _| state.clone(), |_| {})
}
8 changes: 4 additions & 4 deletions packages/yew/tests/use_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use common::obtain_result_by_id;
use std::rc::Rc;
use wasm_bindgen_test::*;
use yew::functional::{
use_context, use_effect, use_ref, use_state, FunctionComponent, FunctionProvider,
use_context, use_effect, use_mut_ref, use_state, FunctionComponent, FunctionProvider,
};
use yew::{html, Children, ContextProvider, Html, Properties};

Expand Down Expand Up @@ -185,7 +185,7 @@ fn use_context_update_works() {
type TProps = RenderCounterProps;

fn run(props: &Self::TProps) -> Html {
let counter = use_ref(|| 0);
let counter = use_mut_ref(|| 0);
*counter.borrow_mut() += 1;
return html! {
<>
Expand All @@ -210,7 +210,7 @@ fn use_context_update_works() {
type TProps = ContextOutletProps;

fn run(props: &Self::TProps) -> Html {
let counter = use_ref(|| 0);
let counter = use_mut_ref(|| 0);
*counter.borrow_mut() += 1;

let ctx = use_context::<Rc<MyContext>>().expect("context not passed down");
Expand All @@ -235,7 +235,7 @@ fn use_context_update_works() {
type MyContextProvider = ContextProvider<Rc<MyContext>>;

let ctx = use_state(|| MyContext("hello".into()));
let rendered = use_ref(|| 0);
let rendered = use_mut_ref(|| 0);

// this is used to force an update specific to test-2
let magic_rc = use_state(|| 0);
Expand Down
6 changes: 3 additions & 3 deletions packages/yew/tests/use_effect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::ops::{Deref, DerefMut};
use std::rc::Rc;
use wasm_bindgen_test::*;
use yew::functional::{
use_effect_with_deps, use_ref, use_state, FunctionComponent, FunctionProvider,
use_effect_with_deps, use_mut_ref, use_state, FunctionComponent, FunctionProvider,
};
use yew::{html, Html, Properties};

Expand Down Expand Up @@ -160,9 +160,9 @@ fn use_effect_refires_on_dependency_change() {
type TProps = ();

fn run(_: &Self::TProps) -> Html {
let number_ref = use_ref(|| 0);
let number_ref = use_mut_ref(|| 0);
let number_ref_c = number_ref.clone();
let number_ref2 = use_ref(|| 0);
let number_ref2 = use_mut_ref(|| 0);
let number_ref2_c = number_ref2.clone();
let arg = *number_ref.borrow_mut().deref_mut();
let counter = use_state(|| 0);
Expand Down
142 changes: 124 additions & 18 deletions packages/yew/tests/use_reducer.rs
Original file line number Diff line number Diff line change
@@ -1,32 +1,41 @@
use std::collections::HashSet;
use std::rc::Rc;

use gloo_utils::document;
use wasm_bindgen::JsCast;
use wasm_bindgen_test::*;
use web_sys::HtmlElement;
use yew::prelude::*;

mod common;

use common::obtain_result;
use wasm_bindgen_test::*;
use yew::functional::{
use_effect_with_deps, use_reducer_with_init, FunctionComponent, FunctionProvider,
};
use yew::{html, Html};

wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);

#[derive(Debug)]
struct CounterState {
counter: i32,
}

impl Reducible for CounterState {
type Action = i32;

fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> {
Self {
counter: self.counter + action,
}
.into()
}
}

#[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 = use_reducer_with_init(
|prev: std::rc::Rc<CounterState>, action: i32| CounterState {
counter: prev.counter + action,
},
0,
|initial: i32| CounterState {
counter: initial + 10,
},
);
let counter = use_reducer(|| CounterState { counter: 10 });

let counter_clone = counter.clone();
use_effect_with_deps(
Expand All @@ -47,9 +56,106 @@ fn use_reducer_works() {
}
type UseReducerComponent = FunctionComponent<UseReducerFunction>;
yew::start_app_in_element::<UseReducerComponent>(
yew::utils::document().get_element_by_id("output").unwrap(),
gloo_utils::document().get_element_by_id("output").unwrap(),
);
let result = obtain_result();

assert_eq!(result.as_str(), "11");
}

#[derive(Debug, Clone, PartialEq)]
struct ContentState {
content: HashSet<String>,
}

impl Reducible for ContentState {
type Action = String;

fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> {
let mut self_: Self = (*self).clone();
self_.content.insert(action);
self_.into()
}
}

#[wasm_bindgen_test]
fn use_reducer_eq_works() {
struct UseReducerFunction {}
impl FunctionProvider for UseReducerFunction {
type TProps = ();
fn run(_: &Self::TProps) -> Html {
let content = use_reducer_eq(|| ContentState {
content: HashSet::default(),
});

let render_count = use_mut_ref(|| 0);

let render_count = {
let mut render_count = render_count.borrow_mut();
*render_count += 1;

*render_count
};

let add_content_a = {
let content = content.clone();
Callback::from(move |_| content.dispatch("A".to_string()))
};

let add_content_b = Callback::from(move |_| content.dispatch("B".to_string()));

return html! {
<>
<div>
{"This component has been rendered: "}<span id="result">{render_count}</span>{" Time(s)."}
</div>
<button onclick={add_content_a} id="add-a">{"Add A to Content"}</button>
<button onclick={add_content_b} id="add-b">{"Add B to Content"}</button>
</>
};
}
}
type UseReducerComponent = FunctionComponent<UseReducerFunction>;
yew::start_app_in_element::<UseReducerComponent>(
document().get_element_by_id("output").unwrap(),
);

let result = obtain_result();
assert_eq!(result.as_str(), "1");

document()
.get_element_by_id("add-a")
.unwrap()
.unchecked_into::<HtmlElement>()
.click();

let result = obtain_result();
assert_eq!(result.as_str(), "2");

document()
.get_element_by_id("add-a")
.unwrap()
.unchecked_into::<HtmlElement>()
.click();

let result = obtain_result();
assert_eq!(result.as_str(), "2");

document()
.get_element_by_id("add-b")
.unwrap()
.unchecked_into::<HtmlElement>()
.click();

let result = obtain_result();
assert_eq!(result.as_str(), "3");

document()
.get_element_by_id("add-b")
.unwrap()
.unchecked_into::<HtmlElement>()
.click();

let result = obtain_result();
assert_eq!(result.as_str(), "3");
}
Loading

0 comments on commit 4efd980

Please sign in to comment.