wip: memoize dom in the prescence of identical components

This commit is contained in:
Jonathan Kelley committed 2022-01-05 00:27:22 -05:00
1 parent 7cf15ee4e8
commit cb2782b4bb
27 files changed
+474 -219

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
{
"rust-analyzer.cargo.allFeatures": true
}
+4 -2
View File
@@ -19,6 +19,7 @@ serde = "1"
url = "2.2.2"
serde_urlencoded = "0.7"
# for wasm
web-sys = { version = "0.3", features = [
"Attr",
"Document",
@@ -33,11 +34,12 @@ web-sys = { version = "0.3", features = [
wasm-bindgen = { version = "0.2", optional = true }
js-sys = { version = "0.3", optional = true }
gloo = { version = "0.5", optional = true }
log = "0.4.14"
[features]
default = ["derive"]
web = ["web-sys"]
default = ["derive", "web"]
web = ["web-sys", "gloo", "js-sys", "wasm-bindgen"]
desktop = []
mobile = []
derive = []
+21 -32
View File
@@ -1,40 +1,29 @@
# Router hook for Dioxus apps
# Routing for Dioxus App
Dioxus-router provides a use_router hook that returns a different value depending on the route.
The router is generic over any value, however it makes sense to return a different set of VNodes
and feed them into the App's return VNodes.
Using the router should feel similar to tide's routing framework where an "address" book is assembled at the head of the app.
Here's an example of how to use the router hook:
DioxusRouter adds React-Router style routing to your Dioxus apps. Works in browser, SSR, and natively.
```rust
#[derive(Clone, PartialEq, Serialize, Deserialize, Routable)]
enum AppRoute {
Home,
Posts,
NotFound
}
static App: Component = |cx| {
let route = use_router(cx, AppRoute::parse);
match route {
AppRoute::Home => rsx!(cx, Home {})
AppRoute::Posts => rsx!(cx, Posts {})
AppRoute::Notfound => rsx!(cx, Notfound {})
}
};
```
Adding links into your app:
```rust
static Leaf: Component = |cx| {
rsx!(cx, div {
Link { to: AppRoute::Home }
fn app() {
cx.render(rsx! {
Routes {
Route { to: "/", Component {} },
Route { to: "/blog", Blog {} },
Route { to: "/blog/:id", BlogPost {} },
}
})
}
```
Then, in your route, you can choose to parse the Route any way you want through `use_route`.
```rust
let id: usize = use_route(&cx).path("id")?;
let state: CustomState = use_route(&cx).parse()?;
```
Adding links into your app:
```rust
Link { to: "id/{id}" }
```
Currently, the router is only supported in a web environment, but we plan to add 1st-party support via the context API when new renderers are available.
+32 -23
View File
@@ -11,31 +11,40 @@ fn main() {
}
static APP: Component = |cx| {
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
enum Route {
Home,
About,
NotFound,
}
impl Default for Route {
fn default() -> Self {
Route::Home
}
}
let route = use_router(&cx, |c| {});
cx.render(rsx! {
div {
{match route {
Route::Home => rsx!(h1 { "Home" }),
Route::About => rsx!(h1 { "About" }),
Route::NotFound => rsx!(h1 { "NotFound" }),
}}
nav {
Link { to: Route::Home, href: "/" }
Link { to: Route::About, href: "/about" }
Router {
onchange: move |route| log::info!("route changed to {}", route),
Route { to: "/", Home {} }
Route { to: "blog"
Route { to: "/", BlogList {} }
Route { to: ":id", BlogPost {} }
}
}
})
};
fn Home(cx: Scope) -> Element {
cx.render(rsx! {
div {
h1 { "Home" }
}
})
}
fn BlogList(cx: Scope) -> Element {
cx.render(rsx! {
div {
}
})
}
fn BlogPost(cx: Scope) -> Element {
let id = use_route(&cx).segment::<usize>("id")?;
cx.render(rsx! {
div {
}
})
}
@@ -1,4 +1,4 @@
use crate::{Routable, RouterService};
use crate::RouterService;
use dioxus::Attribute;
use dioxus_core as dioxus;
use dioxus_core::prelude::*;
@@ -6,8 +6,8 @@ use dioxus_core_macro::{format_args_f, rsx, Props};
use dioxus_html as dioxus_elements;
#[derive(Props)]
pub struct LinkProps<'a, R: Routable> {
to: R,
pub struct LinkProps<'a> {
to: &'a str,
/// The url that gets pushed to the history stack
///
@@ -28,6 +28,9 @@ pub struct LinkProps<'a, R: Routable> {
#[props(default, setter(strip_option))]
class: Option<&'a str>,
#[props(default, setter(strip_option))]
id: Option<&'a str>,
children: Element<'a>,
#[props(default)]
@@ -35,13 +38,21 @@ pub struct LinkProps<'a, R: Routable> {
}
#[allow(non_snake_case)]
pub fn Link<'a, R: Routable>(cx: Scope<'a, LinkProps<'a, R>>) -> Element {
let service = cx.consume_context::<RouterService<R>>()?;
pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
log::debug!("rendering link {:?}", cx.scope_id());
let service = cx.consume_context::<RouterService>()?;
cx.render(rsx! {
a {
href: "#",
href: "{cx.props.to}",
class: format_args!("{}", cx.props.class.unwrap_or("")),
onclick: move |_| service.push_route(cx.props.to.clone()),
id: format_args!("{}", cx.props.id.unwrap_or("")),
prevent_default: "onclick",
onclick: move |_| {
log::debug!("clicked");
service.push_route(cx.props.to.clone());
},
&cx.props.children
}
})
+58
View File
@@ -0,0 +1,58 @@
use dioxus_core::Element;
use dioxus_core as dioxus;
use dioxus_core::prelude::*;
use dioxus_core_macro::Props;
use dioxus_core_macro::*;
use dioxus_html as dioxus_elements;
use crate::{RouteContext, RouterService};
#[derive(Props)]
pub struct RouteProps<'a> {
to: &'a str,
children: Element<'a>,
}
pub fn Route<'a>(cx: Scope<'a, RouteProps<'a>>) -> Element {
// now we want to submit
let router_root = cx
.use_hook(|_| cx.consume_context::<RouterService>())
.as_ref()?;
cx.use_hook(|_| {
// create a bigger, better, longer route if one above us exists
let total_route = match cx.consume_context::<RouteContext>() {
Some(ctx) => format!("{}", ctx.total_route.clone()),
None => format!("{}", cx.props.to.clone()),
};
// provide our route context
let route_context = cx.provide_context(RouteContext {
declared_route: cx.props.to.to_string(),
total_route,
});
// submit our rout
router_root.register_total_route(route_context.total_route.clone(), cx.scope_id());
Some(RouteInner {})
});
log::debug!("rendering route {:?}", cx.scope_id());
if router_root.should_render(cx.scope_id()) {
log::debug!("route {:?} produced nodes", cx.scope_id());
cx.render(rsx!(&cx.props.children))
} else {
None
}
}
struct RouteInner {}
impl Drop for RouteInner {
fn drop(&mut self) {
// todo!()
}
}
+29
View File
@@ -0,0 +1,29 @@
use dioxus_core::Element;
use dioxus_core as dioxus;
use dioxus_core::prelude::*;
use dioxus_core_macro::*;
use dioxus_html as dioxus_elements;
use crate::RouterService;
#[derive(Props)]
pub struct RouterProps<'a> {
children: Element<'a>,
#[props(default, setter(strip_option))]
onchange: Option<&'a Fn(&'a str)>,
}
pub fn Router<'a>(cx: Scope<'a, RouterProps<'a>>) -> Element {
let p = cx.use_hook(|_| {
let update = cx.schedule_update_any();
cx.provide_context(RouterService::new(update, cx.scope_id()))
});
log::debug!("rendering router {:?}", cx.scope_id());
cx.render(rsx!(
div { &cx.props.children }
))
}
+30
View File
@@ -0,0 +1,30 @@
use dioxus_core::ScopeState;
pub struct UseRoute<'a> {
cur_route: String,
cx: &'a ScopeState,
}
impl<'a> UseRoute<'a> {
/// Parse the query part of the URL
pub fn param<T>(&self, param: &str) -> Option<&T> {
todo!()
}
pub fn nth_segment(&self, n: usize) -> Option<&str> {
todo!()
}
pub fn last_segment(&self) -> Option<&str> {
todo!()
}
/// Parse the segments of the URL, using named parameters (defined in your router)
pub fn segment<T>(&self, name: &str) -> Option<&T> {
todo!()
}
}
pub fn use_route(cx: &ScopeState) -> UseRoute {
todo!()
}
+20 -25
View File
@@ -7,17 +7,6 @@
//!
//! ```rust
//! fn app(cx: Scope) -> Element {
//! let route = use_router(&cx, |svc, path| {
//! match path {
//! "/about" => Route::About,
//! _ => Route::Home,
//! }
//! });
//!
//! match route {
//! Route::Home => rsx!(cx, h1 { "Home" }),
//! Route::About => rsx!(cx, h1 { "About" }),
//! }
//! }
//!
//!
@@ -35,22 +24,28 @@
//!
//!
mod link;
mod hooks {
mod use_route;
pub use use_route::*;
}
pub use hooks::*;
mod components {
mod router;
pub use router::*;
mod route;
pub use route::*;
mod link;
pub use link::*;
}
pub use components::*;
mod platform;
mod routecontext;
mod service;
mod userouter;
mod utils;
pub use link::*;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
pub use routecontext::*;
pub use service::*;
pub use userouter::*;
pub trait Routable:
'static + Send + Clone + PartialEq + Serialize + DeserializeOwned + Default
{
}
impl<T> Routable for T where
T: 'static + Send + Clone + PartialEq + Serialize + DeserializeOwned + Default
{
}
-7
View File
@@ -1,7 +0,0 @@
pub fn parse_current_route() -> String {
todo!()
}
pub(crate) fn get_base_route() -> String {
todo!()
}
+11
View File
@@ -0,0 +1,11 @@
use url::Url;
pub trait RouterProvider {
fn get_current_route(&self) -> String;
fn subscribe_to_route_changes(&self, callback: Box<dyn Fn(String)>);
}
enum RouteChange {
LinkTo(String),
Back(String),
}
+7
View File
@@ -0,0 +1,7 @@
pub struct RouteContext {
// "/name/:id"
pub declared_route: String,
// "app/name/:id"
pub total_route: String,
}
+68 -11
View File
@@ -1,18 +1,75 @@
use crate::Routable;
use std::{cell::RefCell, rc::Rc};
use gloo::history::{BrowserHistory, History, HistoryListener};
use std::{cell::RefCell, collections::HashMap, rc::Rc};
pub struct RouterService<R: Routable> {
pub(crate) regen_route: Rc<dyn Fn()>,
pub(crate) pending_routes: RefCell<Vec<R>>,
use dioxus_core::ScopeId;
pub struct RouterService {
pub(crate) regen_route: Rc<dyn Fn(ScopeId)>,
history: RefCell<BrowserHistory>,
registerd_routes: RefCell<RouteSlot>,
slots: RefCell<HashMap<ScopeId, String>>,
cur_root: RefCell<String>,
listener: HistoryListener,
}
impl<R: Routable> RouterService<R> {
pub fn current_path(&self) -> &str {
todo!()
enum RouteSlot {
Routes {
// the partial route
partial: String,
// the total route
total: String,
// Connections to other routs
rest: Vec<RouteSlot>,
},
}
impl RouterService {
pub fn new(regen_route: Rc<dyn Fn(ScopeId)>, root_scope: ScopeId) -> Self {
let history = BrowserHistory::default();
let location = history.location();
let path = location.path();
let regen = Rc::clone(&regen_route);
let listener = history.listen(move || {
regen(root_scope);
});
Self {
registerd_routes: RefCell::new(RouteSlot::Routes {
partial: String::from("/"),
total: String::from("/"),
rest: Vec::new(),
}),
history: RefCell::new(history),
regen_route,
slots: Default::default(),
cur_root: RefCell::new(path.to_string()),
listener,
}
}
pub fn push_route(&self, route: R) {
self.pending_routes.borrow_mut().push(route);
(self.regen_route)();
pub fn push_route(&self, route: &str) {
self.history.borrow_mut().push(route);
}
pub fn register_total_route(&self, route: String, scope: ScopeId) {
self.slots.borrow_mut().insert(scope, route);
}
pub fn should_render(&self, scope: ScopeId) -> bool {
let location = self.history.borrow().location();
let path = location.path();
let roots = self.slots.borrow();
let root = roots.get(&scope);
match root {
Some(r) => r == path,
None => false,
}
}
}
-33
View File
@@ -1,33 +0,0 @@
use std::{cell::RefCell, rc::Rc};
use crate::{Routable, RouterCfg, RouterService};
use dioxus_core::ScopeState;
/// Initialize the app's router service and provide access to `Link` components
pub fn use_router<'a, R: Routable>(cx: &'a ScopeState, _cfg: impl FnOnce(&mut RouterCfg)) -> &'a R {
let router = cx.use_hook(|_| {
let svc: RouterService<R> = RouterService {
regen_route: cx.schedule_update(),
pending_routes: RefCell::new(Vec::new()),
};
let first_path = R::default();
cx.provide_context(svc);
UseRouterInner {
svc: cx.consume_context::<RouterService<R>>().unwrap(),
history: vec![first_path],
}
});
let mut pending_routes = router.svc.pending_routes.borrow_mut();
for route in pending_routes.drain(..) {
router.history.push(route);
}
router.history.last().unwrap()
}
struct UseRouterInner<R: Routable> {
svc: Rc<RouterService<R>>,
history: Vec<R>,
}