From dd398b3a3da146ef4efbbe0973d426fadee3f792 Mon Sep 17 00:00:00 2001 From: Diggory Blake Date: Sat, 8 May 2021 16:17:50 +0100 Subject: [PATCH] Bring the context method more inline with other Yew APIs by separating registration from getting the current context value. --- .../yew-functional/src/hooks/use_context.rs | 24 ++-- packages/yew-functional/src/lib.rs | 2 +- packages/yew/src/context.rs | 115 +++++++++++------- packages/yew/src/html/component/scope.rs | 16 +-- 4 files changed, 90 insertions(+), 67 deletions(-) diff --git a/packages/yew-functional/src/hooks/use_context.rs b/packages/yew-functional/src/hooks/use_context.rs index 34beb9a3c4f..c3c81afd0c0 100644 --- a/packages/yew-functional/src/hooks/use_context.rs +++ b/packages/yew-functional/src/hooks/use_context.rs @@ -1,5 +1,8 @@ use crate::{get_current_scope, use_hook}; -use yew::context::ContextHandle; +use yew::{ + context::{Context, ContextListener}, + Callback, +}; /// 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. @@ -32,7 +35,7 @@ use yew::context::ContextHandle; pub fn use_context() -> Option { struct UseContextState { initialized: bool, - context: Option<(T2, ContextHandle)>, + context: Option<(Context, ContextListener)>, } let scope = get_current_scope() @@ -46,18 +49,15 @@ pub fn use_context() -> Option { |state: &mut UseContextState, updater| { if !state.initialized { state.initialized = true; - let callback = move |ctx: T| { - updater.callback(|state: &mut UseContextState| { - if let Some(context) = &mut state.context { - context.0 = ctx; - } - true - }); - }; - state.context = scope.context::(callback.into()); + if let Some(context) = scope.context::() { + let listener = context.register(Callback::from(move |_| { + updater.callback(|_: &mut UseContextState| true); + })); + state.context = Some((context, listener)); + } } - Some(state.context.as_ref()?.0.clone()) + Some(state.context.as_ref()?.0.current()) }, |state| { state.context = None; diff --git a/packages/yew-functional/src/lib.rs b/packages/yew-functional/src/lib.rs index fcb7a02d26c..f57b8434ce2 100644 --- a/packages/yew-functional/src/lib.rs +++ b/packages/yew-functional/src/lib.rs @@ -149,7 +149,7 @@ where } } -pub(crate) fn get_current_scope() -> Option { +pub fn get_current_scope() -> Option { if CURRENT_HOOK.is_set() { Some(CURRENT_HOOK.with(|state| state.scope.clone())) } else { diff --git a/packages/yew/src/context.rs b/packages/yew/src/context.rs index c1d64180dab..43aae527942 100644 --- a/packages/yew/src/context.rs +++ b/packages/yew/src/context.rs @@ -1,9 +1,9 @@ //! This module defines the `ContextProvider` component. -use crate::html::Scope; use crate::{html, Callback, Children, Component, ComponentLink, Html, Properties}; use slab::Slab; use std::cell::RefCell; +use std::rc::Rc; /// Props for [`ContextProvider`] #[derive(Debug, Clone, PartialEq, Properties)] @@ -22,57 +22,90 @@ pub struct ContextProviderProps { #[derive(Debug)] pub struct ContextProvider { link: ComponentLink, - context: T, children: Children, - consumers: RefCell>>, + pub(crate) context: Context, } -/// Owns the connection to a context provider. When dropped, the component will -/// no longer receive updates from the provider. #[derive(Debug)] -pub struct ContextHandle { - provider: Scope>, - key: usize, +struct ContextState { + value: T, + listeners: Slab>, } -impl Drop for ContextHandle { - fn drop(&mut self) { - if let Some(component) = self.provider.get_component() { - component.consumers.borrow_mut().remove(self.key); +/// A context returned by `scope.context()`. This can be used to access the +/// current context value, or register a callback for when the value changes. +#[derive(Debug, Clone)] +pub struct Context { + state: Rc>>, +} + +impl Context { + fn new(value: T) -> Self { + Self { + state: Rc::new(RefCell::new(ContextState { + value, + listeners: Slab::new(), + })), } } -} -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. - pub(crate) fn subscribe_consumer(&self, callback: Callback) -> (T, ContextHandle) { - let ctx = self.context.clone(); - let key = self.consumers.borrow_mut().insert(callback); - - ( - ctx, - ContextHandle { - provider: self.link.clone(), - key, - }, - ) + /// Get the current context value. + pub fn current(&self) -> T { + self.state.borrow().value.clone() + } + + /// Register a callback to be called whenever the context changes. + /// The callback will be unregistered when the listener is dropped. + pub fn register(&self, callback: Callback) -> ContextListener { + let key = (*self.state).borrow_mut().listeners.insert(callback); + ContextListener { + context: self.clone(), + key, + } } + fn store(&self, value: T) { + let triggers = { + let mut state = (*self.state).borrow_mut(); + if state.value != value { + state.value = value; + state + .listeners + .iter() + .map(|(_, callback)| { + let value = state.value.clone(); + let callback = callback.clone(); + move || callback.emit(value) + }) + .collect() + } else { + Vec::new() + } + }; - /// Notify all subscribed consumers and remove dropped consumers from the list. - fn notify_consumers(&mut self) { - let consumers: Vec> = self - .consumers - .borrow() - .iter() - .map(|(_, v)| v.clone()) - .collect(); - for consumer in consumers { - consumer.emit(self.context.clone()); + // Call into user-code only once state is no longer borrowed. + for trigger in triggers { + trigger(); } } } +/// Owns the connection to a context provider. When dropped, the component will +/// no longer receive updates from the provider. +#[derive(Debug)] +pub struct ContextListener { + context: Context, + key: usize, +} + +impl Drop for ContextListener { + fn drop(&mut self) { + (*self.context.state) + .borrow_mut() + .listeners + .remove(self.key); + } +} + impl Component for ContextProvider { type Message = (); type Properties = ContextProviderProps; @@ -81,8 +114,7 @@ impl Component for ContextProvider { Self { link, children: props.children, - context: props.context, - consumers: RefCell::new(Slab::new()), + context: Context::new(props.context), } } @@ -98,10 +130,7 @@ impl Component for ContextProvider { true }; - if self.context != props.context { - self.context = props.context; - self.notify_consumers(); - } + self.context.store(props.context); should_render } diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index 9890a05cee0..11305fbf75a 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -7,7 +7,7 @@ use super::{ Component, }; use crate::callback::Callback; -use crate::context::{ContextHandle, ContextProvider}; +use crate::context::{Context, ContextProvider}; use crate::html::NodeRef; use crate::scheduler::{self, Shared}; use crate::utils::document; @@ -79,13 +79,10 @@ impl AnyScope { /// Accesses a value provided by a parent `ContextProvider` component of the /// same type. - pub fn context( - &self, - callback: Callback, - ) -> Option<(T, ContextHandle)> { + pub fn context(&self) -> Option> { let scope = self.find_parent_scope::>()?; let component = scope.get_component()?; - Some(component.subscribe_consumer(callback)) + Some(component.context.clone()) } } @@ -336,11 +333,8 @@ impl Scope { /// Accesses a value provided by a parent `ContextProvider` component of the /// same type. - pub fn context( - &self, - callback: Callback, - ) -> Option<(T, ContextHandle)> { - self.to_any().context(callback) + pub fn context(&self) -> Option> { + self.to_any().context() } }