Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions packages/yew-functional/src/hooks/use_context.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -32,7 +35,7 @@ use yew::context::ContextHandle;
pub fn use_context<T: Clone + PartialEq + 'static>() -> Option<T> {
struct UseContextState<T2: Clone + PartialEq + 'static> {
initialized: bool,
context: Option<(T2, ContextHandle<T2>)>,
context: Option<(Context<T2>, ContextListener<T2>)>,
}

let scope = get_current_scope()
Expand All @@ -46,18 +49,15 @@ pub fn use_context<T: Clone + PartialEq + 'static>() -> Option<T> {
|state: &mut UseContextState<T>, updater| {
if !state.initialized {
state.initialized = true;
let callback = move |ctx: T| {
updater.callback(|state: &mut UseContextState<T>| {
if let Some(context) = &mut state.context {
context.0 = ctx;
}
true
});
};
state.context = scope.context::<T>(callback.into());
if let Some(context) = scope.context::<T>() {
let listener = context.register(Callback::from(move |_| {
updater.callback(|_: &mut UseContextState<T>| true);
}));
state.context = Some((context, listener));
}
}

Some(state.context.as_ref()?.0.clone())
Some(state.context.as_ref()?.0.current())
},
|state| {
state.context = None;
Expand Down
2 changes: 1 addition & 1 deletion packages/yew-functional/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ where
}
}

pub(crate) fn get_current_scope() -> Option<AnyScope> {
pub fn get_current_scope() -> Option<AnyScope> {
if CURRENT_HOOK.is_set() {
Some(CURRENT_HOOK.with(|state| state.scope.clone()))
} else {
Expand Down
115 changes: 72 additions & 43 deletions packages/yew/src/context.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand All @@ -22,57 +22,90 @@ pub struct ContextProviderProps<T: Clone + PartialEq> {
#[derive(Debug)]
pub struct ContextProvider<T: Clone + PartialEq + 'static> {
link: ComponentLink<Self>,
context: T,
children: Children,
consumers: RefCell<Slab<Callback<T>>>,
pub(crate) context: Context<T>,
}

/// Owns the connection to a context provider. When dropped, the component will
/// no longer receive updates from the provider.
#[derive(Debug)]
pub struct ContextHandle<T: Clone + PartialEq + 'static> {
provider: Scope<ContextProvider<T>>,
key: usize,
struct ContextState<T: Clone + PartialEq + 'static> {
value: T,
listeners: Slab<Callback<T>>,
}

impl<T: Clone + PartialEq + 'static> Drop for ContextHandle<T> {
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<T: Clone + PartialEq + 'static> {
state: Rc<RefCell<ContextState<T>>>,
}

impl<T: Clone + PartialEq + 'static> Context<T> {
fn new(value: T) -> Self {
Self {
state: Rc::new(RefCell::new(ContextState {
value,
listeners: Slab::new(),
})),
}
}
}

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.
pub(crate) fn subscribe_consumer(&self, callback: Callback<T>) -> (T, ContextHandle<T>) {
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<T>) -> ContextListener<T> {
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<Callback<T>> = 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<T: Clone + PartialEq + 'static> {
context: Context<T>,
key: usize,
}

impl<T: Clone + PartialEq + 'static> Drop for ContextListener<T> {
fn drop(&mut self) {
(*self.context.state)
.borrow_mut()
.listeners
.remove(self.key);
}
}

impl<T: Clone + PartialEq + 'static> Component for ContextProvider<T> {
type Message = ();
type Properties = ContextProviderProps<T>;
Expand All @@ -81,8 +114,7 @@ impl<T: Clone + PartialEq + 'static> Component for ContextProvider<T> {
Self {
link,
children: props.children,
context: props.context,
consumers: RefCell::new(Slab::new()),
context: Context::new(props.context),
}
}

Expand All @@ -98,10 +130,7 @@ impl<T: Clone + PartialEq + 'static> Component for ContextProvider<T> {
true
};

if self.context != props.context {
self.context = props.context;
self.notify_consumers();
}
self.context.store(props.context);

should_render
}
Expand Down
16 changes: 5 additions & 11 deletions packages/yew/src/html/component/scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -79,13 +79,10 @@ impl AnyScope {

/// Accesses a value provided by a parent `ContextProvider` component of the
/// same type.
pub fn context<T: Clone + PartialEq + 'static>(
&self,
callback: Callback<T>,
) -> Option<(T, ContextHandle<T>)> {
pub fn context<T: Clone + PartialEq + 'static>(&self) -> Option<Context<T>> {
let scope = self.find_parent_scope::<ContextProvider<T>>()?;
let component = scope.get_component()?;
Some(component.subscribe_consumer(callback))
Some(component.context.clone())
}
}

Expand Down Expand Up @@ -336,11 +333,8 @@ impl<COMP: Component> Scope<COMP> {

/// Accesses a value provided by a parent `ContextProvider` component of the
/// same type.
pub fn context<T: Clone + PartialEq + 'static>(
&self,
callback: Callback<T>,
) -> Option<(T, ContextHandle<T>)> {
self.to_any().context(callback)
pub fn context<T: Clone + PartialEq + 'static>(&self) -> Option<Context<T>> {
self.to_any().context()
}
}

Expand Down