public inbox for yew-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [yew-devel] [RFC yew-comp] refactor: move LoadableComponent state into component implementations
@ 2025-12-09 13:11 Dietmar Maurer
  2025-12-09 18:31 ` Thomas Lamprecht
  2025-12-10  7:53 ` Thomas Lamprecht
  0 siblings, 2 replies; 5+ messages in thread
From: Dietmar Maurer @ 2025-12-09 13:11 UTC (permalink / raw)
  To: yew-devel

Major Refactoring of the `LoadableComponent` system.

Encapsulate component state (loading, error, view_state) into a new
`LoadableComponentState` struct.
Instead of `LoadableComponentMaster` managing the state externally,
the concrete components now own their `LoadableComponentState`.

- Use `impl_deref_mut_property`LoadableComponentState` struct. to implement
  Deref/DerefMut for components, allowing `LoadableComponentMaster` to
  access the state transparently.
- `LoadableComponentContext` is now a normal yew Scope (removed custom implementation)
- Migrate all `LoadableComponent` implementations (ACL, ACME, APT, Network,
  User/Token/TFA views, etc.) to the new pattern.
- Use `link.custom_callback` and `link.send_custom_message` for internal
  messaging (rename is necessaray because of naming conflict with standard
  Scope function).
- avoid useless Redraw/Datachange/Refresh messages, because `LoadableComponentMaster`
  already implements that.

Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
---
 src/acl/acl_view.rs                        |  26 +-
 src/acme/acme_accounts.rs                  |  28 +-
 src/acme/acme_domains.rs                   |  35 +-
 src/acme/acme_plugins.rs                   |  60 +-
 src/acme/certificate_list.rs               |  36 +-
 src/apt_package_manager.rs                 |  45 +-
 src/apt_repositories.rs                    |  44 +-
 src/auth_view.rs                           |  36 +-
 src/configuration/network_view.rs          |  40 +-
 src/configuration/pve/lxc_network_panel.rs |  31 +-
 src/lib.rs                                 |  33 +-
 src/loadable_component.rs                  | 698 ++++++++++++---------
 src/node_status_panel.rs                   |  37 +-
 src/notes_view.rs                          |  16 +-
 src/object_grid.rs                         |  36 +-
 src/permission_panel.rs                    |  17 +-
 src/subscription_panel.rs                  |  33 +-
 src/tasks.rs                               |  39 +-
 src/tfa/tfa_view.rs                        |  36 +-
 src/token_panel.rs                         |  24 +-
 src/user_panel.rs                          |  31 +-
 21 files changed, 825 insertions(+), 556 deletions(-)

diff --git a/src/acl/acl_view.rs b/src/acl/acl_view.rs
index 58da3fd..9347d72 100644
--- a/src/acl/acl_view.rs
+++ b/src/acl/acl_view.rs
@@ -24,7 +24,9 @@ use proxmox_access_control::types::{AclListItem, AclUgidType};
 use crate::percent_encoding::percent_encode_component;
 use crate::utils::render_boolean;
 use crate::{
-    ConfirmButton, EditWindow, LoadableComponent, LoadableComponentContext, LoadableComponentMaster,
+    impl_deref_mut_property, ConfirmButton, EditWindow, LoadableComponent,
+    LoadableComponentContext, LoadableComponentMaster, LoadableComponentScopeExt,
+    LoadableComponentState,
 };
 
 use super::acl_edit::AclEditWindow;
@@ -95,15 +97,17 @@ enum ViewState {
 }
 
 enum Msg {
-    Reload,
     Remove,
 }
 
 struct ProxmoxAclView {
+    state: LoadableComponentState<ViewState>,
     selection: Selection,
     store: Store<AclListItem>,
 }
 
+impl_deref_mut_property!(ProxmoxAclView, state, LoadableComponentState<ViewState>);
+
 impl ProxmoxAclView {
     fn colmuns() -> Rc<Vec<DataTableHeader<AclListItem>>> {
         Rc::new(vec![
@@ -141,14 +145,21 @@ impl LoadableComponent for ProxmoxAclView {
         let link = ctx.link();
         link.repeated_load(5000);
 
-        let selection = Selection::new().on_select(link.callback(|_| Msg::Reload));
+        let selection = Selection::new().on_select({
+            let link = ctx.link().clone();
+            move |_| link.send_redraw()
+        });
 
         let store = Store::with_extract_key(|record: &AclListItem| {
             let acl_id = format!("{} for {} - {}", record.path, record.ugid, record.roleid);
             Key::from(acl_id)
         });
 
-        Self { selection, store }
+        Self {
+            state: LoadableComponentState::new(),
+            selection,
+            store,
+        }
     }
 
     fn load(
@@ -203,7 +214,7 @@ impl LoadableComponent for ProxmoxAclView {
             ConfirmButton::new(tr!("Remove ACL Entry"))
                 .confirm_message(tr!("Are you sure you want to remove this ACL entry?"))
                 .disabled(disabled)
-                .on_activate(ctx.link().callback(|_| Msg::Remove)),
+                .on_activate(ctx.link().custom_callback(|_| Msg::Remove)),
         );
 
         Some(toolbar.into())
@@ -211,14 +222,13 @@ impl LoadableComponent for ProxmoxAclView {
 
     fn update(&mut self, ctx: &LoadableComponentContext<Self>, msg: Self::Message) -> bool {
         match msg {
-            Msg::Reload => true,
             Msg::Remove => {
                 if let Some(key) = self.selection.selected_key() {
                     if let Some(record) = self.store.read().lookup_record(&key).cloned() {
-                        let link = ctx.link();
+                        let link = ctx.link().clone();
                         let url = ctx.props().acl_api_endpoint.to_owned();
 
-                        link.clone().spawn(async move {
+                        self.spawn(async move {
                             let data = match record.ugid_type {
                                 AclUgidType::User => json!({
                                     "delete": true,
diff --git a/src/acme/acme_accounts.rs b/src/acme/acme_accounts.rs
index 19bf48b..4c4db1a 100644
--- a/src/acme/acme_accounts.rs
+++ b/src/acme/acme_accounts.rs
@@ -15,8 +15,9 @@ use crate::common_api_types::AcmeAccountInfo;
 use crate::percent_encoding::percent_encode_component;
 use crate::utils::render_url;
 use crate::{
-    ConfirmButton, DataViewWindow, LoadableComponent, LoadableComponentContext,
-    LoadableComponentMaster,
+    impl_deref_mut_property, ConfirmButton, DataViewWindow, LoadableComponent,
+    LoadableComponentContext, LoadableComponentMaster, LoadableComponentScopeExt,
+    LoadableComponentState,
 };
 
 use super::AcmeRegisterAccount;
@@ -44,14 +45,17 @@ impl AcmeAccountsPanel {
 
 #[doc(hidden)]
 pub struct ProxmoxAcmeAccountsPanel {
+    state: LoadableComponentState<ViewState>,
     selection: Selection,
     store: Store<AcmeAccountEntry>,
     columns: Rc<Vec<DataTableHeader<AcmeAccountEntry>>>,
 }
 
-pub enum Msg {
-    Redraw,
-}
+impl_deref_mut_property!(
+    ProxmoxAcmeAccountsPanel,
+    state,
+    LoadableComponentState<ViewState>
+);
 
 #[derive(PartialEq)]
 pub enum ViewState {
@@ -61,11 +65,14 @@ pub enum ViewState {
 
 impl LoadableComponent for ProxmoxAcmeAccountsPanel {
     type Properties = AcmeAccountsPanel;
-    type Message = Msg;
+    type Message = ();
     type ViewState = ViewState;
 
     fn create(ctx: &LoadableComponentContext<Self>) -> Self {
-        let selection = Selection::new().on_select(ctx.link().callback(|_| Msg::Redraw));
+        let selection = Selection::new().on_select({
+            let link = ctx.link().clone();
+            move |_| link.send_redraw()
+        });
         let store =
             Store::with_extract_key(|record: &AcmeAccountEntry| Key::from(record.name.clone()));
 
@@ -77,6 +84,7 @@ impl LoadableComponent for ProxmoxAcmeAccountsPanel {
             .into()]);
 
         Self {
+            state: LoadableComponentState::new(),
             selection,
             store,
             columns,
@@ -110,7 +118,7 @@ impl LoadableComponent for ProxmoxAcmeAccountsPanel {
                 Button::new(tr!("View"))
                     .disabled(selected_key.is_none())
                     .onclick({
-                        let link = ctx.link();
+                        let link = ctx.link().clone();
                         let selected_key = selected_key.clone();
                         move |_| {
                             if let Some(selected_key) = &selected_key {
@@ -123,7 +131,7 @@ impl LoadableComponent for ProxmoxAcmeAccountsPanel {
                 ConfirmButton::remove_entry(selected_key.as_deref().unwrap_or("").to_string())
                     .disabled(selected_key.is_none())
                     .on_activate({
-                        let link = ctx.link();
+                        let link = ctx.link().clone();
                         let selected_key = selected_key.clone();
 
                         move |_| {
@@ -159,7 +167,7 @@ impl LoadableComponent for ProxmoxAcmeAccountsPanel {
             .selection(self.selection.clone())
             .on_row_dblclick({
                 let selection = self.selection.clone();
-                let link = ctx.link();
+                let link = ctx.link().clone();
                 move |_: &mut _| {
                     if let Some(selected_key) = selection.selected_key() {
                         link.change_view(Some(ViewState::View(selected_key.clone())));
diff --git a/src/acme/acme_domains.rs b/src/acme/acme_domains.rs
index 6ab924d..32003d8 100644
--- a/src/acme/acme_domains.rs
+++ b/src/acme/acme_domains.rs
@@ -19,8 +19,10 @@ use pwt_macros::builder;
 use crate::common_api_types::{create_acme_config_string, parse_acme_config_string, AcmeConfig};
 use crate::common_api_types::{create_acme_domain_string, parse_acme_domain_string, AcmeDomain};
 use crate::percent_encoding::percent_encode_component;
-use crate::{ConfirmButton, EditWindow};
-use crate::{LoadableComponent, LoadableComponentContext, LoadableComponentMaster};
+use crate::{impl_deref_mut_property, ConfirmButton, EditWindow, LoadableComponentState};
+use crate::{
+    LoadableComponent, LoadableComponentContext, LoadableComponentMaster, LoadableComponentScopeExt,
+};
 
 use super::{AcmeAccountSelector, AcmeChallengeTypeSelector, AcmePluginSelector};
 
@@ -54,14 +56,20 @@ impl AcmeDomainsPanel {
 
 #[doc(hidden)]
 pub struct ProxmoxAcmeDomainsPanel {
+    state: LoadableComponentState<ViewState>,
     selection: Selection,
     store: Store<AcmeDomainEntry>,
     columns: Rc<Vec<DataTableHeader<AcmeDomainEntry>>>,
     acme_account: Option<AcmeConfig>,
 }
 
+impl_deref_mut_property!(
+    ProxmoxAcmeDomainsPanel,
+    state,
+    LoadableComponentState<ViewState>
+);
+
 pub enum Msg {
-    Redraw,
     AcmeAccount(Option<AcmeConfig>),
 }
 
@@ -78,7 +86,10 @@ impl LoadableComponent for ProxmoxAcmeDomainsPanel {
     type ViewState = ViewState;
 
     fn create(ctx: &LoadableComponentContext<Self>) -> Self {
-        let selection = Selection::new().on_select(ctx.link().callback(|_| Msg::Redraw));
+        let selection = Selection::new().on_select({
+            let link = ctx.link().clone();
+            move |_| link.send_redraw()
+        });
         let store = Store::with_extract_key(|record: &AcmeDomainEntry| {
             Key::from(record.config_key.clone())
         });
@@ -109,6 +120,7 @@ impl LoadableComponent for ProxmoxAcmeDomainsPanel {
         ctx.link().repeated_load(3000);
 
         Self {
+            state: LoadableComponentState::new(),
             selection,
             store,
             columns,
@@ -122,7 +134,7 @@ impl LoadableComponent for ProxmoxAcmeDomainsPanel {
     ) -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>> {
         let store = self.store.clone();
         let url = ctx.props().url.clone();
-        let link = ctx.link();
+        let link = ctx.link().clone();
         Box::pin(async move {
             let data: Value = crate::http_get(&*url, None).await?;
 
@@ -147,9 +159,9 @@ impl LoadableComponent for ProxmoxAcmeDomainsPanel {
 
             if let Some(Value::String(acme_account)) = data.get("acme") {
                 let acme_account = parse_acme_config_string(acme_account)?;
-                link.send_message(Msg::AcmeAccount(Some(acme_account)));
+                link.send_custom_message(Msg::AcmeAccount(Some(acme_account)));
             } else {
-                link.send_message(Msg::AcmeAccount(None));
+                link.send_custom_message(Msg::AcmeAccount(None));
             }
             Ok(())
         })
@@ -157,7 +169,6 @@ impl LoadableComponent for ProxmoxAcmeDomainsPanel {
 
     fn update(&mut self, _ctx: &LoadableComponentContext<Self>, msg: Self::Message) -> bool {
         match msg {
-            Msg::Redraw => true,
             Msg::AcmeAccount(acme_account) => {
                 self.acme_account = acme_account;
                 true
@@ -179,7 +190,7 @@ impl LoadableComponent for ProxmoxAcmeDomainsPanel {
                 Button::new(tr!("Edit"))
                     .disabled(selected_key.is_none())
                     .onclick({
-                        let link = ctx.link();
+                        let link = ctx.link().clone();
                         let selected_key = selected_key.clone();
                         move |_| {
                             if let Some(selected_key) = &selected_key {
@@ -192,7 +203,7 @@ impl LoadableComponent for ProxmoxAcmeDomainsPanel {
                 ConfirmButton::remove_entry(selected_key.as_deref().unwrap_or("").to_string())
                     .disabled(selected_key.is_none())
                     .on_activate({
-                        let link = ctx.link();
+                        let link = ctx.link().clone();
                         let url = ctx.props().url.clone();
                         let selected_key = selected_key.clone();
                         move |_| {
@@ -238,7 +249,7 @@ impl LoadableComponent for ProxmoxAcmeDomainsPanel {
                 </div>}
             })
             .with_child(Button::new(tr!("Order Certificate Now")).onclick({
-                let link = ctx.link();
+                let link = ctx.link().clone();
                 move |_| {
                     let command_path = "/nodes/localhost/certificates/acme/certificate";
                     link.start_task(command_path, None, false);
@@ -253,7 +264,7 @@ impl LoadableComponent for ProxmoxAcmeDomainsPanel {
             .class("pwt-flex-fit")
             .selection(self.selection.clone())
             .on_row_dblclick({
-                let link = ctx.link();
+                let link = ctx.link().clone();
                 move |event: &mut DataTableMouseEvent| {
                     let key = &event.record_key;
                     link.change_view(Some(ViewState::Edit(key.clone())));
diff --git a/src/acme/acme_plugins.rs b/src/acme/acme_plugins.rs
index f0b1e69..66baa5f 100644
--- a/src/acme/acme_plugins.rs
+++ b/src/acme/acme_plugins.rs
@@ -9,19 +9,20 @@ use serde::{Deserialize, Serialize};
 use serde_json::Value;
 use yew::virtual_dom::{Key, VComp, VNode};
 
+use pwt::prelude::*;
 use pwt::state::{Selection, Store};
 use pwt::widget::data_table::{DataTable, DataTableColumn, DataTableHeader, DataTableMouseEvent};
 use pwt::widget::form::{DisplayField, Field, FormContext, Number, TextArea};
 use pwt::widget::{Button, InputPanel, Toolbar};
-use pwt::{prelude::*, AsyncPool};
 
 use pwt_macros::builder;
 
 use crate::form::delete_empty_values;
 use crate::percent_encoding::percent_encode_component;
 use crate::{
-    http_get, ConfirmButton, EditWindow, LoadableComponent, LoadableComponentContext,
-    LoadableComponentLink, LoadableComponentMaster,
+    http_get, impl_deref_mut_property, ConfirmButton, EditWindow, LoadableComponent,
+    LoadableComponentContext, LoadableComponentMaster, LoadableComponentScope,
+    LoadableComponentScopeExt, LoadableComponentState,
 };
 
 use super::{AcmeChallengeSchemaItem, AcmeChallengeSelector};
@@ -81,14 +82,20 @@ struct ChallengeSchemaInfo {
 
 #[doc(hidden)]
 pub struct ProxmoxAcmePluginsPanel {
+    state: LoadableComponentState<ViewState>,
     selection: Selection,
     store: Store<PluginConfig>,
     columns: Rc<Vec<DataTableHeader<PluginConfig>>>,
     challenge_schema: Option<AcmeChallengeSchemaItem>,
     schema_info: ChallengeSchemaInfo,
-    async_pool: AsyncPool,
 }
 
+impl_deref_mut_property!(
+    ProxmoxAcmePluginsPanel,
+    state,
+    LoadableComponentState<ViewState>
+);
+
 #[derive(PartialEq)]
 pub enum ViewState {
     Add,
@@ -96,7 +103,6 @@ pub enum ViewState {
 }
 
 pub enum Msg {
-    Redraw,
     CloseDialog,
     Add,
     Edit(Key),
@@ -126,16 +132,20 @@ impl LoadableComponent for ProxmoxAcmePluginsPanel {
     type ViewState = ViewState;
 
     fn create(ctx: &LoadableComponentContext<Self>) -> Self {
-        let selection = Selection::new().on_select(ctx.link().callback(|_| Msg::Redraw));
+        let selection = Selection::new().on_select({
+            let link = ctx.link().clone();
+            move |_| link.send_redraw()
+        });
         let store =
             Store::with_extract_key(|record: &PluginConfig| Key::from(record.plugin.clone()));
 
         let schema_name_map = Rc::new(HashMap::new());
         let columns = columns(schema_name_map.clone());
 
-        ctx.link().send_message(Msg::LoadChallengeSchemaList);
+        ctx.link().send_custom_message(Msg::LoadChallengeSchemaList);
 
         Self {
+            state: LoadableComponentState::new(),
             selection,
             store,
             columns,
@@ -144,7 +154,6 @@ impl LoadableComponent for ProxmoxAcmePluginsPanel {
                 schema_name_map,
                 store: Store::new(),
             },
-            async_pool: AsyncPool::new(),
         }
     }
 
@@ -164,7 +173,6 @@ impl LoadableComponent for ProxmoxAcmePluginsPanel {
 
     fn update(&mut self, ctx: &LoadableComponentContext<Self>, msg: Self::Message) -> bool {
         match msg {
-            Msg::Redraw => true,
             Msg::Add => {
                 self.challenge_schema = None;
                 ctx.link().change_view(Some(ViewState::Add));
@@ -217,10 +225,10 @@ impl LoadableComponent for ProxmoxAcmePluginsPanel {
             }
             Msg::LoadChallengeSchemaList => {
                 let url = ctx.props().challenge_shema_url.clone();
-                let link = ctx.link();
-                self.async_pool.spawn(async move {
+                let link = ctx.link().clone();
+                self.spawn(async move {
                     let result = http_get(&*url, None).await;
-                    link.send_message(Msg::UpdateChallengeSchemaList(result));
+                    link.send_custom_message(Msg::UpdateChallengeSchemaList(result));
                 });
                 false
             }
@@ -241,7 +249,7 @@ impl LoadableComponent for ProxmoxAcmePluginsPanel {
                     );
                     let command_future = crate::http_delete(command_path, None);
                     let link = ctx.link().clone();
-                    self.async_pool.spawn(async move {
+                    self.spawn(async move {
                         match command_future.await {
                             Ok(()) => {
                                 link.send_reload();
@@ -265,16 +273,16 @@ impl LoadableComponent for ProxmoxAcmePluginsPanel {
             .class("pwt-w-100")
             .class("pwt-overflow-hidden")
             .class("pwt-border-bottom")
-            .with_child(Button::new(tr!("Add")).onclick(ctx.link().callback(|_| Msg::Add)))
+            .with_child(Button::new(tr!("Add")).onclick(ctx.link().custom_callback(|_| Msg::Add)))
             .with_child(
                 Button::new(tr!("Edit"))
                     .disabled(selected_key.is_none())
                     .onclick({
-                        let link = ctx.link();
+                        let link = ctx.link().clone();
                         let selected_key = selected_key.clone();
                         move |_| {
                             if let Some(selected_key) = &selected_key {
-                                link.send_message(Msg::Edit(selected_key.clone()));
+                                link.send_custom_message(Msg::Edit(selected_key.clone()));
                             }
                         }
                     }),
@@ -282,7 +290,7 @@ impl LoadableComponent for ProxmoxAcmePluginsPanel {
             .with_child(
                 ConfirmButton::remove_entry(selected_key.as_deref().unwrap_or("").to_string())
                     .disabled(selected_key.is_none())
-                    .on_activate(ctx.link().callback({
+                    .on_activate(ctx.link().custom_callback({
                         let selected_key = selected_key.clone();
                         move |_| Msg::Delete(selected_key.clone())
                     })),
@@ -297,11 +305,11 @@ impl LoadableComponent for ProxmoxAcmePluginsPanel {
             .selection(self.selection.clone())
             .on_row_dblclick({
                 let store = self.store.clone();
-                let link = ctx.link();
+                let link = ctx.link().clone();
                 move |event: &mut DataTableMouseEvent| {
                     let key = &event.record_key;
                     if store.read().lookup_record(key).is_some() {
-                        link.send_message(Msg::Edit(key.clone()));
+                        link.send_custom_message(Msg::Edit(key.clone()));
                     };
                 }
             })
@@ -372,7 +380,7 @@ impl ProxmoxAcmePluginsPanel {
     }
 
     fn dns_plugin_input_panel(
-        link: &LoadableComponentLink<Self>,
+        link: &LoadableComponentScope<Self>,
         form_ctx: &FormContext,
         id: Option<&str>,
         challenge_schema: Option<&AcmeChallengeSchemaItem>,
@@ -411,7 +419,7 @@ impl ProxmoxAcmePluginsPanel {
                 AcmeChallengeSelector::with_store(challenge_store)
                     .name("api")
                     .required(true)
-                    .on_change(link.callback(Msg::ChallengeSchema)),
+                    .on_change(link.custom_callback(Msg::ChallengeSchema)),
             );
 
         if let Some(description) =
@@ -463,7 +471,7 @@ impl ProxmoxAcmePluginsPanel {
                         .on_change({
                             let field_name = field_name.clone();
                             let form_ctx = form_ctx.clone();
-                            link.callback(move |v| {
+                            link.custom_callback(move |v| {
                                 Msg::ApiDataChange(form_ctx.clone(), field_name.clone(), v)
                             })
                         }),
@@ -495,10 +503,10 @@ impl ProxmoxAcmePluginsPanel {
                 move |url: AttrValue| crate::http_get_full(url.to_string(), None),
                 url.clone(),
             ))
-            .on_done(ctx.link().callback(|_| Msg::CloseDialog))
+            .on_done(ctx.link().custom_callback(|_| Msg::CloseDialog))
             .renderer({
                 let id = id.to_owned();
-                let link = ctx.link();
+                let link = ctx.link().clone();
                 let challenge_schema = self.challenge_schema.clone();
                 let challenge_store = self.schema_info.store.clone();
                 move |form_ctx: &FormContext| {
@@ -532,9 +540,9 @@ impl ProxmoxAcmePluginsPanel {
 
     fn create_add_dns_plugin_dialog(&self, ctx: &crate::LoadableComponentContext<Self>) -> Html {
         EditWindow::new(tr!("Add") + ": " + &tr!("ACME DNS Plugin"))
-            .on_done(ctx.link().callback(|_| Msg::CloseDialog))
+            .on_done(ctx.link().custom_callback(|_| Msg::CloseDialog))
             .renderer({
-                let link = ctx.link();
+                let link = ctx.link().clone();
                 let challenge_schema = self.challenge_schema.clone();
                 let challenge_store = self.schema_info.store.clone();
                 move |form_ctx: &FormContext| {
diff --git a/src/acme/certificate_list.rs b/src/acme/certificate_list.rs
index d9ce92c..7ac20e6 100644
--- a/src/acme/certificate_list.rs
+++ b/src/acme/certificate_list.rs
@@ -18,8 +18,9 @@ use pwt::widget::{Button, Container, Dialog, FileButton, MessageBox, Toolbar};
 use crate::common_api_types::CertificateInfo;
 use crate::utils::render_epoch;
 use crate::{
-    ConfirmButton, EditWindow, KVGrid, KVGridRow, LoadableComponent, LoadableComponentContext,
-    LoadableComponentMaster,
+    impl_deref_mut_property, ConfirmButton, EditWindow, KVGrid, KVGridRow, LoadableComponent,
+    LoadableComponentContext, LoadableComponentMaster, LoadableComponentScopeExt,
+    LoadableComponentState,
 };
 
 async fn upload_custom_certificate(form_ctx: FormContext) -> Result<(), Error> {
@@ -46,10 +47,6 @@ impl CertificateList {
     }
 }
 
-pub enum Msg {
-    Redraw,
-}
-
 #[derive(PartialEq)]
 pub enum ViewState {
     CertificateView(Rc<Value>),
@@ -59,24 +56,35 @@ pub enum ViewState {
 
 #[doc(hidden)]
 pub struct ProxmoxCertificateList {
+    state: LoadableComponentState<ViewState>,
     selection: Selection,
     store: Store<CertificateInfo>,
     columns: Rc<Vec<DataTableHeader<CertificateInfo>>>,
     rows: Rc<Vec<KVGridRow>>,
 }
 
+impl_deref_mut_property!(
+    ProxmoxCertificateList,
+    state,
+    LoadableComponentState<ViewState>
+);
+
 impl LoadableComponent for ProxmoxCertificateList {
     type Properties = CertificateList;
-    type Message = Msg;
+    type Message = ();
     type ViewState = ViewState;
 
     fn create(ctx: &LoadableComponentContext<Self>) -> Self {
-        let selection = Selection::new().on_select(ctx.link().callback(|_| Msg::Redraw));
+        let selection = Selection::new().on_select({
+            let link = ctx.link().clone();
+            move |_| link.send_redraw()
+        });
         let store =
             Store::with_extract_key(|record: &CertificateInfo| Key::from(record.filename.clone()));
         let columns = Rc::new(columns());
         let rows = Rc::new(rows());
         Self {
+            state: LoadableComponentState::new(),
             selection,
             store,
             columns,
@@ -121,7 +129,7 @@ impl LoadableComponent for ProxmoxCertificateList {
                         "proxy.pem"
                     ))
                     .on_activate({
-                        let link = ctx.link();
+                        let link = ctx.link().clone();
                         move |_| {
                             let link = link.clone();
                             let command_path = "/nodes/localhost/certificates/custom".to_string();
@@ -145,7 +153,7 @@ impl LoadableComponent for ProxmoxCertificateList {
                     .disabled(selected_cert.is_none())
                     .onclick({
                         let selected_cert = selected_cert.clone();
-                        let link = ctx.link();
+                        let link = ctx.link().clone();
                         move |_| {
                             if let Some(selected_cert) = &selected_cert {
                                 let cert_data: Value =
@@ -167,7 +175,7 @@ impl LoadableComponent for ProxmoxCertificateList {
             .selection(self.selection.clone())
             .on_row_dblclick({
                 let store = self.store.clone();
-                let link = ctx.link();
+                let link = ctx.link().clone();
                 move |event: &mut DataTableMouseEvent| {
                     let key = &event.record_key;
                     if let Some(selected_cert) = store.read().lookup_record(key).cloned() {
@@ -226,10 +234,10 @@ async fn update_field_from_file(
 
 impl ProxmoxCertificateList {
     fn create_upload_custom_certificate(&self, ctx: &LoadableComponentContext<Self>) -> Html {
-        let link = ctx.link();
+        let link = ctx.link().clone();
         EditWindow::new(tr!("Upload Custom Certificate"))
             .width(600)
-            .on_close(ctx.link().change_view_callback(|_| None))
+            .on_close(link.change_view_callback(|_| None))
             .submit_text(tr!("Upload"))
             .renderer(move |form_ctx: &FormContext| {
                 Form::new()
@@ -287,7 +295,7 @@ impl ProxmoxCertificateList {
                     .into()
             })
             .on_submit({
-                let link = ctx.link();
+                let link = ctx.link().clone();
                 move |form_ctx: FormContext| {
                     let link = link.clone();
                     async move {
diff --git a/src/apt_package_manager.rs b/src/apt_package_manager.rs
index 82d536f..7264dae 100644
--- a/src/apt_package_manager.rs
+++ b/src/apt_package_manager.rs
@@ -18,13 +18,14 @@ use pwt::widget::data_table::{
     DataTable, DataTableCellRenderArgs, DataTableColumn, DataTableHeader, DataTableHeaderGroup,
 };
 use pwt::widget::{AlertDialog, Button, Container, Toolbar, Tooltip};
-use pwt::AsyncPool;
 
 use crate::percent_encoding::percent_encode_component;
 use crate::subscription_alert::subscription_is_active;
+use crate::LoadableComponentState;
 use crate::SubscriptionAlert;
 use crate::{
-    DataViewWindow, LoadableComponent, LoadableComponentContext, LoadableComponentMaster, XTermJs,
+    DataViewWindow, LoadableComponent, LoadableComponentContext, LoadableComponentMaster,
+    LoadableComponentScopeExt, XTermJs,
 };
 use proxmox_apt_api_types::APTUpdateInfo;
 
@@ -163,31 +164,43 @@ pub enum ViewState {
 /// Messages for [`ProxmoxAptManager::update`]
 pub enum Msg {
     CheckSubscription,
-    SelectionChange,
 }
 
 pub struct ProxmoxAptPackageManager {
+    state: LoadableComponentState<ViewState>,
     tree_store: TreeStore<TreeEntry>,
     selection: Selection,
     columns: Rc<Vec<DataTableHeader<TreeEntry>>>,
-    async_pool: AsyncPool,
 }
 
+crate::impl_deref_mut_property!(
+    ProxmoxAptPackageManager,
+    state,
+    LoadableComponentState<ViewState>
+);
+
 impl LoadableComponent for ProxmoxAptPackageManager {
     type Properties = AptPackageManager;
     type Message = Msg;
     type ViewState = ViewState;
 
     fn create(ctx: &LoadableComponentContext<Self>) -> Self {
+        let props = ctx.props();
         let tree_store = TreeStore::new().view_root(false);
         let columns = Self::columns(ctx, tree_store.clone());
-        let selection = Selection::new().on_select(ctx.link().callback(|_| Msg::SelectionChange));
+        let selection = Selection::new().on_select({
+            let link = ctx.link().clone();
+            move |_| link.send_redraw()
+        });
+
+        let mut state = LoadableComponentState::new();
+        state.set_task_base_url(props.task_base_url.clone());
 
         Self {
+            state,
             tree_store,
             selection,
             columns,
-            async_pool: AsyncPool::new(),
         }
     }
 
@@ -200,14 +213,12 @@ impl LoadableComponent for ProxmoxAptPackageManager {
                     .clone()
                     .subscription_url
                     .unwrap_or("/nodes/localhost/subscription".into());
-                let task_base_url = props.task_base_url.clone();
                 let command = format!("{}/update", props.base_url);
-                self.async_pool.spawn(async move {
+                self.spawn(async move {
                     let data = crate::http_get::<Value>(url.as_str(), None).await;
                     let is_active = subscription_is_active(Some(&data));
 
                     if is_active {
-                        link.task_base_url(task_base_url);
                         link.start_task(&command, None, false);
                     } else {
                         link.change_view(Some(ViewState::ShowSubscriptionPopup));
@@ -215,7 +226,6 @@ impl LoadableComponent for ProxmoxAptPackageManager {
                 });
                 true
             }
-            Msg::SelectionChange => true,
         }
     }
 
@@ -264,15 +274,14 @@ impl LoadableComponent for ProxmoxAptPackageManager {
             .class("pwt-overflow-hidden")
             .class("pwt-border-bottom")
             .with_child(Button::new(tr!("Refresh")).on_activate({
-                let link = ctx.link();
+                let link = ctx.link().clone();
                 let sub_check = props.subscription_url.is_some();
-                link.task_base_url(props.task_base_url.clone());
 
                 let command = format!("{}/update", props.base_url);
 
                 move |_| {
                     if sub_check {
-                        link.send_message(Msg::CheckSubscription);
+                        link.send_custom_message(Msg::CheckSubscription);
                     } else {
                         link.start_task(&command, None, false);
                     }
@@ -287,7 +296,7 @@ impl LoadableComponent for ProxmoxAptPackageManager {
                 Button::new(tr!("Changelog"))
                     .disabled(selected_package.is_none())
                     .onclick({
-                        let link = ctx.link();
+                        let link = ctx.link().clone();
                         let view = selected_package
                             .as_ref()
                             .map(|p| ViewState::ShowChangelog(p.clone()));
@@ -296,8 +305,8 @@ impl LoadableComponent for ProxmoxAptPackageManager {
             )
             .with_flex_spacer()
             .with_child({
-                let loading = ctx.loading();
-                let link = ctx.link();
+                let loading = self.loading();
+                let link = ctx.link().clone();
                 Button::refresh(loading).onclick(move |_| link.send_reload())
             });
 
@@ -325,11 +334,9 @@ impl LoadableComponent for ProxmoxAptPackageManager {
             ViewState::ShowSubscriptionPopup => {
                 let link = ctx.link().clone();
                 let props = ctx.props();
-                let task_base_url = props.task_base_url.clone();
                 let command = format!("{}/update", props.base_url);
                 let on_close = move |_| {
                     link.change_view(None);
-                    link.task_base_url(task_base_url.clone());
                     link.start_task(&command, None, false);
                 };
                 Some(if let Some(msg) = props.subscription_message.clone() {
@@ -351,6 +358,8 @@ impl LoadableComponent for ProxmoxAptPackageManager {
     ) -> bool {
         let props = ctx.props();
 
+        self.set_task_base_url(props.task_base_url.clone());
+
         if props.base_url != old_props.base_url || props.task_base_url != old_props.task_base_url {
             ctx.link().send_reload();
             true
diff --git a/src/apt_repositories.rs b/src/apt_repositories.rs
index 7157c0c..904c052 100644
--- a/src/apt_repositories.rs
+++ b/src/apt_repositories.rs
@@ -22,7 +22,8 @@ use pwt::widget::{Button, Column, Container, Fa, Row, Toolbar, Tooltip};
 use crate::subscription_alert::subscription_is_active;
 use crate::{
     EditWindow, ExistingProduct, LoadableComponent, LoadableComponentContext,
-    LoadableComponentMaster, ProjectInfo, SubscriptionAlert,
+    LoadableComponentMaster, LoadableComponentScopeExt, LoadableComponentState, ProjectInfo,
+    SubscriptionAlert,
 };
 
 use pwt_macros::builder;
@@ -406,7 +407,6 @@ fn apt_configuration_to_tree(config: &APTRepositoriesResult) -> SlabTree<TreeEnt
 }
 
 pub enum Msg {
-    Refresh,
     ToggleEnable,
     UpdateStatus(APTRepositoriesResult),
     SubscriptionInfo(Result<Value, Error>),
@@ -419,6 +419,7 @@ pub enum ViewState {
 }
 
 pub struct ProxmoxAptRepositories {
+    state: LoadableComponentState<ViewState>,
     tree_store: TreeStore<TreeEntry>,
     selection: Selection,
     columns: Rc<Vec<DataTableHeader<TreeEntry>>>,
@@ -430,6 +431,12 @@ pub struct ProxmoxAptRepositories {
     status_columns: Rc<Vec<DataTableHeader<StatusLine>>>,
 }
 
+crate::impl_deref_mut_property!(
+    ProxmoxAptRepositories,
+    state,
+    LoadableComponentState<ViewState>
+);
+
 impl LoadableComponent for ProxmoxAptRepositories {
     type Properties = AptRepositories;
     type Message = Msg;
@@ -438,19 +445,27 @@ impl LoadableComponent for ProxmoxAptRepositories {
     fn create(ctx: &LoadableComponentContext<Self>) -> Self {
         let tree_store = TreeStore::new().view_root(false);
         let columns = Self::columns(ctx, tree_store.clone());
-        let selection = Selection::new().on_select(ctx.link().callback(|_| Msg::Refresh));
+        let selection = Selection::new().on_select({
+            let link = ctx.link().clone();
+            move |_| link.send_redraw()
+        });
         let status_columns = Self::status_columns(ctx);
 
         let subscription_url = ctx.props().subscription_url.clone();
 
-        let link = ctx.link();
-        link.send_future(async move {
-            // TODO: also reload this in load, not only create?!
-            let data = crate::http_get(subscription_url.to_string(), None).await;
-            Msg::SubscriptionInfo(data)
+        let state = LoadableComponentState::new();
+
+        state.spawn({
+            let link = ctx.link().clone();
+            async move {
+                // TODO: also reload this in load, not only create?!
+                let data = crate::http_get(subscription_url.to_string(), None).await;
+                link.send_custom_message(Msg::SubscriptionInfo(data));
+            }
         });
 
         Self {
+            state,
             tree_store,
             selection,
             columns,
@@ -470,13 +485,13 @@ impl LoadableComponent for ProxmoxAptRepositories {
         let props = ctx.props();
         let base_url = props.base_url.clone();
         let tree_store = self.tree_store.clone();
-        let link = ctx.link();
+        let link = ctx.link().clone();
 
         Box::pin(async move {
             let config = apt_configuration(base_url.clone()).await?;
             let tree = apt_configuration_to_tree(&config);
             tree_store.write().update_root_tree(tree);
-            link.send_message(Msg::UpdateStatus(config));
+            link.send_custom_message(Msg::UpdateStatus(config));
             Ok(())
         })
     }
@@ -484,7 +499,6 @@ impl LoadableComponent for ProxmoxAptRepositories {
     fn update(&mut self, ctx: &LoadableComponentContext<Self>, msg: Self::Message) -> bool {
         let props = ctx.props();
         match msg {
-            Msg::Refresh => true,
             Msg::SubscriptionInfo(status) => {
                 self.subscription_status = Some(status);
                 if let Some(config) = &self.config {
@@ -547,7 +561,7 @@ impl LoadableComponent for ProxmoxAptRepositories {
                     });
                     // fixme: add digest to protect against concurrent changes
                     let url = format!("{}/repositories", props.base_url);
-                    let link = ctx.link();
+                    let link = ctx.link().clone();
                     link.clone().spawn(async move {
                         match crate::http_post(url, Some(param)).await {
                             Ok(()) => {
@@ -606,12 +620,12 @@ impl LoadableComponent for ProxmoxAptRepositories {
                     tr!("Enable")
                 })
                 .disabled(enabled.is_none())
-                .onclick(ctx.link().callback(|_| Msg::ToggleEnable))
+                .onclick(ctx.link().custom_callback(|_| Msg::ToggleEnable))
             })
             .with_flex_spacer()
             .with_child({
-                let loading = ctx.loading();
-                let link = ctx.link();
+                let loading = self.loading();
+                let link = ctx.link().clone();
                 Button::refresh(loading).onclick(move |_| link.send_reload())
             });
 
diff --git a/src/auth_view.rs b/src/auth_view.rs
index d6e5528..bb3960b 100644
--- a/src/auth_view.rs
+++ b/src/auth_view.rs
@@ -21,7 +21,8 @@ use pwt_macros::builder;
 
 use crate::{
     AuthEditLDAP, AuthEditOpenID, EditWindow, LoadableComponent, LoadableComponentContext,
-    LoadableComponentLink, LoadableComponentMaster,
+    LoadableComponentMaster, LoadableComponentScope, LoadableComponentScopeExt,
+    LoadableComponentState,
 };
 
 use crate::common_api_types::BasicRealmInfo;
@@ -76,17 +77,20 @@ pub enum ViewState {
 }
 
 pub enum Msg {
-    Redraw,
     Edit,
     Remove,
     Sync,
 }
+
 #[doc(hidden)]
 pub struct ProxmoxAuthView {
+    state: LoadableComponentState<ViewState>,
     selection: Selection,
     store: Store<BasicRealmInfo>,
 }
 
+crate::impl_deref_mut_property!(ProxmoxAuthView, state, LoadableComponentState<ViewState>);
+
 async fn delete_item(base_url: AttrValue, realm: AttrValue) -> Result<(), Error> {
     let url = format!("{base_url}/{}", percent_encode_component(&realm));
     crate::http_delete(&url, None).await?;
@@ -95,7 +99,7 @@ async fn delete_item(base_url: AttrValue, realm: AttrValue) -> Result<(), Error>
 
 async fn sync_realm(
     form_ctx: FormContext,
-    link: LoadableComponentLink<ProxmoxAuthView>,
+    link: LoadableComponentScope<ProxmoxAuthView>,
     url: impl Into<String>,
 ) -> Result<(), Error> {
     let mut data = form_ctx.get_submit_data();
@@ -178,8 +182,15 @@ impl LoadableComponent for ProxmoxAuthView {
 
     fn create(ctx: &LoadableComponentContext<Self>) -> Self {
         let store = Store::new();
-        let selection = Selection::new().on_select(ctx.link().callback(|_| Msg::Redraw));
-        Self { store, selection }
+        let selection = Selection::new().on_select({
+            let link = ctx.link().clone();
+            move |_| link.send_redraw()
+        });
+        Self {
+            state: LoadableComponentState::new(),
+            store,
+            selection,
+        }
     }
 
     fn load(
@@ -201,7 +212,6 @@ impl LoadableComponent for ProxmoxAuthView {
         let props = ctx.props();
 
         match msg {
-            Msg::Redraw => true,
             Msg::Remove => {
                 let Some(info) = self.get_selected_record() else {
                     return true;
@@ -218,7 +228,7 @@ impl LoadableComponent for ProxmoxAuthView {
                     return true;
                 };
 
-                let link = ctx.link();
+                let link = ctx.link().clone();
                 link.clone().spawn(async move {
                     if let Err(err) = delete_item(base_url, info.realm.into()).await {
                         link.show_error(tr!("Unable to delete item"), err, true);
@@ -319,17 +329,17 @@ impl LoadableComponent for ProxmoxAuthView {
             .with_child(
                 Button::new(tr!("Edit"))
                     .disabled(edit_disabled)
-                    .onclick(ctx.link().callback(|_| Msg::Edit)),
+                    .onclick(ctx.link().custom_callback(|_| Msg::Edit)),
             )
             .with_child(
                 Button::new(tr!("Remove"))
                     .disabled(remove_disabled)
-                    .onclick(ctx.link().callback(|_| Msg::Remove)),
+                    .onclick(ctx.link().custom_callback(|_| Msg::Remove)),
             )
             .with_child(
                 Button::new(tr!("Sync"))
                     .disabled(sync_disabled)
-                    .onclick(ctx.link().callback(|_| Msg::Sync)),
+                    .onclick(ctx.link().custom_callback(|_| Msg::Sync)),
             );
 
         Some(toolbar.into())
@@ -341,8 +351,8 @@ impl LoadableComponent for ProxmoxAuthView {
             .selection(self.selection.clone())
             .class("pwt-flex-fit")
             .on_row_dblclick({
-                let link = ctx.link();
-                move |_: &mut _| link.send_message(Msg::Edit)
+                let link = ctx.link().clone();
+                move |_: &mut _| link.send_custom_message(Msg::Edit)
             })
             .into()
     }
@@ -398,7 +408,7 @@ impl LoadableComponent for ProxmoxAuthView {
                     .into(),
             ),
             ViewState::Sync(realm) => {
-                let link = ctx.link();
+                let link = ctx.link().clone();
                 let url = format!(
                     "{}/{}/sync",
                     ctx.props().base_url,
diff --git a/src/configuration/network_view.rs b/src/configuration/network_view.rs
index d7d519c..62b5e2e 100644
--- a/src/configuration/network_view.rs
+++ b/src/configuration/network_view.rs
@@ -12,7 +12,10 @@ use pwt::widget::data_table::{DataTable, DataTableColumn, DataTableHeader};
 use pwt::widget::menu::{Menu, MenuButton, MenuItem};
 use pwt::widget::{Button, Column, Container, SplitPane, Toolbar};
 
-use crate::{LoadableComponent, LoadableComponentContext, LoadableComponentMaster, TaskProgress};
+use crate::{
+    LoadableComponent, LoadableComponentContext, LoadableComponentMaster,
+    LoadableComponentScopeExt, LoadableComponentState, TaskProgress,
+};
 use proxmox_client::ApiResponseData;
 
 use crate::percent_encoding::percent_encode_component;
@@ -66,12 +69,15 @@ impl NetworkView {
 
 #[doc(hidden)]
 pub struct ProxmoxNetworkView {
+    state: LoadableComponentState<ViewState>,
     columns: Rc<Vec<DataTableHeader<Interface>>>,
     store: Store<Interface>,
     changes: String,
     selection: Selection,
 }
 
+crate::impl_deref_mut_property!(ProxmoxNetworkView, state, LoadableComponentState<ViewState>);
+
 #[derive(PartialEq)]
 pub enum ViewState {
     AddBridge,
@@ -81,7 +87,6 @@ pub enum ViewState {
 }
 
 pub enum Msg {
-    SelectionChange,
     RemoveItem,
     Changes(String),
     RevertChanges,
@@ -119,19 +124,23 @@ impl LoadableComponent for ProxmoxNetworkView {
         ctx: &LoadableComponentContext<Self>,
     ) -> Pin<Box<dyn Future<Output = Result<(), Error>>>> {
         let store = self.store.clone();
-        let link = ctx.link();
+        let link = ctx.link().clone();
         Box::pin(async move {
             let (data, changes) = load_interfaces().await?;
             store.write().set_data(data);
-            link.send_message(Msg::Changes(changes));
+            link.send_custom_message(Msg::Changes(changes));
             Ok(())
         })
     }
 
     fn create(ctx: &LoadableComponentContext<Self>) -> Self {
         let store = Store::with_extract_key(|record: &Interface| Key::from(record.name.as_str()));
-        let selection = Selection::new().on_select(ctx.link().callback(|_| Msg::SelectionChange));
+        let selection = Selection::new().on_select({
+            let link = ctx.link().clone();
+            move |_| link.send_redraw()
+        });
         Self {
+            state: LoadableComponentState::new(),
             store,
             selection,
             changes: String::new(),
@@ -141,10 +150,9 @@ impl LoadableComponent for ProxmoxNetworkView {
 
     fn update(&mut self, ctx: &LoadableComponentContext<Self>, msg: Self::Message) -> bool {
         match msg {
-            Msg::SelectionChange => true,
             Msg::RemoveItem => {
                 if let Some(key) = self.selection.selected_key() {
-                    let link = ctx.link();
+                    let link = ctx.link().clone();
                     link.clone().spawn(async move {
                         if let Err(err) = delete_interface(key).await {
                             link.show_error(tr!("Unable to delete item"), err, true);
@@ -159,8 +167,8 @@ impl LoadableComponent for ProxmoxNetworkView {
                 true
             }
             Msg::RevertChanges => {
-                let link = ctx.link();
-                link.clone().spawn(async move {
+                let link = ctx.link().clone();
+                self.spawn(async move {
                     if let Err(err) = revert_changes().await {
                         link.show_error(tr!("Unable to revert changes"), err, true);
                     }
@@ -169,7 +177,7 @@ impl LoadableComponent for ProxmoxNetworkView {
                 false
             }
             Msg::ApplyChanges => {
-                let link = ctx.link();
+                let link = ctx.link().clone();
                 link.clone().spawn(async move {
                     match apply_changes().await {
                         Err(err) => {
@@ -219,7 +227,7 @@ impl LoadableComponent for ProxmoxNetworkView {
             .with_child(
                 Button::new(tr!("Revert"))
                     .disabled(no_changes)
-                    .onclick(link.callback(|_| Msg::RevertChanges)),
+                    .onclick(link.custom_callback(|_| Msg::RevertChanges)),
             )
             .with_child(
                 Button::new(tr!("Edit"))
@@ -229,18 +237,18 @@ impl LoadableComponent for ProxmoxNetworkView {
             .with_child(
                 Button::new(tr!("Remove"))
                     .disabled(disabled)
-                    .onclick(link.callback(|_| Msg::RemoveItem)),
+                    .onclick(link.custom_callback(|_| Msg::RemoveItem)),
             )
             .with_spacer()
             .with_child(
                 Button::new(tr!("Apply Configuration"))
                     .disabled(no_changes)
-                    .onclick(link.callback(|_| Msg::ApplyChanges)),
+                    .onclick(link.custom_callback(|_| Msg::ApplyChanges)),
             )
             .with_flex_spacer()
             .with_child({
-                let loading = ctx.loading();
-                let link = ctx.link();
+                let loading = self.loading();
+                let link = ctx.link().clone();
                 Button::refresh(loading).onclick(move |_| link.send_reload())
             });
 
@@ -248,7 +256,7 @@ impl LoadableComponent for ProxmoxNetworkView {
     }
 
     fn main_view(&self, ctx: &LoadableComponentContext<Self>) -> Html {
-        let link = ctx.link();
+        let link = ctx.link().clone();
 
         let table = DataTable::new(Rc::clone(&self.columns), self.store.clone())
             .class("pwt-flex-fit")
diff --git a/src/configuration/pve/lxc_network_panel.rs b/src/configuration/pve/lxc_network_panel.rs
index 953edba..3527b1e 100644
--- a/src/configuration/pve/lxc_network_panel.rs
+++ b/src/configuration/pve/lxc_network_panel.rs
@@ -23,9 +23,12 @@ use crate::form::pve::lxc_network_property;
 use crate::form::typed_load;
 use crate::{
     configuration::guest_config_url, form::pve::PveGuestType, LoadableComponent,
-    LoadableComponentContext,
+    LoadableComponentContext, LoadableComponentScopeExt,
+};
+use crate::{
+    http_put, impl_deref_mut_property, ConfirmButton, LoadableComponentMaster,
+    LoadableComponentState, PropertyEditDialog,
 };
-use crate::{http_put, ConfirmButton, LoadableComponentMaster, PropertyEditDialog};
 
 #[derive(Clone, PartialEq, Properties)]
 #[builder]
@@ -82,21 +85,23 @@ pub enum ViewState {
 }
 
 pub enum Msg {
-    Redraw,
     SelectionChange,
     Remove(Key),
 }
 
 pub struct LxcNetworkComp {
+    state: LoadableComponentState<ViewState>,
     columns: Rc<Vec<DataTableHeader<NetworkEntry>>>,
     store: Store<NetworkEntry>,
     selection: Selection,
 }
 
+impl_deref_mut_property!(LxcNetworkComp, state, LoadableComponentState<ViewState>);
+
 impl LxcNetworkComp {
     fn edit_dialog(&self, ctx: &LoadableComponentContext<Self>, name: Option<String>) -> Html {
         let props = ctx.props();
-        let link = ctx.link();
+        let link = ctx.link().clone();
 
         let property = lxc_network_property(
             Some(props.node.clone()),
@@ -156,12 +161,17 @@ impl LoadableComponent for LxcNetworkComp {
     }
 
     fn create(ctx: &LoadableComponentContext<Self>) -> Self {
-        let selection = Selection::new().on_select(ctx.link().callback(|_| Msg::SelectionChange));
-        let store = Store::new().on_change(ctx.link().callback(|_| Msg::Redraw));
+        let selection =
+            Selection::new().on_select(ctx.link().custom_callback(|_| Msg::SelectionChange));
+        let store = Store::new().on_change({
+            let link = ctx.link().clone();
+            move |_| link.send_redraw()
+        });
 
         ctx.link().repeated_load(3000);
 
         Self {
+            state: LoadableComponentState::new(),
             store,
             selection,
             columns: columns(),
@@ -170,15 +180,14 @@ impl LoadableComponent for LxcNetworkComp {
 
     fn update(&mut self, ctx: &LoadableComponentContext<Self>, msg: Self::Message) -> bool {
         let props = ctx.props();
-        let link = ctx.link();
+        let link = ctx.link().clone();
         match msg {
             Msg::SelectionChange => true,
-            Msg::Redraw => true,
             Msg::Remove(key) => {
                 let url =
                     guest_config_url(props.vmid, &props.node, &props.remote, PveGuestType::Lxc);
 
-                link.clone().spawn(async move {
+                self.spawn(async move {
                     let param = json!({ "delete": [ key.to_string() ]});
                     let result: Result<(), _> = crate::http_put(&url, Some(param)).await;
                     if let Err(err) = result {
@@ -238,7 +247,7 @@ impl LoadableComponent for LxcNetworkComp {
                         let key = selected_key.clone();
                         move |_| {
                             if let Some(key) = &key {
-                                link.send_message(Msg::Remove(key.clone()))
+                                link.send_custom_message(Msg::Remove(key.clone()))
                             }
                         }
                     })
@@ -259,7 +268,7 @@ impl LoadableComponent for LxcNetworkComp {
     fn main_view(&self, ctx: &LoadableComponentContext<Self>) -> Html {
         let props = ctx.props();
         let readonly = props.readonly;
-        let link = ctx.link();
+        let link = ctx.link().clone();
 
         if props.mobile {
             let mut tiles = Vec::new();
diff --git a/src/lib.rs b/src/lib.rs
index 6df8a32..f4761a2 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -88,7 +88,8 @@ pub mod layout;
 
 mod loadable_component;
 pub use loadable_component::{
-    LoadableComponent, LoadableComponentContext, LoadableComponentLink, LoadableComponentMaster,
+    LoadableComponent, LoadableComponentContext, LoadableComponentMaster, LoadableComponentScope,
+    LoadableComponentScopeExt, LoadableComponentState,
 };
 
 mod node_info;
@@ -336,3 +337,33 @@ pub fn available_language_list() -> Vec<LanguageInfo> {
         ),
     ]
 }
+
+#[cfg(doc)]
+use std::ops::{Deref, DerefMut};
+
+/// Implement [Deref] to a structure member.
+#[macro_export]
+macro_rules! impl_deref_property {
+    ($ty:ty, $property_name:ident, $property_type:ty) => {
+        impl std::ops::Deref for $ty {
+            type Target = $property_type;
+
+            fn deref(&self) -> &Self::Target {
+                &self.$property_name
+            }
+        }
+    };
+}
+
+/// Implement [DerefMut] to a structure member.
+#[macro_export]
+macro_rules! impl_deref_mut_property {
+    ($ty:ty, $property_name:ident, $property_type:ty) => {
+        $crate::impl_deref_property!($ty, $property_name, $property_type);
+        impl std::ops::DerefMut for $ty {
+            fn deref_mut(&mut self) -> &mut Self::Target {
+                &mut self.$property_name
+            }
+        }
+    };
+}
diff --git a/src/loadable_component.rs b/src/loadable_component.rs
index f0e28a9..43cb7a9 100644
--- a/src/loadable_component.rs
+++ b/src/loadable_component.rs
@@ -1,163 +1,313 @@
-use anyhow::Error;
-use serde_json::Value;
 use std::future::Future;
+use std::ops::DerefMut;
 use std::pin::Pin;
-use yew_router::scope_ext::RouterScopeExt;
 
+use anyhow::Error;
 use gloo_timers::callback::Timeout;
 
+use serde_json::Value;
 use yew::html::Scope;
 
 use pwt::dom::DomVisibilityObserver;
 use pwt::prelude::*;
-use pwt::state::NavigationContextExt;
 use pwt::widget::{AlertDialog, Column};
 use pwt::AsyncPool;
 
+#[cfg(doc)]
+use crate::impl_deref_mut_property;
+#[cfg(doc)]
+use pwt::widget::Dialog;
+
 use crate::{TaskProgress, TaskViewer};
 
-pub struct LoadableComponentState {
-    loading: usize,
-    last_load_error: Option<String>,
-    repeat_timespan: u32, /* 0 => no repeated loading */
-    task_base_url: Option<AttrValue>,
-}
+pub type LoadableComponentContext<L> = Context<LoadableComponentMaster<L>>;
+pub type LoadableComponentScope<L> = Scope<LoadableComponentMaster<L>>;
+
+/// Loadable Components
+///
+/// - Load data using an async function [LoadableComponent::load]
+/// - repeated load possible
+/// - pause repeated load when component is not visible (uses [DomVisibilityObserver])
+/// - display the loaded data [LoadableComponent::main_view]
+/// - display an optional toolbar [LoadableComponent::toolbar]
+/// - display any errors from failed load.
+/// - display additional dialogs depening on [LoadableComponent::ViewState]
+///
+/// The [LoadableComponentScopeExt] defines available control function on the scope.
+///
+/// The [LoadableComponentState] provides acces to load status informations and add the ability
+/// to spawn tasks.
+///
+/// ```
+/// use proxmox_yew_comp::{LoadableComponent, LoadableComponentState, LoadableComponentContext};
+/// // include the scope extension for (for `change_view`, `send_custom_message`, ...)
+/// use proxmox_yew_comp::LoadableComponentScopeExt;
+/// # use std::pin::Pin;
+/// # use std::rc::Rc;
+/// # use std::future::Future;
+/// # use pwt::prelude::*;
+/// # use proxmox_yew_comp::http_get;
+/// # use yew::virtual_dom::{VComp, VNode, Key};
+///
+/// // define the component properties
+/// #[derive(Clone, PartialEq, Properties)]
+/// pub struct MyComponent {
+///     key: Option<Key>,
+///     /* add whatever you need  */
+/// };
+///
+/// // define your view states
+/// #[derive(PartialEq)]
+/// pub enum ViewState { Add, Edit }
+///
+/// // define the component message type
+/// pub enum Msg { UpdateData(String) }
+///
+/// // define the component state
+/// pub struct MyComponentState {
+///     // you need to inlucde a LoadableComponentState
+///     state: LoadableComponentState<ViewState>,
+///     // Add any other data you need
+///     loaded_data: Option<String>,
+/// }
+///
+/// // implement DerefMut
+/// proxmox_yew_comp::impl_deref_mut_property!(
+///     MyComponentState,
+///     state,
+///     LoadableComponentState<ViewState>
+/// );
+///
+/// impl LoadableComponent for MyComponentState {
+///     type Properties = MyComponent;
+///     type Message = Msg; // component message type
+///     type ViewState = ViewState;
+///
+///     fn create(ctx: &LoadableComponentContext<Self>) -> Self {
+///         Self {
+///             state: LoadableComponentState::new(),
+///             loaded_data: None,
+///         }
+///     }
+///
+///     fn load(
+///         &self,
+///         ctx: &LoadableComponentContext<Self>,
+///     ) -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>> {
+///         let link = ctx.link().clone();
+///         Box::pin(async move {
+///             let data = http_get("/something", None).await?; // load something here
+///             link.send_custom_message(Msg::UpdateData(data));
+///             Ok(())
+///         })
+///     }
+///
+///     fn update(&mut self, ctx: &LoadableComponentContext<Self>, msg: Self::Message) -> bool {
+///         match msg {
+///             Msg::UpdateData(data) => self.loaded_data = Some(data),
+///         }
+///         true
+///     }
+///
+///     fn main_view(&self, ctx: &LoadableComponentContext<Self>) -> Html {
+///         let text: String  = if let Some(data) = &self.loaded_data {
+///             data.clone()
+///         } else {
+///             "no data".into()
+///         };
+///         html!{text}
+///     }
+/// }
+///
+/// // add ability to generate the yew Component (provided by [LoadableComponentMaster])
+/// use proxmox_yew_comp::LoadableComponentMaster;
+/// impl From<MyComponent> for VNode {
+///     fn from(props: MyComponent) -> VNode {
+///         let key =  props.key.clone();
+///         let comp = VComp::new::<LoadableComponentMaster<MyComponentState>>(Rc::new(props), key);
+///         VNode::from(comp)
+///     }
+/// }
+///
+/// ```
+pub trait LoadableComponent:
+    Sized + DerefMut<Target = LoadableComponentState<Self::ViewState>> + 'static
+{
+    /// The yew component properties.
+    type Properties: Properties;
+    /// The yew component message type.
+    type Message: 'static;
+    /// The view state
+    ///
+    /// The view state of the component can be changed with [LoadableComponentScopeExt::change_view].
+    /// The value is then passed to the [LoadableComponent::dialog_view] function which can render
+    /// different dialogs.
+    type ViewState: 'static + PartialEq;
 
-pub struct LoadableComponentContext<'a, L: LoadableComponent + Sized + 'static> {
-    ctx: &'a Context<LoadableComponentMaster<L>>,
-    comp_state: &'a LoadableComponentState,
-}
+    /// Create a new instance
+    fn create(ctx: &LoadableComponentContext<Self>) -> Self;
+
+    /// Async Load
+    fn load(
+        &self,
+        ctx: &LoadableComponentContext<Self>,
+    ) -> Pin<Box<dyn Future<Output = Result<(), Error>>>>;
 
-impl<L: LoadableComponent + Sized> LoadableComponentContext<'_, L> {
-    pub fn props(&self) -> &L::Properties {
-        self.ctx.props()
+    /// Yew component update function (see [Component::update])
+    #[allow(unused_variables)]
+    fn update(&mut self, ctx: &LoadableComponentContext<Self>, msg: Self::Message) -> bool {
+        true
     }
-    pub fn link(&self) -> LoadableComponentLink<L> {
-        LoadableComponentLink {
-            link: self.ctx.link().clone(),
-        }
+
+    /// Yew component changed function (see [Component::changed])
+    #[allow(unused_variables)]
+    fn changed(
+        &mut self,
+        ctx: &LoadableComponentContext<Self>,
+        _old_props: &Self::Properties,
+    ) -> bool {
+        true
     }
-    pub fn loading(&self) -> bool {
-        self.comp_state.loading > 0
+
+    /// Optional toolbar
+    #[allow(unused_variables)]
+    fn toolbar(&self, ctx: &LoadableComponentContext<Self>) -> Option<Html> {
+        None
     }
 
-    pub fn last_load_errors(&self) -> Option<&str> {
-        self.comp_state.last_load_error.as_deref()
+    /// Main view (see [Component::view])
+    ///
+    /// The difference is that we render the result into a [Column], with an optional
+    /// toolbar on the top.
+    fn main_view(&self, ctx: &LoadableComponentContext<Self>) -> Html;
+
+    /// ViewState dependent dialogs
+    ///
+    /// The result is rendered below the main view. Usually some kind of [Dialog] window.
+    ///
+    /// The view state can be changed with `link.change_view(..)` and `link.change_view_callback(...)`.
+    #[allow(unused_variables)]
+    fn dialog_view(
+        &self,
+        ctx: &LoadableComponentContext<Self>,
+        view_state: &Self::ViewState,
+    ) -> Option<Html> {
+        None
     }
+
+    /// Yew component rendered function (see [Component::rendered])
+    #[allow(unused_variables)]
+    fn rendered(&mut self, ctx: &LoadableComponentContext<Self>, first_render: bool) {}
 }
 
-pub struct LoadableComponentLink<L: LoadableComponent + Sized + 'static> {
-    link: Scope<LoadableComponentMaster<L>>,
+#[derive(Clone, PartialEq)]
+pub enum ViewState<V: PartialEq> {
+    Main,
+    /// Show the dialog returned by dialog_view
+    Dialog(V),
+    /// Show proxmox api task status
+    TaskProgress(String),
+    /// Show proxmox api task log
+    TaskLog(String, Option<i64>),
+    /// Show an error message dialog
+    Error(String, String, /* reload_on_close */ bool),
 }
 
-impl<L: LoadableComponent + Sized> Clone for LoadableComponentLink<L> {
-    fn clone(&self) -> Self {
-        Self {
-            link: self.link.clone(),
-        }
-    }
+pub enum Msg<M, V: PartialEq> {
+    DataChange,
+    Load,
+    RepeatedLoad(u32 /* repeat time in miliseconds */),
+    LoadResult(Result<(), Error>),
+    ChangeView(/*reload*/ bool, ViewState<V>),
+    ChildMessage(M),
+    Visible(bool),
+    Spawn(Pin<Box<dyn Future<Output = ()>>>),
 }
 
-impl<L: LoadableComponent + Sized> LoadableComponentLink<L> {
-    pub fn send_message(&self, msg: impl Into<L::Message>) {
-        let msg = msg.into();
-        self.link.send_message(Msg::ChildMessage(msg));
-    }
+pub trait LoadableComponentScopeExt<M, V: PartialEq> {
+    fn send_custom_message(&self, msg: M);
+    fn send_reload(&self);
+    fn send_redraw(&self);
+    fn repeated_load(&self, miliseconds: u32);
 
-    pub fn callback<F, IN, M>(&self, function: F) -> Callback<IN>
+    fn change_view(&self, child_view_state: Option<V>);
+    fn custom_callback<F, IN>(&self, function: F) -> Callback<IN>
     where
-        M: Into<L::Message>,
-        F: Fn(IN) -> M + 'static,
-    {
-        self.link.callback(move |p: IN| {
-            let msg: L::Message = function(p).into();
-            Msg::ChildMessage(msg)
-        })
-    }
+        M: Into<M>,
+        F: Fn(IN) -> M + 'static;
 
-    /// Spawn a future using the [AsyncPool] from the component.
-    pub fn spawn<Fut>(&self, future: Fut)
+    fn change_view_callback<C, F, IN>(&self, function: F) -> Callback<IN>
     where
-        Fut: Future<Output = ()> + 'static,
-    {
-        self.link.send_message(Msg::Spawn(Box::pin(future)));
-    }
+        C: Into<Option<V>>,
+        F: Fn(IN) -> C + 'static;
 
-    pub fn send_future<Fut, M>(&self, future: Fut)
+    /// Spawn a future using the [AsyncPool] from the component.
+    fn spawn<Fut>(&self, future: Fut)
     where
-        M: Into<L::Message>,
-        Fut: Future<Output = M> + 'static,
-    {
-        let link = self.link.clone();
-        self.link.send_message(Msg::Spawn(Box::pin(async move {
-            let message: L::Message = future.await.into();
-            link.send_message(Msg::ChildMessage(message));
-        })));
-    }
+        Fut: Future<Output = ()> + 'static;
 
-    pub fn callback_future<F, Fut, IN, M>(&self, function: F) -> Callback<IN>
-    where
-        M: Into<L::Message>,
-        Fut: Future<Output = M> + 'static,
-        F: Fn(IN) -> Fut + 'static,
-    {
-        let link = self.clone();
+    fn show_error(
+        &self,
+        title: impl Into<String>,
+        msg: impl std::fmt::Display,
+        reload_on_close: bool,
+    );
 
-        let closure = move |input: IN| {
-            link.send_future(function(input));
-        };
+    fn show_task_progres(&self, task_id: impl Into<String>);
 
-        closure.into()
-    }
+    fn show_task_log(&self, task_id: impl Into<String>, endtime: Option<i64>);
 
-    pub fn send_reload(&self) {
-        self.link.send_message(Msg::Load)
-    }
+    fn start_task(&self, command_path: impl Into<String>, data: Option<Value>, short: bool);
+}
 
-    pub fn repeated_load(&self, miliseconds: u32) {
-        self.link.send_message(Msg::RepeatedLoad(miliseconds));
+impl<M, V: PartialEq, T: 'static + LoadableComponent<Message = M, ViewState = V>>
+    LoadableComponentScopeExt<M, V> for Scope<LoadableComponentMaster<T>>
+{
+    fn send_custom_message(&self, msg: M) {
+        self.send_message(Msg::ChildMessage(msg));
     }
 
-    pub fn task_base_url(&self, base_url: impl Into<AttrValue>) {
-        self.link.send_message(Msg::TaskBaseUrl(base_url.into()));
+    fn send_reload(&self) {
+        self.send_message(Msg::Load);
     }
 
-    pub fn show_error(
-        &self,
-        title: impl Into<String>,
-        msg: impl std::fmt::Display,
-        reload_on_close: bool,
-    ) {
-        let view_state = ViewState::Error(title.into(), msg.to_string(), reload_on_close);
-        self.link.send_message(Msg::ChangeView(false, view_state));
+    fn send_redraw(&self) {
+        self.send_message(Msg::DataChange);
     }
 
-    pub fn show_task_progres(&self, task_id: impl Into<String>) {
-        let view_state = ViewState::TaskProgress(task_id.into());
-        self.link.send_message(Msg::ChangeView(false, view_state));
+    fn repeated_load(&self, miliseconds: u32) {
+        self.send_message(Msg::RepeatedLoad(miliseconds));
     }
 
-    pub fn show_task_log(&self, task_id: impl Into<String>, endtime: Option<i64>) {
-        let view_state = ViewState::TaskLog(task_id.into(), endtime);
-        self.link.send_message(Msg::ChangeView(false, view_state));
+    fn custom_callback<F, IN>(&self, function: F) -> Callback<IN>
+    where
+        M: Into<M>,
+        F: Fn(IN) -> M + 'static,
+    {
+        let scope = self.clone();
+        let closure = move |input| {
+            let output = function(input);
+            scope.send_custom_message(output);
+        };
+        Callback::from(closure)
     }
 
-    pub fn change_view(&self, child_view_state: Option<L::ViewState>) {
+    fn change_view(&self, child_view_state: Option<V>) {
         let view_state = if let Some(child_view_state) = child_view_state {
             ViewState::Dialog(child_view_state)
         } else {
             ViewState::Main
         };
-        self.link.send_message(Msg::ChangeView(false, view_state));
+        self.send_message(Msg::ChangeView(false, view_state));
     }
 
-    pub fn change_view_callback<F, IN, M>(&self, function: F) -> Callback<IN>
+    fn change_view_callback<C, F, IN>(&self, function: F) -> Callback<IN>
     where
-        M: Into<Option<L::ViewState>>,
-        F: Fn(IN) -> M + 'static,
+        C: Into<Option<V>>,
+        F: Fn(IN) -> C + 'static,
     {
-        self.link.callback(move |p: IN| {
-            let state: Option<L::ViewState> = function(p).into();
+        self.callback(move |p: IN| {
+            let state: Option<V> = function(p).into();
             if let Some(state) = state {
                 Msg::ChangeView(true, ViewState::Dialog(state))
             } else {
@@ -166,11 +316,38 @@ impl<L: LoadableComponent + Sized> LoadableComponentLink<L> {
         })
     }
 
-    pub fn start_task(&self, command_path: impl Into<String>, data: Option<Value>, short: bool) {
+    fn spawn<Fut>(&self, future: Fut)
+    where
+        Fut: Future<Output = ()> + 'static,
+    {
+        self.send_message(Msg::Spawn(Box::pin(future)));
+    }
+
+    fn show_error(
+        &self,
+        title: impl Into<String>,
+        msg: impl std::fmt::Display,
+        reload_on_close: bool,
+    ) {
+        let view_state = ViewState::Error(title.into(), msg.to_string(), reload_on_close);
+        self.send_message(Msg::ChangeView(false, view_state));
+    }
+
+    fn show_task_progres(&self, task_id: impl Into<String>) {
+        let view_state = ViewState::TaskProgress(task_id.into());
+        self.send_message(Msg::ChangeView(false, view_state));
+    }
+
+    fn show_task_log(&self, task_id: impl Into<String>, endtime: Option<i64>) {
+        let view_state = ViewState::TaskLog(task_id.into(), endtime);
+        self.send_message(Msg::ChangeView(false, view_state));
+    }
+
+    fn start_task(&self, command_path: impl Into<String>, data: Option<Value>, short: bool) {
         let command_path: String = command_path.into();
         let link = self.clone();
         let command_future = crate::http_post::<String>(command_path, data);
-        self.link.send_message(Msg::Spawn(Box::pin(async move {
+        self.send_message(Msg::Spawn(Box::pin(async move {
             match command_future.await {
                 Ok(task_id) => {
                     link.send_reload();
@@ -187,140 +364,86 @@ impl<L: LoadableComponent + Sized> LoadableComponentLink<L> {
             }
         })));
     }
-
-    /// Returns the original [`yew::html::Scope`] of the master component.
-    ///
-    /// This is useful when e.g. trying to get an higher level context
-    pub fn yew_link(&self) -> &Scope<LoadableComponentMaster<L>> {
-        &self.link
-    }
 }
 
-impl<L: LoadableComponent + Sized> RouterScopeExt for LoadableComponentLink<L> {
-    fn navigator(&self) -> Option<yew_router::prelude::Navigator> {
-        self.link.navigator()
-    }
-
-    fn location(&self) -> Option<yew_router::prelude::Location> {
-        self.link.location()
-    }
-
-    fn route<R>(&self) -> Option<R>
-    where
-        R: yew_router::Routable + 'static,
-    {
-        self.link.route()
-    }
-
-    fn add_location_listener(
-        &self,
-        cb: Callback<yew_router::prelude::Location>,
-    ) -> Option<yew_router::prelude::LocationHandle> {
-        self.link.add_location_listener(cb)
-    }
-
-    fn add_navigator_listener(
-        &self,
-        cb: Callback<yew_router::prelude::Navigator>,
-    ) -> Option<yew_router::prelude::NavigatorHandle> {
-        self.link.add_navigator_listener(cb)
-    }
+/// Base state for [LoadableComponent] implementations.
+///
+/// The struct provides the following features:
+///
+/// - access to load status informations
+/// - setup task base url
+/// - spawn tasks: includes an [AsyncPool], so that any [LoadableComponent] can spawn
+///   task via this pool.
+///
+/// The [LoadableComponent] trait requires access to this struct via [DerefMut]. The
+/// macro [impl_deref_mut_property] provides an easy way to
+/// implement that.
+///
+/// ```
+/// use proxmox_yew_comp::LoadableComponentState;
+/// # #[derive(PartialEq)]
+/// # pub enum ViewState { Add, Edit }
+/// pub struct MyComponentState {
+///     state: LoadableComponentState<ViewState>,
+///     // Add any other data you need
+///     other_data: String,
+/// }
+/// // implement DerefMut
+/// proxmox_yew_comp::impl_deref_mut_property!(MyComponentState, state, LoadableComponentState<ViewState>);
+/// ```
+pub struct LoadableComponentState<V: PartialEq> {
+    loading: usize,
+    last_load_error: Option<String>,
+    repeat_timespan: u32, /* 0 => no repeated loading */
+    task_base_url: Option<AttrValue>,
+    view_state: ViewState<V>,
+    reload_timeout: Option<Timeout>,
+    visible: bool,
+    visibitlity_observer: Option<DomVisibilityObserver>,
+    node_ref: NodeRef,
+    async_pool: AsyncPool,
 }
 
-impl<L: LoadableComponent + Sized> NavigationContextExt for LoadableComponentLink<L> {
-    fn nav_context(&self) -> Option<pwt::state::NavigationContext> {
-        self.link.nav_context()
-    }
-
-    fn full_path(&self) -> Option<String> {
-        self.link.full_path()
-    }
-
-    fn push_relative_route(&self, path: &str) {
-        self.link.push_relative_route(path)
+impl<V: PartialEq> LoadableComponentState<V> {
+    pub fn new() -> Self {
+        Self {
+            loading: 0,
+            last_load_error: None,
+            repeat_timespan: 0,
+            task_base_url: None,
+            view_state: ViewState::Main,
+            reload_timeout: None,
+            visible: true,
+            visibitlity_observer: None,
+            node_ref: NodeRef::default(),
+            async_pool: AsyncPool::new(),
+        }
     }
-}
-
-pub trait LoadableComponent: Sized {
-    type Properties: Properties;
-    type Message: 'static;
-    type ViewState: 'static + PartialEq;
 
-    fn create(ctx: &LoadableComponentContext<Self>) -> Self;
-
-    fn load(
-        &self,
-        ctx: &LoadableComponentContext<Self>,
-    ) -> Pin<Box<dyn Future<Output = Result<(), Error>>>>;
-
-    #[allow(unused_variables)]
-    fn update(&mut self, ctx: &LoadableComponentContext<Self>, msg: Self::Message) -> bool {
-        true
+    pub fn loading(&self) -> bool {
+        self.loading > 0
     }
 
-    #[allow(unused_variables)]
-    fn changed(
-        &mut self,
-        ctx: &LoadableComponentContext<Self>,
-        _old_props: &Self::Properties,
-    ) -> bool {
-        true
+    pub fn last_load_errors(&self) -> Option<&str> {
+        self.last_load_error.as_deref()
     }
 
-    #[allow(unused_variables)]
-    fn toolbar(&self, ctx: &LoadableComponentContext<Self>) -> Option<Html> {
-        None
+    pub fn set_task_base_url(&mut self, base_url: AttrValue) {
+        self.task_base_url = Some(base_url);
     }
 
-    fn main_view(&self, ctx: &LoadableComponentContext<Self>) -> Html;
-
-    #[allow(unused_variables)]
-    fn dialog_view(
-        &self,
-        ctx: &LoadableComponentContext<Self>,
-        view_state: &Self::ViewState,
-    ) -> Option<Html> {
-        None
+    /// Spawn a future using the [AsyncPool] from the component.
+    pub fn spawn<Fut>(&self, future: Fut)
+    where
+        Fut: Future<Output = ()> + 'static,
+    {
+        self.async_pool.spawn(future);
     }
-
-    #[allow(unused_variables)]
-    fn rendered(&mut self, ctx: &LoadableComponentContext<Self>, first_render: bool) {}
-}
-
-#[derive(Clone, PartialEq)]
-pub enum ViewState<V: PartialEq> {
-    Main,
-    /// Show the dialog returned by dialog_view
-    Dialog(V),
-    /// Show proxmox api task status
-    TaskProgress(String),
-    /// Show proxmox api task log
-    TaskLog(String, Option<i64>),
-    /// Show an error message dialog
-    Error(String, String, /* reload_on_close */ bool),
-}
-
-pub enum Msg<M, V: PartialEq> {
-    DataChange,
-    Load,
-    RepeatedLoad(u32 /* repeat time in miliseconds */),
-    LoadResult(Result<(), Error>),
-    ChangeView(/*reload*/ bool, ViewState<V>),
-    ChildMessage(M),
-    TaskBaseUrl(AttrValue),
-    Visible(bool),
-    Spawn(Pin<Box<dyn Future<Output = ()>>>),
 }
 
+#[doc(hidden)]
 pub struct LoadableComponentMaster<L: LoadableComponent> {
     state: L,
-    comp_state: LoadableComponentState,
-    view_state: ViewState<L::ViewState>,
-    reload_timeout: Option<Timeout>,
-    visible: bool,
-    visibitlity_observer: Option<DomVisibilityObserver>,
-    node_ref: NodeRef,
-    async_pool: AsyncPool,
 }
 
 impl<L: LoadableComponent + 'static> Component for LoadableComponentMaster<L> {
@@ -328,91 +451,65 @@ impl<L: LoadableComponent + 'static> Component for LoadableComponentMaster<L> {
     type Properties = L::Properties;
 
     fn create(ctx: &Context<Self>) -> Self {
-        let loading = 0;
-
-        let comp_state = LoadableComponentState {
-            loading,
-            last_load_error: None,
-            repeat_timespan: 0,
-            task_base_url: None,
-        };
-
-        let sub_context = LoadableComponentContext {
-            ctx,
-            comp_state: &comp_state,
-        };
-
         // Send Msg::Load first (before any Msg::RepeatedLoad in create), so that we
         // can avoid multiple loads at startup
         ctx.link().send_message(Msg::Load);
 
-        let state = L::create(&sub_context);
+        let mut state = L::create(ctx);
+        state.visible = true;
 
-        Self {
-            state,
-            comp_state,
-            view_state: ViewState::Main,
-            reload_timeout: None,
-            visible: true,
-            visibitlity_observer: None,
-            node_ref: NodeRef::default(),
-            async_pool: AsyncPool::new(),
-        }
+        Self { state }
     }
 
     fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
         match msg {
             Msg::Spawn(future) => {
-                self.async_pool.spawn(future);
+                self.state.async_pool.spawn(future);
                 false
             }
             Msg::DataChange => true,
             Msg::Load => {
-                self.comp_state.loading += 1;
+                let load_future = self.state.load(ctx);
+                self.state.loading += 1;
                 let link = ctx.link().clone();
-                let sub_context = LoadableComponentContext {
-                    ctx,
-                    comp_state: &self.comp_state,
-                };
-                let load_future = self.state.load(&sub_context);
-                self.async_pool.spawn(async move {
+                self.state.async_pool.spawn(async move {
                     let data = load_future.await;
                     link.send_message(Msg::LoadResult(data));
                 });
                 true
             }
             Msg::RepeatedLoad(timespan) => {
-                self.comp_state.repeat_timespan = timespan;
-                self.reload_timeout = None;
-                if self.comp_state.loading == 0 {
+                self.state.repeat_timespan = timespan;
+                self.state.reload_timeout = None;
+                if self.state.loading == 0 {
                     <Self as yew::Component>::update(self, ctx, Msg::Load);
                 }
                 false
             }
             Msg::LoadResult(data) => {
-                self.comp_state.loading -= 1;
+                self.state.loading -= 1;
                 match data {
                     Ok(()) => {
-                        self.comp_state.last_load_error = None;
+                        self.state.last_load_error = None;
                     }
                     Err(err) => {
-                        let this_is_the_first_error = self.comp_state.last_load_error.is_none();
-                        self.comp_state.last_load_error = Some(err.to_string());
+                        let this_is_the_first_error = self.state.last_load_error.is_none();
+                        self.state.last_load_error = Some(err.to_string());
                         if this_is_the_first_error {
-                            self.view_state =
+                            self.state.view_state =
                                 ViewState::Error(tr!("Load failed"), err.to_string(), false);
                         }
                     }
                 }
 
-                self.reload_timeout = None;
-                if self.comp_state.loading == 0 {
+                self.state.reload_timeout = None;
+                if self.state.loading == 0 {
                     /* no outstanding loads */
-                    if self.comp_state.repeat_timespan > 0 {
+                    if self.state.repeat_timespan > 0 {
                         let link = ctx.link().clone();
-                        if self.visible {
-                            self.reload_timeout =
-                                Some(Timeout::new(self.comp_state.repeat_timespan, move || {
+                        if self.state.visible {
+                            self.state.reload_timeout =
+                                Some(Timeout::new(self.state.repeat_timespan, move || {
                                     link.send_message(Msg::Load);
                                 }));
                         }
@@ -421,7 +518,7 @@ impl<L: LoadableComponent + 'static> Component for LoadableComponentMaster<L> {
                 true
             }
             Msg::ChangeView(reload_data, view_state) => {
-                if self.view_state == view_state {
+                if self.state.view_state == view_state {
                     return false;
                 }
 
@@ -429,29 +526,21 @@ impl<L: LoadableComponent + 'static> Component for LoadableComponentMaster<L> {
                     ctx.link().send_message(Msg::Load);
                 }
 
-                self.view_state = view_state;
+                self.state.view_state = view_state;
                 true
             }
             Msg::ChildMessage(child_msg) => {
-                let sub_context = LoadableComponentContext {
-                    ctx,
-                    comp_state: &self.comp_state,
-                };
-                self.state.update(&sub_context, child_msg);
+                self.state.update(ctx, child_msg);
                 true
             }
-            Msg::TaskBaseUrl(base_url) => {
-                self.comp_state.task_base_url = Some(base_url);
-                false
-            }
             Msg::Visible(visible) => {
-                if self.visible == visible {
+                if self.state.visible == visible {
                     return false;
                 }
-                self.visible = visible;
-                if self.comp_state.loading == 0 && self.visible {
+                self.state.visible = visible;
+                if self.state.loading == 0 && self.state.visible {
                     /* no outstanding loads */
-                    if self.comp_state.loading == 0 {
+                    if self.state.loading == 0 {
                         <Self as yew::Component>::update(self, ctx, Msg::Load);
                     }
                 }
@@ -461,26 +550,16 @@ impl<L: LoadableComponent + 'static> Component for LoadableComponentMaster<L> {
     }
 
     fn changed(&mut self, ctx: &Context<Self>, _old_props: &Self::Properties) -> bool {
-        let sub_context = LoadableComponentContext {
-            ctx,
-            comp_state: &self.comp_state,
-        };
-
-        self.state.changed(&sub_context, _old_props)
+        self.state.changed(ctx, _old_props)
     }
 
     fn view(&self, ctx: &Context<Self>) -> Html {
-        let sub_context = LoadableComponentContext {
-            ctx,
-            comp_state: &self.comp_state,
-        };
-
-        let main_view = self.state.main_view(&sub_context);
+        let main_view = self.state.main_view(ctx);
 
         let dialog: Option<Html> =
-            match &self.view_state {
+            match &self.state.view_state {
                 ViewState::Main => None,
-                ViewState::Dialog(view_state) => self.state.dialog_view(&sub_context, view_state),
+                ViewState::Dialog(view_state) => self.state.dialog_view(ctx, view_state),
                 ViewState::Error(title, msg, reload_on_close) => {
                     let reload_on_close = *reload_on_close;
                     Some(
@@ -498,7 +577,7 @@ impl<L: LoadableComponent + 'static> Component for LoadableComponentMaster<L> {
                             .callback(move |_| Msg::ChangeView(true, ViewState::Main)),
                     );
 
-                    if let Some(base_url) = &self.comp_state.task_base_url {
+                    if let Some(base_url) = &self.state.task_base_url {
                         task_progress.set_base_url(base_url);
                     }
 
@@ -510,19 +589,19 @@ impl<L: LoadableComponent + 'static> Component for LoadableComponentMaster<L> {
                             .callback(move |_| Msg::ChangeView(true, ViewState::Main)),
                     );
 
-                    if let Some(base_url) = &self.comp_state.task_base_url {
+                    if let Some(base_url) = &self.state.task_base_url {
                         task_viewer.set_base_url(base_url);
                     }
                     Some(task_viewer.into())
                 }
             };
 
-        let toolbar = self.state.toolbar(&sub_context);
+        let toolbar = self.state.toolbar(ctx);
 
         let mut alert_msg = None;
 
         if dialog.is_none() {
-            if let Some(msg) = &self.comp_state.last_load_error {
+            if let Some(msg) = &self.state.last_load_error {
                 alert_msg = Some(pwt::widget::error_message(msg).class("pwt-border-top"));
             }
         }
@@ -533,23 +612,18 @@ impl<L: LoadableComponent + 'static> Component for LoadableComponentMaster<L> {
             .with_child(main_view)
             .with_optional_child(alert_msg)
             .with_optional_child(dialog)
-            .into_html_with_ref(self.node_ref.clone())
+            .into_html_with_ref(self.state.node_ref.clone())
     }
 
     fn rendered(&mut self, ctx: &Context<Self>, first_render: bool) {
-        if self.visibitlity_observer.is_none() && self.reload_timeout.is_some() {
-            if let Some(el) = self.node_ref.cast::<web_sys::Element>() {
-                self.visibitlity_observer = Some(DomVisibilityObserver::new(
+        if self.state.visibitlity_observer.is_none() && self.state.reload_timeout.is_some() {
+            if let Some(el) = self.state.node_ref.cast::<web_sys::Element>() {
+                self.state.visibitlity_observer = Some(DomVisibilityObserver::new(
                     &el,
                     ctx.link().callback(Msg::Visible),
                 ))
             }
         }
-        let sub_context = LoadableComponentContext {
-            ctx,
-            comp_state: &self.comp_state,
-        };
-
-        self.state.rendered(&sub_context, first_render);
+        self.state.rendered(ctx, first_render);
     }
 }
diff --git a/src/node_status_panel.rs b/src/node_status_panel.rs
index 3f8946d..9249c01 100644
--- a/src/node_status_panel.rs
+++ b/src/node_status_panel.rs
@@ -17,7 +17,7 @@ use proxmox_node_status::{NodePowerCommand, NodeStatus};
 use crate::utils::copy_text_to_clipboard;
 use crate::{
     http_get, http_post, node_info, ConfirmButton, LoadableComponent, LoadableComponentContext,
-    LoadableComponentMaster,
+    LoadableComponentMaster, LoadableComponentScopeExt, LoadableComponentState,
 };
 
 #[derive(Properties, Clone, PartialEq)]
@@ -53,7 +53,6 @@ enum Msg {
     Error(Error),
     Loaded(Rc<NodeStatus>),
     RebootOrShutdown(NodePowerCommand),
-    Reload,
 }
 
 #[derive(PartialEq)]
@@ -62,10 +61,17 @@ enum ViewState {
 }
 
 struct ProxmoxNodeStatusPanel {
+    state: LoadableComponentState<ViewState>,
     node_status: Option<Rc<NodeStatus>>,
     error: Option<Error>,
 }
 
+crate::impl_deref_mut_property!(
+    ProxmoxNodeStatusPanel,
+    state,
+    LoadableComponentState<ViewState>
+);
+
 impl ProxmoxNodeStatusPanel {
     fn change_power_state(&self, ctx: &LoadableComponentContext<Self>, command: NodePowerCommand) {
         let Some(url) = ctx.props().status_base_url.clone() else {
@@ -79,8 +85,8 @@ impl ProxmoxNodeStatusPanel {
             }));
 
             match http_post(url.as_str(), data).await {
-                Ok(()) => link.send_message(Msg::Reload),
-                Err(err) => link.send_message(Msg::Error(err)),
+                Ok(()) => link.send_redraw(),
+                Err(err) => link.send_custom_message(Msg::Error(err)),
             }
         });
     }
@@ -90,8 +96,8 @@ impl ProxmoxNodeStatusPanel {
         ctx: &LoadableComponentContext<Self>,
         fingerprint: &str,
     ) -> Dialog {
-        let link = ctx.link();
-        let link_button = ctx.link();
+        let link = ctx.link().clone();
+        let link_button = ctx.link().clone();
         let fingerprint = fingerprint.to_owned();
 
         Dialog::new(tr!("Fingerprint"))
@@ -135,10 +141,10 @@ impl LoadableComponent for ProxmoxNodeStatusPanel {
     type ViewState = ViewState;
     type Properties = NodeStatusPanel;
 
-    fn create(ctx: &crate::LoadableComponentContext<Self>) -> Self {
+    fn create(ctx: &LoadableComponentContext<Self>) -> Self {
         ctx.link().repeated_load(5000);
-
         Self {
+            state: LoadableComponentState::new(),
             node_status: None,
             error: None,
         }
@@ -146,7 +152,7 @@ impl LoadableComponent for ProxmoxNodeStatusPanel {
 
     fn load(
         &self,
-        ctx: &crate::LoadableComponentContext<Self>,
+        ctx: &LoadableComponentContext<Self>,
     ) -> std::pin::Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>> {
         let url = ctx.props().status_base_url.clone();
         let link = ctx.link().clone();
@@ -154,15 +160,15 @@ impl LoadableComponent for ProxmoxNodeStatusPanel {
         Box::pin(async move {
             if let Some(url) = url {
                 match http_get(url.as_str(), None).await {
-                    Ok(res) => link.send_message(Msg::Loaded(Rc::new(res))),
-                    Err(err) => link.send_message(Msg::Error(err)),
+                    Ok(res) => link.send_custom_message(Msg::Loaded(Rc::new(res))),
+                    Err(err) => link.send_custom_message(Msg::Error(err)),
                 }
             }
             Ok(())
         })
     }
 
-    fn update(&mut self, ctx: &crate::LoadableComponentContext<Self>, msg: Self::Message) -> bool {
+    fn update(&mut self, ctx: &LoadableComponentContext<Self>, msg: Self::Message) -> bool {
         match msg {
             Msg::Error(err) => {
                 self.error = Some(err);
@@ -177,7 +183,6 @@ impl LoadableComponent for ProxmoxNodeStatusPanel {
                 self.change_power_state(ctx, command);
                 false
             }
-            Msg::Reload => true,
         }
     }
 
@@ -197,7 +202,7 @@ impl LoadableComponent for ProxmoxNodeStatusPanel {
         None
     }
 
-    fn main_view(&self, ctx: &crate::LoadableComponentContext<Self>) -> Html {
+    fn main_view(&self, ctx: &LoadableComponentContext<Self>) -> Html {
         let status = self
             .node_status
             .as_ref()
@@ -223,7 +228,7 @@ impl LoadableComponent for ProxmoxNodeStatusPanel {
                     .confirm_message(tr!("Are you sure you want to reboot the node?"))
                     .on_activate(
                         ctx.link()
-                            .callback(|_| Msg::RebootOrShutdown(NodePowerCommand::Reboot)),
+                            .custom_callback(|_| Msg::RebootOrShutdown(NodePowerCommand::Reboot)),
                     )
                     .icon_class("fa fa-undo"),
             );
@@ -232,7 +237,7 @@ impl LoadableComponent for ProxmoxNodeStatusPanel {
                     .confirm_message(tr!("Are you sure you want to shut down the node?"))
                     .on_activate(
                         ctx.link()
-                            .callback(|_| Msg::RebootOrShutdown(NodePowerCommand::Shutdown)),
+                            .custom_callback(|_| Msg::RebootOrShutdown(NodePowerCommand::Shutdown)),
                     )
                     .icon_class("fa fa-power-off"),
             );
diff --git a/src/notes_view.rs b/src/notes_view.rs
index 6b0a379..9c10c00 100644
--- a/src/notes_view.rs
+++ b/src/notes_view.rs
@@ -17,7 +17,7 @@ use proxmox_client::ApiResponseData;
 
 use crate::{
     ApiLoadCallback, EditWindow, LoadableComponent, LoadableComponentContext,
-    LoadableComponentMaster, Markdown,
+    LoadableComponentMaster, LoadableComponentScopeExt, LoadableComponentState, Markdown,
 };
 
 #[derive(Serialize, Deserialize, Clone, PartialEq)]
@@ -107,10 +107,13 @@ pub enum Msg {
 
 #[doc(hidden)]
 pub struct ProxmoxNotesView {
+    state: LoadableComponentState<ViewState>,
     data: NotesWithDigest,
     edit_window_loader: ApiLoadCallback<Value>,
 }
 
+crate::impl_deref_mut_property!(ProxmoxNotesView, state, LoadableComponentState<ViewState>);
+
 impl LoadableComponent for ProxmoxNotesView {
     type Properties = NotesView;
     type Message = Msg;
@@ -130,6 +133,7 @@ impl LoadableComponent for ProxmoxNotesView {
             }
         });
         Self {
+            state: LoadableComponentState::new(),
             data: NotesWithDigest {
                 notes: String::new(),
                 digest: None,
@@ -143,12 +147,12 @@ impl LoadableComponent for ProxmoxNotesView {
         ctx: &LoadableComponentContext<Self>,
     ) -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>> {
         let loader = ctx.props().loader.clone();
-        let link = ctx.link();
+        let link = ctx.link().clone();
         Box::pin(async move {
             let resp = loader.apply().await?;
             let notes = resp.data;
             let digest = resp.attribs.get("digest").cloned();
-            link.send_message(Msg::Load(NotesWithDigest { notes, digest }));
+            link.send_custom_message(Msg::Load(NotesWithDigest { notes, digest }));
             Ok(())
         })
     }
@@ -169,10 +173,8 @@ impl LoadableComponent for ProxmoxNotesView {
                 .class("pwt-overflow-hidden")
                 .class("pwt-border-bottom")
                 .with_child(
-                    Button::new(tr!("Edit")).on_activate(
-                        ctx.link()
-                            .change_view_callback(|_| Some(ViewState::EditNotes)),
-                    ),
+                    Button::new(tr!("Edit"))
+                        .on_activate(ctx.link().change_view_callback(|_| ViewState::EditNotes)),
                 )
                 .into(),
         )
diff --git a/src/object_grid.rs b/src/object_grid.rs
index a6313d0..e2321d0 100644
--- a/src/object_grid.rs
+++ b/src/object_grid.rs
@@ -8,7 +8,7 @@ use indexmap::IndexMap;
 use proxmox_client::ApiResponseData;
 use serde_json::Value;
 
-use yew::html::IntoPropValue;
+use yew::html::{IntoPropValue, Scope};
 use yew::prelude::*;
 use yew::virtual_dom::{Key, VComp, VNode};
 
@@ -20,8 +20,11 @@ use pwt::widget::form::FormContext;
 use pwt::widget::{Button, Toolbar};
 
 use crate::{ApiLoadCallback, IntoApiLoadCallback};
-use crate::{EditWindow, KVGrid, KVGridRow, LoadableComponentLink};
-use crate::{LoadableComponent, LoadableComponentContext, LoadableComponentMaster};
+use crate::{EditWindow, KVGrid, KVGridRow};
+use crate::{
+    LoadableComponent, LoadableComponentContext, LoadableComponentMaster,
+    LoadableComponentScopeExt, LoadableComponentState,
+};
 
 use pwt_macros::builder;
 
@@ -257,6 +260,8 @@ pub enum ViewState {
 
 #[doc(hidden)]
 pub struct PwtObjectGrid {
+    state: LoadableComponentState<ViewState>,
+
     selection: Option<Key>,
     data: Rc<Value>,
 
@@ -266,6 +271,8 @@ pub struct PwtObjectGrid {
     controller_observer: Option<SharedStateObserver<Vec<ObjectGridCommand>>>,
 }
 
+crate::impl_deref_mut_property!(PwtObjectGrid, state, LoadableComponentState<ViewState>);
+
 impl PwtObjectGrid {
     fn update_rows(&mut self, props: &ObjectGrid) {
         let mut rows = Vec::new();
@@ -280,7 +287,11 @@ impl PwtObjectGrid {
         self.rows = Rc::new(rows);
     }
 
-    fn update_controller(&mut self, props: &ObjectGrid, link: LoadableComponentLink<Self>) {
+    fn update_controller(
+        &mut self,
+        props: &ObjectGrid,
+        link: Scope<LoadableComponentMaster<Self>>,
+    ) {
         match &props.controller {
             None => self.controller_observer = None,
             Some(controller) => {
@@ -292,7 +303,7 @@ impl PwtObjectGrid {
                             guard.split_off(0)
                         };
                         for command in commands {
-                            link.send_message(Msg::ControllerCommand(command));
+                            link.send_custom_message(Msg::ControllerCommand(command));
                         }
                     },
                 ));
@@ -335,6 +346,7 @@ impl LoadableComponent for PwtObjectGrid {
         ctx.link().repeated_load(3000);
 
         let mut me = Self {
+            state: LoadableComponentState::new(),
             data: Rc::new(Value::Null),
             rows: Rc::new(Vec::new()),
             editors: IndexMap::new(),
@@ -342,7 +354,7 @@ impl LoadableComponent for PwtObjectGrid {
             controller_observer: None,
         };
         me.update_rows(props);
-        me.update_controller(props, ctx.link());
+        me.update_controller(props, ctx.link().clone());
         me
     }
 
@@ -352,12 +364,12 @@ impl LoadableComponent for PwtObjectGrid {
     ) -> Pin<Box<dyn Future<Output = Result<(), Error>>>> {
         let props = ctx.props();
         let loader = props.loader.clone();
-        let link = ctx.link();
+        let link = ctx.link().clone();
 
         Box::pin(async move {
             if let Some(loader) = &loader {
                 let api_resp: ApiResponseData<Value> = loader.apply().await?;
-                link.send_message(Msg::DataChange(api_resp));
+                link.send_custom_message(Msg::DataChange(api_resp));
             }
             Ok(())
         })
@@ -418,7 +430,7 @@ impl LoadableComponent for PwtObjectGrid {
         let mut toolbar = Toolbar::new()
             .border_bottom(true)
             .with_child(Button::new("Edit").disabled(disable_edit).onclick({
-                let link = ctx.link();
+                let link = ctx.link().clone();
                 move |_| {
                     link.change_view(Some(ViewState::EditObject));
                 }
@@ -437,11 +449,11 @@ impl LoadableComponent for PwtObjectGrid {
             .class("pwt-flex-fit")
             .rows(Rc::clone(&self.rows))
             .data(self.data.clone())
-            .on_select(ctx.link().callback(Msg::Select))
+            .on_select(ctx.link().custom_callback(Msg::Select))
             .on_row_dblclick({
                 let link = ctx.link().clone();
                 move |event: &mut DataTableMouseEvent| {
-                    link.send_message(Msg::Select(Some(event.record_key.clone())));
+                    link.send_custom_message(Msg::Select(Some(event.record_key.clone())));
                     link.change_view(Some(ViewState::EditObject));
                 }
             })
@@ -449,7 +461,7 @@ impl LoadableComponent for PwtObjectGrid {
                 let link = ctx.link().clone();
                 move |event: &mut DataTableKeyboardEvent| {
                     if event.key() == " " {
-                        link.send_message(Msg::Select(Some(event.record_key.clone())));
+                        link.send_custom_message(Msg::Select(Some(event.record_key.clone())));
                         link.change_view(Some(ViewState::EditObject));
                     }
                 }
diff --git a/src/permission_panel.rs b/src/permission_panel.rs
index 3ed07b1..e7d2c27 100644
--- a/src/permission_panel.rs
+++ b/src/permission_panel.rs
@@ -18,7 +18,7 @@ use pwt::widget::data_table::{
 
 use pwt_macros::builder;
 
-use crate::{http_get, LoadableComponent, LoadableComponentMaster};
+use crate::{http_get, LoadableComponent, LoadableComponentMaster, LoadableComponentState};
 
 #[derive(Clone, PartialEq, Properties)]
 #[builder]
@@ -46,10 +46,13 @@ impl PermissionPanel {
     }
 }
 pub struct ProxmoxPermissionPanel {
+    state: LoadableComponentState<()>,
     store: TreeStore<PermissionInfo>,
     columns: Rc<Vec<DataTableHeader<PermissionInfo>>>,
 }
 
+crate::impl_deref_mut_property!(ProxmoxPermissionPanel, state, LoadableComponentState<()>);
+
 #[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
 enum PermissionInfo {
     Permission(String, String, bool),
@@ -113,15 +116,19 @@ impl LoadableComponent for ProxmoxPermissionPanel {
     type Message = ();
     type ViewState = ();
 
-    fn create(_ctx: &crate::LoadableComponentContext<Self>) -> Self {
+    fn create(_ctx: &Context<LoadableComponentMaster<Self>>) -> Self {
         let store = TreeStore::new();
         let columns = Rc::new(columns(&store));
-        Self { store, columns }
+        Self {
+            state: LoadableComponentState::new(),
+            store,
+            columns,
+        }
     }
 
     fn load(
         &self,
-        ctx: &crate::LoadableComponentContext<Self>,
+        ctx: &Context<LoadableComponentMaster<Self>>,
     ) -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>> {
         let props = ctx.props();
         let base_url = props.base_url.clone();
@@ -151,7 +158,7 @@ impl LoadableComponent for ProxmoxPermissionPanel {
         })
     }
 
-    fn main_view(&self, _ctx: &crate::LoadableComponentContext<Self>) -> Html {
+    fn main_view(&self, _ctx: &Context<LoadableComponentMaster<Self>>) -> Html {
         DataTable::new(Rc::clone(&self.columns), self.store.clone())
             .class("pwt-flex-fit")
             .into()
diff --git a/src/subscription_panel.rs b/src/subscription_panel.rs
index 9f8e65e..1b6a8e4 100644
--- a/src/subscription_panel.rs
+++ b/src/subscription_panel.rs
@@ -12,8 +12,13 @@ use pwt::widget::form::{Field, FormContext};
 use pwt::widget::{Button, Container, InputPanel, Toolbar};
 
 use crate::utils::render_epoch;
-use crate::{ConfirmButton, DataViewWindow, EditWindow, KVGrid, KVGridRow, ProjectInfo};
-use crate::{LoadableComponent, LoadableComponentContext, LoadableComponentMaster};
+use crate::{
+    ConfirmButton, DataViewWindow, EditWindow, KVGrid, KVGridRow, LoadableComponentState,
+    ProjectInfo,
+};
+use crate::{
+    LoadableComponent, LoadableComponentContext, LoadableComponentMaster, LoadableComponentScopeExt,
+};
 
 #[derive(Properties, PartialEq, Clone)]
 pub struct SubscriptionPanel {
@@ -38,20 +43,26 @@ pub enum ViewState {
     SystemReport,
 }
 
-pub enum Msg {}
-
 pub struct ProxmoxSubscriptionPanel {
+    state: LoadableComponentState<ViewState>,
     rows: Rc<Vec<KVGridRow>>,
     data: Rc<RefCell<Rc<Value>>>,
 }
 
+crate::impl_deref_mut_property!(
+    ProxmoxSubscriptionPanel,
+    state,
+    LoadableComponentState<ViewState>
+);
+
 impl LoadableComponent for ProxmoxSubscriptionPanel {
-    type Message = Msg;
+    type Message = ();
     type Properties = SubscriptionPanel;
     type ViewState = ViewState;
 
     fn create(_ctx: &LoadableComponentContext<Self>) -> Self {
         Self {
+            state: LoadableComponentState::new(),
             rows: Rc::new(rows()),
             data: Rc::new(RefCell::new(Rc::new(Value::Null))),
         }
@@ -59,7 +70,7 @@ impl LoadableComponent for ProxmoxSubscriptionPanel {
 
     fn load(
         &self,
-        ctx: &crate::LoadableComponentContext<Self>,
+        ctx: &Context<LoadableComponentMaster<Self>>,
     ) -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>> {
         let data = self.data.clone();
         let base_url = ctx.props().base_url.to_string();
@@ -70,7 +81,7 @@ impl LoadableComponent for ProxmoxSubscriptionPanel {
         })
     }
 
-    fn toolbar(&self, ctx: &LoadableComponentContext<Self>) -> Option<Html> {
+    fn toolbar(&self, ctx: &Context<LoadableComponentMaster<Self>>) -> Option<Html> {
         let toolbar = Toolbar::new()
             .class("pwt-overflow-hidden")
             .with_child(
@@ -85,7 +96,7 @@ impl LoadableComponent for ProxmoxSubscriptionPanel {
                 Button::new(tr!("Check"))
                     .icon_class("fa fa-check-square-o")
                     .onclick({
-                        let link = ctx.link();
+                        let link = ctx.link().clone();
                         let base_url = ctx.props().base_url.to_string();
                         move |_| {
                             link.spawn({
@@ -112,7 +123,7 @@ impl LoadableComponent for ProxmoxSubscriptionPanel {
                         html! {tr!("Are you sure you want to remove the subscription key?")},
                     )
                     .on_activate({
-                        let link = ctx.link();
+                        let link = ctx.link().clone();
                         let base_url = ctx.props().base_url.to_string();
                         move |_| {
                             link.spawn({
@@ -141,8 +152,8 @@ impl LoadableComponent for ProxmoxSubscriptionPanel {
             )
             .with_flex_spacer()
             .with_child({
-                let loading = ctx.loading();
-                let link = ctx.link();
+                let loading = self.loading();
+                let link = ctx.link().clone();
                 Button::refresh(loading).onclick(move |_| link.send_reload())
             });
 
diff --git a/src/tasks.rs b/src/tasks.rs
index d3e814f..6e1118e 100644
--- a/src/tasks.rs
+++ b/src/tasks.rs
@@ -26,7 +26,10 @@ use pbs_api_types::TaskListItem;
 
 use pwt_macros::builder;
 
-use crate::{LoadableComponent, LoadableComponentContext, LoadableComponentMaster, TaskViewer};
+use crate::{
+    LoadableComponent, LoadableComponentContext, LoadableComponentMaster,
+    LoadableComponentScopeExt, LoadableComponentState, TaskViewer,
+};
 
 use super::{TaskStatusSelector, TaskTypeSelector};
 
@@ -98,7 +101,6 @@ pub enum ViewDialog {
 }
 
 pub enum Msg {
-    Redraw,
     ToggleFilter,
     LoadBatch(bool), // fresh load
     LoadFinished,
@@ -106,6 +108,7 @@ pub enum Msg {
     ShowTask,
 }
 pub struct ProxmoxTasks {
+    state: LoadableComponentState<ViewDialog>,
     selection: Selection,
     store: Store<TaskListItem>,
     show_filter: PersistentState<bool>,
@@ -117,6 +120,8 @@ pub struct ProxmoxTasks {
     columns: Rc<Vec<DataTableHeader<TaskListItem>>>,
 }
 
+crate::impl_deref_mut_property!(ProxmoxTasks, state, LoadableComponentState<ViewDialog>);
+
 impl ProxmoxTasks {
     fn columns(ctx: &LoadableComponentContext<Self>) -> Rc<Vec<DataTableHeader<TaskListItem>>> {
         if let Some(columns) = ctx.props().columns.clone() {
@@ -169,18 +174,21 @@ impl LoadableComponent for ProxmoxTasks {
 
     fn create(ctx: &LoadableComponentContext<Self>) -> Self {
         let link = ctx.link();
-        let selection = Selection::new().on_select(link.callback(|_| Msg::Redraw));
+        let selection = Selection::new().on_select({
+            let link = ctx.link().clone();
+            move |_| link.send_redraw()
+        });
         let store = Store::with_extract_key(|item: &TaskListItem| Key::from(item.upid.clone()));
 
         let filter_form_context =
-            FormContext::new().on_change(ctx.link().callback(|_| Msg::UpdateFilter));
+            FormContext::new().on_change(ctx.link().custom_callback(|_| Msg::UpdateFilter));
 
         let row_render_callback = DataTableRowRenderCallback::new({
             let store = store.clone();
             let link = link.clone();
             move |args: &mut _| {
                 if args.row_index() > store.data_len().saturating_sub(LOAD_BUFFER_ROWS) {
-                    link.send_message(Msg::LoadBatch(false));
+                    link.send_custom_message(Msg::LoadBatch(false));
                 }
                 let record: &TaskListItem = args.record();
                 match record.status.as_deref() {
@@ -194,6 +202,7 @@ impl LoadableComponent for ProxmoxTasks {
         });
 
         Self {
+            state: LoadableComponentState::new(),
             selection,
             store,
             show_filter: PersistentState::new("ProxmoxTasksShowFilter"),
@@ -286,14 +295,13 @@ impl LoadableComponent for ProxmoxTasks {
                 }
                 store.append(&mut data);
             }
-            link.send_message(Msg::LoadFinished);
+            link.send_custom_message(Msg::LoadFinished);
             Ok(())
         })
     }
 
     fn update(&mut self, ctx: &LoadableComponentContext<Self>, msg: Self::Message) -> bool {
         match msg {
-            Msg::Redraw => true,
             Msg::ToggleFilter => {
                 self.show_filter.update(!*self.show_filter);
                 true
@@ -304,7 +312,7 @@ impl LoadableComponent for ProxmoxTasks {
                     return false;
                 }
                 let filter_params = form_context.get_submit_data();
-                if ctx.loading() && self.last_filter == filter_params {
+                if self.loading() && self.last_filter == filter_params {
                     return false;
                 }
 
@@ -369,7 +377,7 @@ impl LoadableComponent for ProxmoxTasks {
             .with_child(
                 Button::new(tr!("View"))
                     .disabled(disabled)
-                    .onclick(ctx.link().callback(|_| Msg::ShowTask)),
+                    .onclick(ctx.link().custom_callback(|_| Msg::ShowTask)),
             )
             .with_flex_spacer()
             .with_child({
@@ -381,12 +389,13 @@ impl LoadableComponent for ProxmoxTasks {
             .with_child(
                 Button::new("Filter")
                     .icon_class(filter_icon_class)
-                    .onclick(ctx.link().callback(|_| Msg::ToggleFilter)),
+                    .onclick(ctx.link().custom_callback(|_| Msg::ToggleFilter)),
             )
             .with_child({
-                let loading = ctx.loading();
-                let link = ctx.link();
-                Button::refresh(loading).onclick(move |_| link.send_message(Msg::LoadBatch(true)))
+                let loading = self.loading();
+                let link = ctx.link().clone();
+                Button::refresh(loading)
+                    .onclick(move |_| link.send_custom_message(Msg::LoadBatch(true)))
             });
 
         let filter_classes = classes!(
@@ -441,13 +450,13 @@ impl LoadableComponent for ProxmoxTasks {
 
     fn main_view(&self, ctx: &LoadableComponentContext<Self>) -> Html {
         let columns = self.columns.clone();
-        let link = ctx.link();
+        let link = ctx.link().clone();
 
         DataTable::new(columns, self.store.clone())
             .class("pwt-flex-fit")
             .selection(self.selection.clone())
             .on_row_dblclick(move |_: &mut _| {
-                link.send_message(Msg::ShowTask);
+                link.send_custom_message(Msg::ShowTask);
             })
             .row_render_callback(self.row_render_callback.clone())
             .into()
diff --git a/src/tfa/tfa_view.rs b/src/tfa/tfa_view.rs
index c4c1a43..540c98b 100644
--- a/src/tfa/tfa_view.rs
+++ b/src/tfa/tfa_view.rs
@@ -16,7 +16,10 @@ use pwt::widget::{Button, Mask, Toolbar};
 
 use pwt_macros::builder;
 
-use crate::{LoadableComponent, LoadableComponentContext, LoadableComponentMaster};
+use crate::{
+    impl_deref_mut_property, LoadableComponent, LoadableComponentContext, LoadableComponentMaster,
+    LoadableComponentScopeExt, LoadableComponentState,
+};
 
 use proxmox_tfa::{TfaType, TfaUser};
 
@@ -87,7 +90,6 @@ impl TfaView {
 }
 
 pub enum Msg {
-    Redraw,
     Edit,
     Remove(Option<String>),
     RemoveResult(Result<(), Error>),
@@ -104,11 +106,14 @@ pub enum ViewState {
 
 #[doc(hidden)]
 pub struct ProxmoxTfaView {
+    state: LoadableComponentState<ViewState>,
     selection: Selection,
     store: Store<TfaEntry>,
     removing: bool,
 }
 
+impl_deref_mut_property!(ProxmoxTfaView, state, LoadableComponentState<ViewState>);
+
 impl ProxmoxTfaView {
     fn get_selected_record(&self) -> Option<TfaEntry> {
         let selected_key = self.selection.selected_key();
@@ -127,8 +132,12 @@ impl LoadableComponent for ProxmoxTfaView {
 
     fn create(ctx: &LoadableComponentContext<Self>) -> Self {
         let store = Store::new();
-        let selection = Selection::new().on_select(ctx.link().callback(|_| Msg::Redraw));
+        let selection = Selection::new().on_select({
+            let link = ctx.link().clone();
+            move |_| link.send_redraw()
+        });
         Self {
+            state: LoadableComponentState::new(),
             store,
             selection,
             removing: false,
@@ -177,7 +186,6 @@ impl LoadableComponent for ProxmoxTfaView {
     fn update(&mut self, ctx: &LoadableComponentContext<Self>, msg: Self::Message) -> bool {
         let props = ctx.props();
         match msg {
-            Msg::Redraw => true,
             Msg::Edit => {
                 let info = match self.get_selected_record() {
                     Some(info) => info,
@@ -204,8 +212,8 @@ impl LoadableComponent for ProxmoxTfaView {
                 // fixme: ask use if he really wants to remove
                 let link = ctx.link().clone();
                 let base_url = props.base_url.clone();
-                link.send_future(async move {
-                    Msg::RemoveResult(
+                self.spawn(async move {
+                    link.send_custom_message(Msg::RemoveResult(
                         delete_item(
                             base_url,
                             info.user_id.clone(),
@@ -213,7 +221,7 @@ impl LoadableComponent for ProxmoxTfaView {
                             password,
                         )
                         .await,
-                    )
+                    ))
                 });
 
                 false
@@ -273,7 +281,7 @@ impl LoadableComponent for ProxmoxTfaView {
             .with_child(
                 Button::new(tr!("Edit"))
                     .disabled(edit_disabled)
-                    .onclick(ctx.link().callback(|_| Msg::Edit)),
+                    .onclick(ctx.link().custom_callback(|_| Msg::Edit)),
             )
             .with_child(
                 Button::new(tr!("Remove"))
@@ -282,8 +290,8 @@ impl LoadableComponent for ProxmoxTfaView {
             )
             .with_flex_spacer()
             .with_child({
-                let loading = ctx.loading();
-                let link = ctx.link();
+                let loading = self.loading();
+                let link = ctx.link().clone();
                 Button::refresh(loading).onclick(move |_| link.send_reload())
             });
 
@@ -296,8 +304,8 @@ impl LoadableComponent for ProxmoxTfaView {
             .selection(self.selection.clone())
             .class("pwt-flex-fit")
             .on_row_dblclick({
-                let link = ctx.link();
-                move |_: &mut _| link.send_message(Msg::Edit)
+                let link = ctx.link().clone();
+                move |_: &mut _| link.send_custom_message(Msg::Edit)
             });
         Mask::new(view).visible(self.removing).into()
     }
@@ -314,8 +322,8 @@ impl LoadableComponent for ProxmoxTfaView {
                 TfaConfirmRemove::new(info)
                     .on_close(ctx.link().change_view_callback(|_| None))
                     .on_confirm({
-                        let link = ctx.link();
-                        move |password| link.send_message(Msg::Remove(password))
+                        let link = ctx.link().clone();
+                        move |password| link.send_custom_message(Msg::Remove(password))
                     })
                     .into()
             }),
diff --git a/src/token_panel.rs b/src/token_panel.rs
index 031d54f..66c25db 100644
--- a/src/token_panel.rs
+++ b/src/token_panel.rs
@@ -25,7 +25,8 @@ use crate::utils::{
 };
 use crate::{
     AuthidSelector, ConfirmButton, EditWindow, LoadableComponent, LoadableComponentContext,
-    LoadableComponentLink, LoadableComponentMaster, PermissionPanel,
+    LoadableComponentMaster, LoadableComponentScope, LoadableComponentScopeExt,
+    LoadableComponentState, PermissionPanel,
 };
 
 async fn load_api_tokens() -> Result<Vec<ApiToken>, Error> {
@@ -37,7 +38,7 @@ async fn load_api_tokens() -> Result<Vec<ApiToken>, Error> {
 
 async fn create_token(
     form_ctx: FormContext,
-    link: LoadableComponentLink<ProxmoxTokenView>,
+    link: LoadableComponentScope<ProxmoxTokenView>,
 ) -> Result<(), Error> {
     let mut data = form_ctx.get_submit_data();
 
@@ -118,17 +119,19 @@ enum ViewState {
 }
 
 enum Msg {
-    Refresh,
     Remove,
     Regenerate,
 }
 
 struct ProxmoxTokenView {
+    state: LoadableComponentState<ViewState>,
     selection: Selection,
     store: Store<ApiToken>,
     columns: Rc<Vec<DataTableHeader<ApiToken>>>,
 }
 
+crate::impl_deref_mut_property!(ProxmoxTokenView, state, LoadableComponentState<ViewState>);
+
 fn token_api_url(user: &str, tokenname: &str) -> String {
     format!(
         "/access/users/{}/token/{}",
@@ -146,11 +149,15 @@ impl LoadableComponent for ProxmoxTokenView {
         let link = ctx.link();
         link.repeated_load(5000);
 
-        let selection = Selection::new().on_select(link.callback(|_| Msg::Refresh));
+        let selection = Selection::new().on_select({
+            let link = ctx.link().clone();
+            move |_| link.send_redraw()
+        });
         let store =
             Store::with_extract_key(|record: &ApiToken| Key::from(record.tokenid.to_string()));
 
         Self {
+            state: LoadableComponentState::new(),
             selection,
             store,
             columns: columns(),
@@ -193,7 +200,7 @@ impl LoadableComponent for ProxmoxTokenView {
                     .confirm_message(tr!("Are you sure you want to remove the API token? \
                         All existing users of the token will lose access!"))
                     .disabled(disabled)
-                    .on_activate(link.callback(|_| Msg::Remove)),
+                    .on_activate(link.custom_callback(|_| Msg::Remove)),
             )
             .with_spacer()
             .with_child(
@@ -203,7 +210,7 @@ impl LoadableComponent for ProxmoxTokenView {
                         All existing users of the token will lose access!"
                     ))
                     .disabled(disabled)
-                    .on_activate(link.callback(|_| Msg::Regenerate)),
+                    .on_activate(link.custom_callback(|_| Msg::Regenerate)),
             )
             .with_spacer()
             .with_child(
@@ -217,7 +224,6 @@ impl LoadableComponent for ProxmoxTokenView {
 
     fn update(&mut self, ctx: &LoadableComponentContext<Self>, msg: Self::Message) -> bool {
         match msg {
-            Msg::Refresh => true,
             Msg::Remove => {
                 let Some(record) = self.store.selected_record(&self.selection) else {
                     return false;
@@ -230,7 +236,7 @@ impl LoadableComponent for ProxmoxTokenView {
                 };
 
                 let url = token_api_url(&user, tokenname.as_str());
-                let link = ctx.link();
+                let link = ctx.link().clone();
                 link.clone().spawn(async move {
                     match crate::http_delete(url, None).await {
                         Ok(()) => {
@@ -271,7 +277,7 @@ impl LoadableComponent for ProxmoxTokenView {
     }
 
     fn main_view(&self, ctx: &LoadableComponentContext<Self>) -> Html {
-        let link = ctx.link();
+        let link = ctx.link().clone();
 
         DataTable::new(self.columns.clone(), self.store.clone())
             .class("pwt-flex-fit")
diff --git a/src/user_panel.rs b/src/user_panel.rs
index d364df8..cbc6b49 100644
--- a/src/user_panel.rs
+++ b/src/user_panel.rs
@@ -23,8 +23,9 @@ use crate::form::delete_empty_values;
 use crate::percent_encoding::percent_encode_component;
 use crate::utils::{epoch_to_input_value, render_epoch_short};
 use crate::{
-    EditWindow, LoadableComponent, LoadableComponentContext, LoadableComponentMaster,
-    PermissionPanel, RealmSelector, SchemaValidation,
+    impl_deref_mut_property, EditWindow, LoadableComponent, LoadableComponentContext,
+    LoadableComponentMaster, LoadableComponentScopeExt, LoadableComponentState, PermissionPanel,
+    RealmSelector, SchemaValidation,
 };
 
 async fn load_user_list() -> Result<Vec<UserWithTokens>, Error> {
@@ -111,15 +112,17 @@ pub enum ViewState {
 }
 
 pub enum Msg {
-    SelectionChange,
     RemoveItem,
 }
 
 pub struct ProxmoxUserPanel {
+    state: LoadableComponentState<ViewState>,
     store: Store<UserWithTokens>,
     selection: Selection,
 }
 
+impl_deref_mut_property!(ProxmoxUserPanel, state, LoadableComponentState<ViewState>);
+
 impl LoadableComponent for ProxmoxUserPanel {
     type Message = Msg;
     type Properties = UserPanel;
@@ -142,17 +145,23 @@ impl LoadableComponent for ProxmoxUserPanel {
             Key::from(record.user.userid.as_str())
         });
 
-        let selection = Selection::new().on_select(ctx.link().callback(|_| Msg::SelectionChange));
+        let selection = Selection::new().on_select({
+            let link = ctx.link().clone();
+            move |_| link.send_redraw()
+        });
 
-        Self { store, selection }
+        Self {
+            state: LoadableComponentState::new(),
+            store,
+            selection,
+        }
     }
 
     fn update(&mut self, ctx: &LoadableComponentContext<Self>, msg: Self::Message) -> bool {
         match msg {
-            Msg::SelectionChange => true,
             Msg::RemoveItem => {
                 if let Some(key) = self.selection.selected_key() {
-                    let link = ctx.link();
+                    let link = ctx.link().clone();
                     link.clone().spawn(async move {
                         if let Err(err) = delete_user(key).await {
                             link.show_error(tr!("Unable to delete user"), err, true);
@@ -191,7 +200,7 @@ impl LoadableComponent for ProxmoxUserPanel {
             .with_child(
                 Button::new(tr!("Remove"))
                     .disabled(no_selection)
-                    .onclick(link.callback(|_| Msg::RemoveItem)),
+                    .onclick(link.custom_callback(|_| Msg::RemoveItem)),
             )
             .with_spacer()
             .with_child(
@@ -206,8 +215,8 @@ impl LoadableComponent for ProxmoxUserPanel {
             )
             .with_flex_spacer()
             .with_child({
-                let loading = ctx.loading();
-                let link = ctx.link();
+                let loading = self.loading();
+                let link = ctx.link().clone();
                 Button::refresh(loading).onclick(move |_| link.send_reload())
             });
 
@@ -215,7 +224,7 @@ impl LoadableComponent for ProxmoxUserPanel {
     }
 
     fn main_view(&self, ctx: &LoadableComponentContext<Self>) -> Html {
-        let link = ctx.link();
+        let link = ctx.link().clone();
         DataTable::new(columns(), self.store.clone())
             .class("pwt-flex-fill pwt-overflow-auto")
             .selection(self.selection.clone())
-- 
2.47.3


_______________________________________________
yew-devel mailing list
yew-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/yew-devel


^ permalink raw reply	[flat|nested] 5+ messages in thread

end of thread, other threads:[~2025-12-10  9:32 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-12-09 13:11 [yew-devel] [RFC yew-comp] refactor: move LoadableComponent state into component implementations Dietmar Maurer
2025-12-09 18:31 ` Thomas Lamprecht
2025-12-10  7:53 ` Thomas Lamprecht
2025-12-10  9:23   ` Dietmar Maurer
2025-12-10  9:32     ` Thomas Lamprecht

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal