Skip to content

Commit 3678d79

Browse files
committed
Bring context to standard components
1 parent 21eca25 commit 3678d79

File tree

5 files changed

+175
-158
lines changed

5 files changed

+175
-158
lines changed
Lines changed: 24 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -1,124 +1,6 @@
11
use super::{use_hook, Hook};
22
use crate::get_current_scope;
3-
use std::any::TypeId;
4-
use std::cell::RefCell;
5-
use std::rc::{Rc, Weak};
6-
use std::{iter, mem};
7-
use yew::html;
8-
use yew::html::{AnyScope, Scope};
9-
use yew::{Children, Component, ComponentLink, Html, Properties};
10-
11-
type ConsumerCallback<T> = Box<dyn Fn(Rc<T>)>;
12-
13-
/// Props for [`ContextProvider`]
14-
#[derive(Clone, PartialEq, Properties)]
15-
pub struct ContextProviderProps<T: Clone + PartialEq> {
16-
pub context: T,
17-
pub children: Children,
18-
}
19-
20-
/// The context provider component.
21-
///
22-
/// Every child (direct or indirect) of this component may access the context value.
23-
/// Currently the only way to consume the context is using the [`use_context`] hook.
24-
pub struct ContextProvider<T: Clone + PartialEq + 'static> {
25-
context: Rc<T>,
26-
children: Children,
27-
consumers: RefCell<Vec<Weak<ConsumerCallback<T>>>>,
28-
}
29-
30-
impl<T: Clone + PartialEq> ContextProvider<T> {
31-
/// Add the callback to the subscriber list to be called whenever the context changes.
32-
/// The consumer is unsubscribed as soon as the callback is dropped.
33-
fn subscribe_consumer(&self, mut callback: Weak<ConsumerCallback<T>>) {
34-
let mut consumers = self.consumers.borrow_mut();
35-
// consumers re-subscribe on every render. Try to keep the subscriber list small by reusing dead slots.
36-
for cb in consumers.iter_mut() {
37-
if cb.strong_count() == 0 {
38-
mem::swap(cb, &mut callback);
39-
return;
40-
}
41-
}
42-
43-
// no slot to reuse, this is a new consumer
44-
consumers.push(callback);
45-
}
46-
47-
/// Notify all subscribed consumers and remove dropped consumers from the list.
48-
fn notify_consumers(&mut self) {
49-
let context = &self.context;
50-
self.consumers.borrow_mut().retain(|cb| {
51-
if let Some(cb) = cb.upgrade() {
52-
cb(Rc::clone(context));
53-
true
54-
} else {
55-
false
56-
}
57-
});
58-
}
59-
}
60-
61-
impl<T: Clone + PartialEq + 'static> Component for ContextProvider<T> {
62-
type Message = ();
63-
type Properties = ContextProviderProps<T>;
64-
65-
fn create(props: Self::Properties, _link: ComponentLink<Self>) -> Self {
66-
Self {
67-
children: props.children,
68-
context: Rc::new(props.context),
69-
consumers: RefCell::new(Vec::new()),
70-
}
71-
}
72-
73-
fn update(&mut self, _msg: Self::Message) -> bool {
74-
true
75-
}
76-
77-
fn change(&mut self, props: Self::Properties) -> bool {
78-
let should_render = if self.children == props.children {
79-
false
80-
} else {
81-
self.children = props.children;
82-
true
83-
};
84-
85-
let new_context = Rc::new(props.context);
86-
if self.context != new_context {
87-
self.context = new_context;
88-
self.notify_consumers();
89-
}
90-
91-
should_render
92-
}
93-
94-
fn view(&self) -> Html {
95-
html! { <>{ self.children.clone() }</> }
96-
}
97-
}
98-
99-
fn find_context_provider_scope<T: Clone + PartialEq + 'static>(
100-
scope: &AnyScope,
101-
) -> Option<Scope<ContextProvider<T>>> {
102-
let expected_type_id = TypeId::of::<ContextProvider<T>>();
103-
iter::successors(Some(scope), |scope| scope.get_parent())
104-
.filter(|scope| scope.get_type_id() == &expected_type_id)
105-
.cloned()
106-
.map(AnyScope::downcast::<ContextProvider<T>>)
107-
.next()
108-
}
109-
110-
fn with_provider_component<T, F, R>(
111-
provider_scope: &Option<Scope<ContextProvider<T>>>,
112-
f: F,
113-
) -> Option<R>
114-
where
115-
T: Clone + PartialEq,
116-
F: FnOnce(&ContextProvider<T>) -> R,
117-
{
118-
provider_scope
119-
.as_ref()
120-
.and_then(|scope| scope.get_component().map(|comp| f(&*comp)))
121-
}
3+
use yew::context::ContextHandle;
1224

1235
/// Hook for consuming context values in function components.
1246
/// The context of the type passed as `T` is returned. If there is no such context in scope, `None` is returned.
@@ -148,17 +30,14 @@ where
14830
/// }
14931
/// }
15032
/// ```
151-
pub fn use_context<T: Clone + PartialEq + 'static>() -> Option<Rc<T>> {
33+
pub fn use_context<T: Clone + PartialEq + 'static>() -> Option<T> {
15234
struct UseContextState<T2: Clone + PartialEq + 'static> {
153-
provider_scope: Option<Scope<ContextProvider<T2>>>,
154-
current_context: Option<Rc<T2>>,
155-
callback: Option<Rc<ConsumerCallback<T2>>>,
35+
initialized: bool,
36+
context: Option<(T2, ContextHandle<T2>)>,
15637
}
15738
impl<T: Clone + PartialEq + 'static> Hook for UseContextState<T> {
15839
fn tear_down(&mut self) {
159-
if let Some(cb) = self.callback.take() {
160-
drop(cb);
161-
}
40+
self.context = None;
16241
}
16342
}
16443

@@ -167,31 +46,27 @@ pub fn use_context<T: Clone + PartialEq + 'static>() -> Option<Rc<T>> {
16746

16847
use_hook(
16948
|state: &mut UseContextState<T>, hook_callback| {
170-
state.callback = Some(Rc::new(Box::new(move |ctx: Rc<T>| {
171-
hook_callback(
172-
|state: &mut UseContextState<T>| {
173-
state.current_context = Some(ctx);
174-
true
175-
},
176-
false, // run pre render
177-
);
178-
})));
179-
let weak_cb = Rc::downgrade(state.callback.as_ref().unwrap());
180-
with_provider_component(&state.provider_scope, |comp| {
181-
comp.subscribe_consumer(weak_cb)
182-
});
49+
if !state.initialized {
50+
state.initialized = true;
51+
let callback = move |ctx: T| {
52+
hook_callback(
53+
|state: &mut UseContextState<T>| {
54+
if let Some(context) = &mut state.context {
55+
context.0 = ctx;
56+
}
57+
true
58+
},
59+
false, // run pre render
60+
);
61+
};
62+
state.context = scope.context::<T>(callback.into());
63+
}
18364

184-
state.current_context.clone()
65+
Some(state.context.as_ref()?.0.clone())
18566
},
186-
move || {
187-
let provider_scope = find_context_provider_scope::<T>(&scope);
188-
let current_context =
189-
with_provider_component(&provider_scope, |comp| Rc::clone(&comp.context));
190-
UseContextState {
191-
provider_scope,
192-
current_context,
193-
callback: None,
194-
}
67+
move || UseContextState {
68+
initialized: false,
69+
context: None,
19570
},
19671
)
19772
}

packages/yew-functional/tests/use_context.rs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@ mod common;
33
use common::obtain_result_by_id;
44
use std::rc::Rc;
55
use wasm_bindgen_test::*;
6-
use yew::{html, App, Children, Html, Properties};
6+
use yew::{html, App, Children, ContextProvider, Html, Properties};
77
use yew_functional::{
8-
use_context, use_effect, use_ref, use_state, ContextProvider, FunctionComponent,
9-
FunctionProvider,
8+
use_context, use_effect, use_ref, use_state, FunctionComponent, FunctionProvider,
109
};
1110

1211
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
@@ -91,8 +90,8 @@ fn use_context_works_with_multiple_types() {
9190
type TProps = ();
9291

9392
fn run(_props: &Self::TProps) -> Html {
94-
assert_eq!(use_context::<ContextA>(), Some(Rc::new(ContextA(2))));
95-
assert_eq!(use_context::<ContextB>(), Some(Rc::new(ContextB(1))));
93+
assert_eq!(use_context::<ContextA>(), Some(ContextA(2)));
94+
assert_eq!(use_context::<ContextB>(), Some(ContextB(1)));
9695

9796
return html! {};
9897
}
@@ -104,8 +103,8 @@ fn use_context_works_with_multiple_types() {
104103
type TProps = ();
105104

106105
fn run(_props: &Self::TProps) -> Html {
107-
assert_eq!(use_context::<ContextA>(), Some(Rc::new(ContextA(0))));
108-
assert_eq!(use_context::<ContextB>(), Some(Rc::new(ContextB(1))));
106+
assert_eq!(use_context::<ContextA>(), Some(ContextA(0)));
107+
assert_eq!(use_context::<ContextB>(), Some(ContextB(1)));
109108

110109
return html! {};
111110
}
@@ -117,7 +116,7 @@ fn use_context_works_with_multiple_types() {
117116
type TProps = ();
118117

119118
fn run(_props: &Self::TProps) -> Html {
120-
assert_eq!(use_context::<ContextA>(), Some(Rc::new(ContextA(0))));
119+
assert_eq!(use_context::<ContextA>(), Some(ContextA(0)));
121120
assert_eq!(use_context::<ContextB>(), None);
122121

123122
return html! {};

packages/yew/src/context.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
//! This module defines the `ContextProvider` component.
2+
3+
use crate::html::Scope;
4+
use crate::{html, Callback, Children, Component, ComponentLink, Html, Properties};
5+
use slab::Slab;
6+
use std::cell::RefCell;
7+
8+
/// Props for [`ContextProvider`]
9+
#[derive(Debug, Clone, PartialEq, Properties)]
10+
pub struct ContextProviderProps<T: Clone + PartialEq> {
11+
/// Context value to be passed down
12+
pub context: T,
13+
/// Children
14+
pub children: Children,
15+
}
16+
17+
/// The context provider component.
18+
///
19+
/// Every child (direct or indirect) of this component may access the context value.
20+
/// Currently the only way to consume the context is using the [`use_context`] hook.
21+
#[derive(Debug)]
22+
pub struct ContextProvider<T: Clone + PartialEq + 'static> {
23+
link: ComponentLink<Self>,
24+
context: T,
25+
children: Children,
26+
consumers: RefCell<Slab<Callback<T>>>,
27+
}
28+
29+
/// Owns the connection to a context provider. When dropped, the component will
30+
/// no longer receive updates from the provider.
31+
#[derive(Debug)]
32+
pub struct ContextHandle<T: Clone + PartialEq + 'static> {
33+
provider: Scope<ContextProvider<T>>,
34+
key: usize,
35+
}
36+
37+
impl<T: Clone + PartialEq + 'static> Drop for ContextHandle<T> {
38+
fn drop(&mut self) {
39+
if let Some(component) = self.provider.get_component() {
40+
component.consumers.borrow_mut().remove(self.key);
41+
}
42+
}
43+
}
44+
45+
impl<T: Clone + PartialEq> ContextProvider<T> {
46+
/// Add the callback to the subscriber list to be called whenever the context changes.
47+
/// The consumer is unsubscribed as soon as the callback is dropped.
48+
pub(crate) fn subscribe_consumer(&self, callback: Callback<T>) -> (T, ContextHandle<T>) {
49+
let ctx = self.context.clone();
50+
let key = self.consumers.borrow_mut().insert(callback);
51+
52+
(
53+
ctx,
54+
ContextHandle {
55+
provider: self.link.clone(),
56+
key,
57+
},
58+
)
59+
}
60+
61+
/// Notify all subscribed consumers and remove dropped consumers from the list.
62+
fn notify_consumers(&mut self) {
63+
let consumers: Vec<Callback<T>> = self
64+
.consumers
65+
.borrow()
66+
.iter()
67+
.map(|(_, v)| v.clone())
68+
.collect();
69+
for consumer in consumers {
70+
consumer.emit(self.context.clone());
71+
}
72+
}
73+
}
74+
75+
impl<T: Clone + PartialEq + 'static> Component for ContextProvider<T> {
76+
type Message = ();
77+
type Properties = ContextProviderProps<T>;
78+
79+
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
80+
Self {
81+
link,
82+
children: props.children,
83+
context: props.context,
84+
consumers: RefCell::new(Slab::new()),
85+
}
86+
}
87+
88+
fn update(&mut self, _msg: Self::Message) -> bool {
89+
true
90+
}
91+
92+
fn change(&mut self, props: Self::Properties) -> bool {
93+
let should_render = if self.children == props.children {
94+
false
95+
} else {
96+
self.children = props.children;
97+
true
98+
};
99+
100+
if self.context != props.context {
101+
self.context = props.context;
102+
self.notify_consumers();
103+
}
104+
105+
should_render
106+
}
107+
108+
fn view(&self) -> Html {
109+
html! { <>{ self.children.clone() }</> }
110+
}
111+
}

0 commit comments

Comments
 (0)