Make Switch to accept a closure as render function directly (#2659)

* RenderFn can be be directly Fn() now.

* Switch switch to function component.

* Make Link a function component.

* Remove non-exhaustive.

* Add migration guide.

* Update Website Docs.

* Fix CI.

* Pushing to Navigator no longer requires an owned instance.

* Fix CI.

* Fix code size.

* Further optimisation.
This commit is contained in:
Kaede Hoshikawa 2022-05-06 09:56:49 +09:00 committed by GitHub
parent 27da544cae
commit 00b94d0051
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 223 additions and 294 deletions

View File

@ -37,7 +37,7 @@ pub fn App() -> Html {
<Nav /> <Nav />
<main> <main>
<Switch<Route> render={Switch::render(switch)} /> <Switch<Route> render={switch} />
</main> </main>
<footer class="footer"> <footer class="footer">
<div class="content has-text-centered"> <div class="content has-text-centered">
@ -71,7 +71,7 @@ pub fn ServerApp(props: &ServerAppProps) -> Html {
<Nav /> <Nav />
<main> <main>
<Switch<Route> render={Switch::render(switch)} /> <Switch<Route> render={switch} />
</main> </main>
<footer class="footer"> <footer class="footer">
<div class="content has-text-centered"> <div class="content has-text-centered">
@ -87,8 +87,8 @@ pub fn ServerApp(props: &ServerAppProps) -> Html {
} }
} }
fn switch(routes: &Route) -> Html { fn switch(routes: Route) -> Html {
match routes.clone() { match routes {
Route::Post { id } => { Route::Post { id } => {
html! { <Post seed={id} /> } html! { <Post seed={id} /> }
} }

View File

@ -62,7 +62,7 @@ impl Component for App {
{ self.view_nav(ctx.link()) } { self.view_nav(ctx.link()) }
<main> <main>
<Switch<Route> render={Switch::render(switch)} /> <Switch<Route> render={switch} />
</main> </main>
<footer class="footer"> <footer class="footer">
<div class="content has-text-centered"> <div class="content has-text-centered">
@ -124,8 +124,8 @@ impl App {
} }
} }
fn switch(routes: &Route) -> Html { fn switch(routes: Route) -> Html {
match routes.clone() { match routes {
Route::Post { id } => { Route::Post { id } => {
html! { <Post seed={id} /> } html! { <Post seed={id} /> }
} }

View File

@ -1,12 +1,10 @@
use std::marker::PhantomData;
use serde::Serialize; use serde::Serialize;
use wasm_bindgen::UnwrapThrowExt; use wasm_bindgen::UnwrapThrowExt;
use yew::prelude::*; use yew::prelude::*;
use yew::virtual_dom::AttrValue; use yew::virtual_dom::AttrValue;
use crate::navigator::NavigatorKind; use crate::navigator::NavigatorKind;
use crate::scope_ext::RouterScopeExt; use crate::prelude::*;
use crate::{utils, Routable}; use crate::{utils, Routable};
/// Props for [`Link`] /// Props for [`Link`]
@ -31,96 +29,65 @@ where
} }
/// A wrapper around `<a>` tag to be used with [`Router`](crate::Router) /// A wrapper around `<a>` tag to be used with [`Router`](crate::Router)
pub struct Link<R, Q = ()> #[function_component]
pub fn Link<R, Q = ()>(props: &LinkProps<R, Q>) -> Html
where where
R: Routable + 'static, R: Routable + 'static,
Q: Clone + PartialEq + Serialize + 'static, Q: Clone + PartialEq + Serialize + 'static,
{ {
_route: PhantomData<R>, let LinkProps {
_query: PhantomData<Q>, classes,
} to,
children,
disabled,
query,
} = props.clone();
pub enum Msg { let navigator = use_navigator().expect_throw("failed to get navigator");
OnClick,
}
impl<R, Q> Component for Link<R, Q> let onclick = {
where let navigator = navigator.clone();
R: Routable + 'static, let to = to.clone();
Q: Clone + PartialEq + Serialize + 'static, let query = query.clone();
{
type Message = Msg;
type Properties = LinkProps<R, Q>;
fn create(_ctx: &Context<Self>) -> Self { Callback::from(move |e: MouseEvent| {
Self {
_route: PhantomData,
_query: PhantomData,
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::OnClick => {
let LinkProps { to, query, .. } = ctx.props();
let navigator = ctx
.link()
.navigator()
.expect_throw("failed to get navigator");
match query {
None => {
navigator.push(to.clone());
}
Some(data) => {
navigator
.push_with_query(to.clone(), data.clone())
.expect_throw("failed push history with query");
}
};
false
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let LinkProps {
classes,
to,
query,
children,
disabled,
..
} = ctx.props().clone();
let onclick = ctx.link().callback(|e: MouseEvent| {
e.prevent_default(); e.prevent_default();
Msg::OnClick
});
let navigator = ctx match query {
.link() None => {
.navigator() navigator.push(&to);
.expect_throw("failed to get navigator"); }
let href: AttrValue = { Some(ref data) => {
let pathname = navigator.route_to_url(to); navigator
let path = query .push_with_query(&to, data)
.and_then(|query| serde_urlencoded::to_string(query).ok()) .expect_throw("failed push history with query");
.and_then(|query| utils::compose_path(&pathname, &query)) }
.unwrap_or_else(|| pathname.to_string());
match navigator.kind() {
NavigatorKind::Hash => format!("#{}", path),
_ => path,
} }
.into() })
}; };
html! {
<a class={classes} let href = {
{href} let route_s = to.to_path();
{onclick} let pathname = navigator.prefix_basename(&route_s);
{disabled} let mut path = query
> .and_then(|query| serde_urlencoded::to_string(query).ok())
{ children } .and_then(|query| utils::compose_path(&pathname, &query))
</a> .unwrap_or_else(|| pathname.into_owned());
if navigator.kind() == NavigatorKind::Hash {
path.insert(0, '#');
} }
AttrValue::from(path)
};
html! {
<a class={classes}
{href}
{onclick}
{disabled}
>
{ children }
</a>
} }
} }

View File

@ -21,7 +21,7 @@ where
let target_route = props.to.clone(); let target_route = props.to.clone();
use_effect(move || { use_effect(move || {
history.push(target_route.clone()); history.push(&target_route);
|| {} || {}
}); });

View File

@ -23,7 +23,7 @@
//! fn secure() -> Html { //! fn secure() -> Html {
//! let navigator = use_navigator().unwrap(); //! let navigator = use_navigator().unwrap();
//! //!
//! let onclick_callback = Callback::from(move |_| navigator.push(Route::Home)); //! let onclick_callback = Callback::from(move |_| navigator.push(&Route::Home));
//! html! { //! html! {
//! <div> //! <div>
//! <h1>{ "Secure" }</h1> //! <h1>{ "Secure" }</h1>
@ -36,12 +36,12 @@
//! fn app() -> Html { //! fn app() -> Html {
//! html! { //! html! {
//! <BrowserRouter> //! <BrowserRouter>
//! <Switch<Route> render={Switch::render(switch)} /> //! <Switch<Route> render={switch} />
//! </BrowserRouter> //! </BrowserRouter>
//! } //! }
//! } //! }
//! //!
//! fn switch(routes: &Route) -> Html { //! fn switch(routes: Route) -> Html {
//! match routes { //! match routes {
//! Route::Home => html! { <h1>{ "Home" }</h1> }, //! Route::Home => html! { <h1>{ "Home" }</h1> },
//! Route::Secure => html! { //! Route::Secure => html! {
@ -79,7 +79,7 @@ pub mod utils;
pub use routable::{AnyRoute, Routable}; pub use routable::{AnyRoute, Routable};
pub use router::{BrowserRouter, HashRouter, Router}; pub use router::{BrowserRouter, HashRouter, Router};
pub use switch::{RenderFn, Switch}; pub use switch::Switch;
pub mod history { pub mod history {
//! A module that provides universal session history and location information. //! A module that provides universal session history and location information.

View File

@ -57,77 +57,94 @@ impl Navigator {
} }
/// Pushes a [`Routable`] entry. /// Pushes a [`Routable`] entry.
pub fn push(&self, route: impl Routable) { pub fn push<R>(&self, route: &R)
self.inner.push(self.route_to_url(route)); where
R: Routable,
{
self.inner.push(self.prefix_basename(&route.to_path()));
} }
/// Replaces the current history entry with provided [`Routable`] and [`None`] state. /// Replaces the current history entry with provided [`Routable`] and [`None`] state.
pub fn replace(&self, route: impl Routable) { pub fn replace<R>(&self, route: &R)
self.inner.replace(self.route_to_url(route)); where
R: Routable,
{
self.inner.replace(self.prefix_basename(&route.to_path()));
} }
/// Pushes a [`Routable`] entry with state. /// Pushes a [`Routable`] entry with state.
pub fn push_with_state<T>(&self, route: impl Routable, state: T) pub fn push_with_state<R, T>(&self, route: &R, state: T)
where where
R: Routable,
T: 'static, T: 'static,
{ {
self.inner.push_with_state(self.route_to_url(route), state); self.inner
.push_with_state(self.prefix_basename(&route.to_path()), state);
} }
/// Replaces the current history entry with provided [`Routable`] and state. /// Replaces the current history entry with provided [`Routable`] and state.
pub fn replace_with_state<T>(&self, route: impl Routable, state: T) pub fn replace_with_state<R, T>(&self, route: &R, state: T)
where where
R: Routable,
T: 'static, T: 'static,
{ {
self.inner self.inner
.replace_with_state(self.route_to_url(route), state); .replace_with_state(self.prefix_basename(&route.to_path()), state);
} }
/// Same as `.push()` but affix the queries to the end of the route. /// Same as `.push()` but affix the queries to the end of the route.
pub fn push_with_query<Q>(&self, route: impl Routable, query: Q) -> NavigationResult<()> pub fn push_with_query<R, Q>(&self, route: &R, query: &Q) -> NavigationResult<()>
where where
R: Routable,
Q: Serialize, Q: Serialize,
{ {
self.inner.push_with_query(self.route_to_url(route), query) self.inner
.push_with_query(self.prefix_basename(&route.to_path()), query)
} }
/// Same as `.replace()` but affix the queries to the end of the route. /// Same as `.replace()` but affix the queries to the end of the route.
pub fn replace_with_query<Q>(&self, route: impl Routable, query: Q) -> NavigationResult<()> pub fn replace_with_query<R, Q>(&self, route: &R, query: &Q) -> NavigationResult<()>
where where
R: Routable,
Q: Serialize, Q: Serialize,
{ {
self.inner self.inner
.replace_with_query(self.route_to_url(route), query) .replace_with_query(self.prefix_basename(&route.to_path()), query)
} }
/// Same as `.push_with_state()` but affix the queries to the end of the route. /// Same as `.push_with_state()` but affix the queries to the end of the route.
pub fn push_with_query_and_state<Q, T>( pub fn push_with_query_and_state<R, Q, T>(
&self, &self,
route: impl Routable, route: &R,
query: Q, query: &Q,
state: T, state: T,
) -> NavigationResult<()> ) -> NavigationResult<()>
where where
R: Routable,
Q: Serialize, Q: Serialize,
T: 'static, T: 'static,
{ {
self.inner self.inner
.push_with_query_and_state(self.route_to_url(route), query, state) .push_with_query_and_state(self.prefix_basename(&route.to_path()), query, state)
} }
/// Same as `.replace_with_state()` but affix the queries to the end of the route. /// Same as `.replace_with_state()` but affix the queries to the end of the route.
pub fn replace_with_query_and_state<Q, T>( pub fn replace_with_query_and_state<R, Q, T>(
&self, &self,
route: impl Routable, route: &R,
query: Q, query: &Q,
state: T, state: T,
) -> NavigationResult<()> ) -> NavigationResult<()>
where where
R: Routable,
Q: Serialize, Q: Serialize,
T: 'static, T: 'static,
{ {
self.inner self.inner.replace_with_query_and_state(
.replace_with_query_and_state(self.route_to_url(route), query, state) self.prefix_basename(&route.to_path()),
query,
state,
)
} }
/// Returns the Navigator kind. /// Returns the Navigator kind.
@ -139,22 +156,17 @@ impl Navigator {
} }
} }
pub(crate) fn route_to_url(&self, route: impl Routable) -> Cow<'static, str> { pub(crate) fn prefix_basename<'a>(&self, route_s: &'a str) -> Cow<'a, str> {
let url = route.to_path(); match self.basename() {
let path = match self.basename() {
Some(base) => { Some(base) => {
let path = format!("{}{}", base, url); if route_s.is_empty() && route_s.is_empty() {
if path.is_empty() {
Cow::from("/") Cow::from("/")
} else { } else {
path.into() Cow::from(format!("{}{}", base, route_s))
} }
} }
None => url.into(), None => route_s.into(),
}; }
path
} }
pub(crate) fn strip_basename<'a>(&self, path: Cow<'a, str>) -> Cow<'a, str> { pub(crate) fn strip_basename<'a>(&self, path: Cow<'a, str>) -> Cow<'a, str> {

View File

@ -30,12 +30,16 @@ pub struct NavigatorHandle {
/// # use wasm_bindgen::UnwrapThrowExt; /// # use wasm_bindgen::UnwrapThrowExt;
/// # use yew::prelude::*; /// # use yew::prelude::*;
/// # use yew_router::prelude::*; /// # use yew_router::prelude::*;
/// # use yew_router::components::{LinkProps, Msg}; /// # use yew_router::components::LinkProps;
/// # /// #
/// # pub struct Link<R: Routable + 'static> { /// # pub struct Link<R: Routable + 'static> {
/// # _data: PhantomData<R>, /// # _data: PhantomData<R>,
/// # } /// # }
/// # /// #
/// # pub enum Msg {
/// # OnClick,
/// # }
/// #
/// impl<R: Routable + 'static> Component for Link<R> { /// impl<R: Routable + 'static> Component for Link<R> {
/// type Message = Msg; /// type Message = Msg;
/// type Properties = LinkProps<R>; /// type Properties = LinkProps<R>;
@ -50,7 +54,7 @@ pub struct NavigatorHandle {
/// ctx.link() /// ctx.link()
/// .navigator() /// .navigator()
/// .expect_throw("failed to get navigator.") /// .expect_throw("failed to get navigator.")
/// .push(ctx.props().to.clone()); /// .push(&ctx.props().to);
/// false /// false
/// } /// }
/// } /// }

View File

@ -1,44 +1,9 @@
//! The [`Switch`] Component. //! The [`Switch`] Component.
use std::marker::PhantomData;
use std::rc::Rc;
use gloo::console; use gloo::console;
use wasm_bindgen::UnwrapThrowExt;
use yew::prelude::*; use yew::prelude::*;
use crate::prelude::*; use crate::prelude::*;
use crate::scope_ext::LocationHandle;
/// Wraps `Rc` around `Fn` so it can be passed as a prop.
pub struct RenderFn<R>(Rc<dyn Fn(&R) -> Html>);
impl<R> RenderFn<R> {
/// Creates a new [`RenderFn`]
///
/// It is recommended that you use [`Switch::render`] instead
pub fn new(value: impl Fn(&R) -> Html + 'static) -> Self {
Self(Rc::new(value))
}
pub fn render(&self, route: &R) -> Html {
(self.0)(route)
}
}
impl<T> Clone for RenderFn<T> {
fn clone(&self) -> Self {
Self(Rc::clone(&self.0))
}
}
impl<T> PartialEq for RenderFn<T> {
fn eq(&self, other: &Self) -> bool {
// https://github.com/rust-lang/rust-clippy/issues/6524
#[allow(clippy::vtable_address_comparisons)]
Rc::ptr_eq(&self.0, &other.0)
}
}
/// Props for [`Switch`] /// Props for [`Switch`]
#[derive(Properties, PartialEq, Clone)] #[derive(Properties, PartialEq, Clone)]
@ -47,16 +12,11 @@ where
R: Routable, R: Routable,
{ {
/// Callback which returns [`Html`] to be rendered for the current route. /// Callback which returns [`Html`] to be rendered for the current route.
pub render: RenderFn<R>, pub render: Callback<R, Html>,
#[prop_or_default] #[prop_or_default]
pub pathname: Option<String>, pub pathname: Option<String>,
} }
#[doc(hidden)]
pub enum Msg {
ReRender,
}
/// A Switch that dispatches route among variants of a [`Routable`]. /// A Switch that dispatches route among variants of a [`Routable`].
/// ///
/// When a route can't be matched, including when the path is matched but the deserialization fails, /// When a route can't be matched, including when the path is matched but the deserialization fails,
@ -65,65 +25,24 @@ pub enum Msg {
/// Otherwise `html! {}` is rendered and a message is logged to console /// Otherwise `html! {}` is rendered and a message is logged to console
/// stating that no route can be matched. /// stating that no route can be matched.
/// See the [crate level document][crate] for more information. /// See the [crate level document][crate] for more information.
pub struct Switch<R: Routable + 'static> { #[function_component]
_listener: LocationHandle, pub fn Switch<R>(props: &SwitchProps<R>) -> Html
_phantom: PhantomData<R>,
}
impl<R> Component for Switch<R>
where where
R: Routable + 'static, R: Routable + 'static,
{ {
type Message = Msg; let route = use_route::<R>();
type Properties = SwitchProps<R>;
fn create(ctx: &Context<Self>) -> Self { let route = props
let link = ctx.link(); .pathname
let listener = link .as_ref()
.add_location_listener(link.callback(move |_| Msg::ReRender)) .and_then(|p| R::recognize(p))
.expect_throw("failed to create history handle. Do you have a router registered?"); .or(route);
Self { match route {
_listener: listener, Some(route) => props.render.emit(route),
_phantom: PhantomData, None => {
console::warn!("no route matched");
Html::default()
} }
} }
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::ReRender => true,
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let route = ctx
.props()
.pathname
.as_ref()
.and_then(|p| R::recognize(p))
.or_else(|| ctx.link().route::<R>());
let children = match &route {
Some(ref route) => (ctx.props().render.0)(route),
None => {
console::warn!("no route matched");
Html::default()
}
};
html! {<>{children}</>}
}
}
impl<R> Switch<R>
where
R: Routable + Clone + 'static,
{
/// Creates a [`RenderFn`].
pub fn render<F>(func: F) -> RenderFn<R>
where
F: Fn(&R) -> Html + 'static,
{
RenderFn::new(func)
}
} }

View File

@ -50,13 +50,13 @@ fn no(props: &NoProps) -> Html {
fn component() -> Html { fn component() -> Html {
let navigator = use_navigator().unwrap(); let navigator = use_navigator().unwrap();
let switch = Switch::render(move |routes| { let switch = move |routes| {
let navigator_clone = navigator.clone(); let navigator_clone = navigator.clone();
let replace_route = Callback::from(move |_| { let replace_route = Callback::from(move |_| {
navigator_clone navigator_clone
.replace_with_query( .replace_with_query(
Routes::No { id: 2 }, &Routes::No { id: 2 },
Query { &Query {
foo: "bar".to_string(), foo: "bar".to_string(),
}, },
) )
@ -67,8 +67,8 @@ fn component() -> Html {
let push_route = Callback::from(move |_| { let push_route = Callback::from(move |_| {
navigator_clone navigator_clone
.push_with_query( .push_with_query(
Routes::No { id: 3 }, &Routes::No { id: 3 },
Query { &Query {
foo: "baz".to_string(), foo: "baz".to_string(),
}, },
) )
@ -84,13 +84,13 @@ fn component() -> Html {
}, },
Routes::No { id } => html! { Routes::No { id } => html! {
<> <>
<No id={*id} /> <No id={id} />
<button onclick={push_route}>{"push a route"}</button> <button onclick={push_route}>{"push a route"}</button>
</> </>
}, },
Routes::NotFound => html! { <div id="result">{"404"}</div> }, Routes::NotFound => html! { <div id="result">{"404"}</div> },
} }
}); };
html! { html! {
<Switch<Routes> render={switch} /> <Switch<Routes> render={switch} />

View File

@ -50,13 +50,13 @@ fn no(props: &NoProps) -> Html {
fn component() -> Html { fn component() -> Html {
let navigator = use_navigator().unwrap(); let navigator = use_navigator().unwrap();
let switch = Switch::render(move |routes| { let switch = move |routes| {
let navigator_clone = navigator.clone(); let navigator_clone = navigator.clone();
let replace_route = Callback::from(move |_| { let replace_route = Callback::from(move |_| {
navigator_clone navigator_clone
.replace_with_query( .replace_with_query(
Routes::No { id: 2 }, &Routes::No { id: 2 },
Query { &Query {
foo: "bar".to_string(), foo: "bar".to_string(),
}, },
) )
@ -67,8 +67,8 @@ fn component() -> Html {
let push_route = Callback::from(move |_| { let push_route = Callback::from(move |_| {
navigator_clone navigator_clone
.push_with_query( .push_with_query(
Routes::No { id: 3 }, &Routes::No { id: 3 },
Query { &Query {
foo: "baz".to_string(), foo: "baz".to_string(),
}, },
) )
@ -84,13 +84,13 @@ fn component() -> Html {
}, },
Routes::No { id } => html! { Routes::No { id } => html! {
<> <>
<No id={*id} /> <No id={id} />
<button onclick={push_route}>{"push a route"}</button> <button onclick={push_route}>{"push a route"}</button>
</> </>
}, },
Routes::NotFound => html! { <div id="result">{"404"}</div> }, Routes::NotFound => html! { <div id="result">{"404"}</div> },
} }
}); };
html! { html! {
<Switch<Routes> render={switch} /> <Switch<Routes> render={switch} />

View File

@ -50,13 +50,13 @@ fn no(props: &NoProps) -> Html {
fn component() -> Html { fn component() -> Html {
let navigator = use_navigator().unwrap(); let navigator = use_navigator().unwrap();
let switch = Switch::render(move |routes| { let switch = move |routes| {
let navigator_clone = navigator.clone(); let navigator_clone = navigator.clone();
let replace_route = Callback::from(move |_| { let replace_route = Callback::from(move |_| {
navigator_clone navigator_clone
.replace_with_query( .replace_with_query(
Routes::No { id: 2 }, &Routes::No { id: 2 },
Query { &Query {
foo: "bar".to_string(), foo: "bar".to_string(),
}, },
) )
@ -67,8 +67,8 @@ fn component() -> Html {
let push_route = Callback::from(move |_| { let push_route = Callback::from(move |_| {
navigator_clone navigator_clone
.push_with_query( .push_with_query(
Routes::No { id: 3 }, &Routes::No { id: 3 },
Query { &Query {
foo: "baz".to_string(), foo: "baz".to_string(),
}, },
) )
@ -84,13 +84,13 @@ fn component() -> Html {
}, },
Routes::No { id } => html! { Routes::No { id } => html! {
<> <>
<No id={*id} /> <No id={id} />
<button onclick={push_route}>{"push a route"}</button> <button onclick={push_route}>{"push a route"}</button>
</> </>
}, },
Routes::NotFound => html! { <div id="result">{"404"}</div> }, Routes::NotFound => html! { <div id="result">{"404"}</div> },
} }
}); };
html! { html! {
<Switch<Routes> render={switch} /> <Switch<Routes> render={switch} />

View File

@ -16,7 +16,7 @@ pub use use_reducer::*;
pub use use_ref::*; pub use use_ref::*;
pub use use_state::*; pub use use_state::*;
use crate::functional::{AnyScope, HookContext}; use crate::functional::HookContext;
/// A trait that is implemented on hooks. /// A trait that is implemented on hooks.
/// ///
@ -52,17 +52,3 @@ impl<T> Hook for BoxedHook<'_, T> {
(self.inner)(ctx) (self.inner)(ctx)
} }
} }
pub(crate) fn use_component_scope() -> impl Hook<Output = AnyScope> {
struct HookProvider {}
impl Hook for HookProvider {
type Output = AnyScope;
fn run(self, ctx: &mut HookContext) -> Self::Output {
ctx.scope.clone()
}
}
HookProvider {}
}

View File

@ -1,6 +1,10 @@
use std::cell::RefCell;
use std::marker::PhantomData;
use std::rc::Rc;
use crate::callback::Callback; use crate::callback::Callback;
use crate::context::ContextHandle; use crate::context::ContextHandle;
use crate::functional::{hook, use_component_scope, use_memo, use_state}; use crate::functional::{Hook, HookContext};
/// Hook for consuming context values in function components. /// 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` /// The context of the type passed as `T` is returned. If there is no such context in scope, `None`
@ -65,29 +69,53 @@ use crate::functional::{hook, use_component_scope, use_memo, use_state};
/// } /// }
/// } /// }
/// ``` /// ```
#[hook] pub fn use_context<T: Clone + PartialEq + 'static>() -> impl Hook<Output = Option<T>> {
pub fn use_context<T: Clone + PartialEq + 'static>() -> Option<T> { struct HookProvider<T: Clone + PartialEq + 'static> {
struct UseContext<T: Clone + PartialEq + 'static> { _marker: PhantomData<T>,
context: Option<(T, ContextHandle<T>)>,
} }
let scope = use_component_scope(); struct UseContext<T: Clone + PartialEq + 'static> {
_handle: Option<ContextHandle<T>>,
value: Rc<RefCell<Option<T>>>,
}
let val = use_state(|| -> Option<T> { None }); impl<T> Hook for HookProvider<T>
let state = { where
let val_dispatcher = val.setter(); T: Clone + PartialEq + 'static,
use_memo( {
move |_| UseContext { type Output = Option<T>;
context: scope.context::<T>(Callback::from(move |m| {
val_dispatcher.clone().set(Some(m));
})),
},
(),
)
};
// we fallback to initial value if it was not updated. fn run(self, ctx: &mut HookContext) -> Self::Output {
(*val) let scope = ctx.scope.clone();
.clone()
.or_else(move || state.context.as_ref().map(|m| m.0.clone())) let state = ctx.next_state(move |re_render| -> UseContext<T> {
let value_cell: Rc<RefCell<Option<T>>> = Rc::default();
let (init_value, handle) = {
let value_cell = value_cell.clone();
scope.context(Callback::from(move |m| {
*(value_cell.borrow_mut()) = Some(m);
re_render()
}))
}
.map(|(value, handle)| (Some(value), Some(handle)))
.unwrap_or((None, None));
*(value_cell.borrow_mut()) = init_value;
UseContext {
_handle: handle,
value: value_cell,
}
});
let value = state.value.borrow();
value.clone()
}
}
HookProvider {
_marker: PhantomData,
}
} }

View File

@ -1,4 +1,5 @@
#![cfg(feature = "hydration")] #![cfg(feature = "hydration")]
#![cfg(target_arch = "wasm32")]
use std::rc::Rc; use std::rc::Rc;
use std::time::Duration; use std::time::Duration;

View File

@ -71,7 +71,7 @@ enum Route {
fn secure() -> Html { fn secure() -> Html {
let navigator = use_navigator().unwrap(); let navigator = use_navigator().unwrap();
let onclick = Callback::from(move |_| navigator.push(Route::Home)); let onclick = Callback::from(move |_| navigator.push(&Route::Home));
html! { html! {
<div> <div>
<h1>{ "Secure" }</h1> <h1>{ "Secure" }</h1>
@ -80,7 +80,7 @@ fn secure() -> Html {
} }
} }
fn switch(routes: &Route) -> Html { fn switch(routes: Route) -> Html {
match routes { match routes {
Route::Home => html! { <h1>{ "Home" }</h1> }, Route::Home => html! { <h1>{ "Home" }</h1> },
Route::Secure => html! { Route::Secure => html! {
@ -94,7 +94,7 @@ fn switch(routes: &Route) -> Html {
fn app() -> Html { fn app() -> Html {
html! { html! {
<BrowserRouter> <BrowserRouter>
<Switch<Route> render={Switch::render(switch)} /> <Switch<Route> render={switch} />
</BrowserRouter> </BrowserRouter>
} }
} }
@ -206,7 +206,7 @@ Here's how to implement a button that navigates to the `Home` route when clicked
#[function_component(MyComponent)] #[function_component(MyComponent)]
pub fn my_component() -> Html { pub fn my_component() -> Html {
let navigator = use_navigator().unwrap(); let navigator = use_navigator().unwrap();
let onclick = Callback::from(move |_| navigator.push(Route::Home)); let onclick = Callback::from(move |_| navigator.push(&Route::Home));
html! { html! {
<> <>
@ -239,7 +239,7 @@ pub fn nav_items() -> Html {
let go_home_button = { let go_home_button = {
let navigator = navigator.clone(); let navigator = navigator.clone();
let onclick = Callback::from(move |_| navigator.push(Route::Home)); let onclick = Callback::from(move |_| navigator.push(&Route::Home));
html! { html! {
<button {onclick}>{"click to go home"}</button> <button {onclick}>{"click to go home"}</button>
} }
@ -247,14 +247,14 @@ pub fn nav_items() -> Html {
let go_to_first_post_button = { let go_to_first_post_button = {
let navigator = navigator.clone(); let navigator = navigator.clone();
let onclick = Callback::from(move |_| navigator.push(Route::Post { id: "first-post".to_string() })); let onclick = Callback::from(move |_| navigator.push(&Route::Post { id: "first-post".to_string() }));
html! { html! {
<button {onclick}>{"click to go the first post"}</button> <button {onclick}>{"click to go the first post"}</button>
} }
}; };
let go_to_secure_button = { let go_to_secure_button = {
let onclick = Callback::from(move |_| navigator.push(Route::Secure)); let onclick = Callback::from(move |_| navigator.push(&Route::Secure));
html! { html! {
<button {onclick}>{"click to go to secure"}</button> <button {onclick}>{"click to go to secure"}</button>
} }
@ -278,7 +278,7 @@ identical with the function component case. Here's an example of a view function
```rust ,ignore ```rust ,ignore
fn view(&self, ctx: &Context<Self>) -> Html { fn view(&self, ctx: &Context<Self>) -> Html {
let navigator = ctx.link().navigator().unwrap(); let navigator = ctx.link().navigator().unwrap();
let onclick = Callback::from(move |_| navigator.push(MainRoute::Home)); let onclick = Callback::from(move |_| navigator.push(&MainRoute::Home));
html!{ html!{
<button {onclick}>{"Go Home"}</button> <button {onclick}>{"Go Home"}</button>
} }
@ -455,17 +455,17 @@ enum SettingsRoute {
NotFound, NotFound,
} }
fn switch_main(route: &MainRoute) -> Html { fn switch_main(route: MainRoute) -> Html {
match route { match route {
MainRoute::Home => html! {<h1>{"Home"}</h1>}, MainRoute::Home => html! {<h1>{"Home"}</h1>},
MainRoute::News => html! {<h1>{"News"}</h1>}, MainRoute::News => html! {<h1>{"News"}</h1>},
MainRoute::Contact => html! {<h1>{"Contact"}</h1>}, MainRoute::Contact => html! {<h1>{"Contact"}</h1>},
MainRoute::SettingsRoot | MainRoute::Settings => html! { <Switch<SettingsRoute> render={Switch::render(switch_settings)} /> }, MainRoute::SettingsRoot | MainRoute::Settings => html! { <Switch<SettingsRoute> render={switch_settings} /> },
MainRoute::NotFound => html! {<h1>{"Not Found"}</h1>}, MainRoute::NotFound => html! {<h1>{"Not Found"}</h1>},
} }
} }
fn switch_settings(route: &SettingsRoute) -> Html { fn switch_settings(route: SettingsRoute) -> Html {
match route { match route {
SettingsRoute::Profile => html! {<h1>{"Profile"}</h1>}, SettingsRoute::Profile => html! {<h1>{"Profile"}</h1>},
SettingsRoute::Friends => html! {<h1>{"Friends"}</h1>}, SettingsRoute::Friends => html! {<h1>{"Friends"}</h1>},
@ -478,7 +478,7 @@ fn switch_settings(route: &SettingsRoute) -> Html {
pub fn app() -> Html { pub fn app() -> Html {
html! { html! {
<BrowserRouter> <BrowserRouter>
<Switch<MainRoute> render={Switch::render(switch_main)} /> <Switch<MainRoute> render={switch_main} />
</BrowserRouter> </BrowserRouter>
} }
} }

View File

@ -0,0 +1,12 @@
---
title: 'From 0.16.0 to 0.17.0'
---
## `Switch::render` is no longer needed
The `<Switch />` component now accepts a closure of `Fn(Routable) -> Html` as
the render function directly.
## `navigator` API
The History API has been replaced with the Navigator API.