From a4e0139fd6b4546d1d6f688d0218eea5d453e94a Mon Sep 17 00:00:00 2001 From: marc2332 Date: Sun, 18 Aug 2024 13:39:28 +0200 Subject: [PATCH 1/6] blog: WIP Custom state hook post --- website/src/content/blog/custom-state-hook.md | 139 ++++++++++++++++++ website/src/pages/blog.astro | 2 + 2 files changed, 141 insertions(+) create mode 100644 website/src/content/blog/custom-state-hook.md diff --git a/website/src/content/blog/custom-state-hook.md b/website/src/content/blog/custom-state-hook.md new file mode 100644 index 000000000..508d2c9b5 --- /dev/null +++ b/website/src/content/blog/custom-state-hook.md @@ -0,0 +1,139 @@ +--- +title: 'Custom State management' +date: 2024-08-08 +description: 'Building a custom state management solution for Dioxus.' +author: 'marc2332' +layout: ../../layouts/BlogPostLayout.astro +slug: "custom-state-management" +--- + +In this post I will teach you how hooks work and how to make make +a reactive state lib from scratch. + +### Why? + +You may ask yourself why do we even need third-party libraries for state management +and the truth is that you might not need them. Dioxus comes with a Signals +a very basic and simple to use state management solution that while +it works great, it might not scale for more complex apps. + +### What? + +So a bit of spoiler here, our library will consist of a hook that let use subscribe +and write to a key-value store. We will call it `use_value`. + +```rs +fn CoolComponent() { + let mut count = use_value("counter"); + + rsx!( + Button { + onclick: |_| { + count += 1; + } + } + label { + "{count}" + } + ) +} + +fn app() { + let same_count = use_value("counter"); + + rsx!( + label { + "{same_count}" + } + ) +} + +``` + +### The basics + +#### `use_hook` + +All hooks in Dioxus are built on top of a low level core hook called `use_hook`. +This one let us store a value that tied to life of the **Scope** it was created in, +which means that when the **Component** is dropped, our stored value will be dropped as well. + +Every time the component where the store value is created is re-run it will give is access to the value we store, +but for this there is a catch, the stored value must be `Clone` so we can get a hold of it +on every render of the component. + +But if the value is cloned it means that any change that we make to the cloned value will not be +persisted for the next change? Worry no more, we got smart pointers. + +Here there is an example, even though our component will run as many times as it needs it will always hold the same value +it was created with, the `Default` of `T`. Because when the component runs what is cloned is the `Rc` and not `T`. +```rs +fn use_value() -> Rc> { + use_hook(|| { + Rc::new(RefCell::new(T::default())) + }) +} +``` + +### The not so basic +Alright we got a dummy hook that all it does is store a value, but how do we share this value with an assigned key? +We just need to build a registry. + + + +```rs +struct RegistryEntry { + value: Signal, + subscribers: HashSet +} + +struct Registry { + map: HashMap> +} + +impl Registry { + /// Subscribe the given [Scope] in this registry to `key`. + /// Will initialize the value to `T::default` if there wasn't one before. + pub fn subscribe(&mut self, key: String, scope_id: ScopeId) -> { + self.map.insert_or_get(key, || RegistryEntry { + value: Signal::new(T::default()), + subscribers: HashSet::from([scope_id]) + }) + .subscribers.insert(scope_id) + } + + /// Unsubscriber the given [ScopeId] from this registry `key`. + pub fn unsubscribe(&mut self, key: &str, scope_id: ScopeId) { + let registry_entry = self.map.get_mut(key).unwrap(); + registry_entry.subscribers.remove(scope_id); + + // Remove the registry entry if there are no more subscribers left + if registry_entry.subscribers.is_empty() { + self.map.remove(key); + } + } + + /// Get the [Signal] assigned to the given `key`. + pub fn get(&self, key: &str) -> Signal { + self.map.get(key).copied().unwrap() + } +} + +fn use_value(key: &str) -> Signal { + use_hook(|| { + // Access the registry and if it doesn't exist create it in the root scope + let mut registry = consume_context::>>().unwrap_or_else(|| { + provide_root_context(Signal::new_in_scope(Registry { + map: HashMap::default() + }, ScopeId::ROOT)) + }); + + // Subscribe this component + registry.subscribe(current_scope_id().unwrap()); + + // Return the Signal assigned to the given key + registry.get(key) + }) +} +``` + diff --git a/website/src/pages/blog.astro b/website/src/pages/blog.astro index 374216eca..651550379 100644 --- a/website/src/pages/blog.astro +++ b/website/src/pages/blog.astro @@ -1,6 +1,8 @@ --- const posts = await Astro.glob('../content/blog/*.md'); import Layout from '../layouts/Layout.astro'; + +posts.sort((a, b) => new Date(b.frontmatter.date).getTime() - new Date(a.frontmatter.date).getTime()) --- From 5a3a32fd7112241c1727a5b33f9eab44576c4efa Mon Sep 17 00:00:00 2001 From: marc2332 Date: Sun, 18 Aug 2024 13:54:11 +0200 Subject: [PATCH 2/6] some updates --- ...d => building-custom-state-hook-dioxus.md} | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) rename website/src/content/blog/{custom-state-hook.md => building-custom-state-hook-dioxus.md} (69%) diff --git a/website/src/content/blog/custom-state-hook.md b/website/src/content/blog/building-custom-state-hook-dioxus.md similarity index 69% rename from website/src/content/blog/custom-state-hook.md rename to website/src/content/blog/building-custom-state-hook-dioxus.md index 508d2c9b5..6af142781 100644 --- a/website/src/content/blog/custom-state-hook.md +++ b/website/src/content/blog/building-custom-state-hook-dioxus.md @@ -1,29 +1,37 @@ --- -title: 'Custom State management' -date: 2024-08-08 +title: 'Building a custom state Hook for Dioxus' +date: 2024-08-18 description: 'Building a custom state management solution for Dioxus.' author: 'marc2332' layout: ../../layouts/BlogPostLayout.astro -slug: "custom-state-management" +slug: "building-custom-state-hook-dioxus" --- -In this post I will teach you how hooks work and how to make make -a reactive state lib from scratch. +As nobody has done this before, I think it would be interesting to people interested in Dioxus, to learn +how are hooks made. -### Why? +### ๐Ÿ‘‹ Preface -You may ask yourself why do we even need third-party libraries for state management -and the truth is that you might not need them. Dioxus comes with a Signals -a very basic and simple to use state management solution that while -it works great, it might not scale for more complex apps. +**Dioxus** already comes with a few state management solutions, primarily in the form of hooks and powered by **Signals**. +I am not gonna deep into what hooks or signals are, so if you don't know what they are you may check the official docs first. -### What? +You may be asking yourself why do we even need to build custom hooks if Dioxus already give us different tools for different use cases. +And it actually just depends on your needs, for the majority of the cases the official tools will work just fine. -So a bit of spoiler here, our library will consist of a hook that let use subscribe -and write to a key-value store. We will call it `use_value`. +Now, there are certain cases where it's just not a good fit and you might see yourself wanting a certain reactive behavior or just a different API. + +For those who happen to be in this situation, as I was myself when building [Valin](https://github.com/marc2332/valin/), worry no more, I got you. + +### ๐Ÿค Introduction + +So, a bit of introduction. We will build a custom hook that give us a reactive `key-value` store, +where we can subscribe to the value of specific keys across multiple components. The hook will be called `use_value` + +This is how it will look like: ```rs fn CoolComponent() { + // Subscribe to any changes to the value identified by the key `counter`. let mut count = use_value("counter"); rsx!( @@ -39,6 +47,7 @@ fn CoolComponent() { } fn app() { + // Just like above, subscribe to any changes to the value identified by the key `counter`. let same_count = use_value("counter"); rsx!( @@ -50,7 +59,7 @@ fn app() { ``` -### The basics +### ๐Ÿงต The basics #### `use_hook` From 2b1775ac2ef85a86e3c321c77cbdee7834051535 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Sun, 18 Aug 2024 14:03:19 +0200 Subject: [PATCH 3/6] style improvements --- website/src/layouts/BlogPostLayout.astro | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/website/src/layouts/BlogPostLayout.astro b/website/src/layouts/BlogPostLayout.astro index 6a08b67d3..0acd4df4e 100644 --- a/website/src/layouts/BlogPostLayout.astro +++ b/website/src/layouts/BlogPostLayout.astro @@ -42,7 +42,13 @@ const formattedDate = (new Date(frontmatter.date)).toLocaleString().split(',')[0 } main * :not(span, pre, code) { - color: rgb(235, 235, 235); + color: #d2d2d2; + font-family: "Inter", system-ui, sans-serif; + line-height: 1.7; + } + + main *:is(h1, h2, h3, h4, h5, h6) { + color: white; font-family: "Inter", system-ui, sans-serif; line-height: 1.7; } From 42275a0edd10499bea5b2f13df390ed8d3086567 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Sun, 18 Aug 2024 14:03:32 +0200 Subject: [PATCH 4/6] typo --- website/src/content/blog/building-custom-state-hook-dioxus.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/content/blog/building-custom-state-hook-dioxus.md b/website/src/content/blog/building-custom-state-hook-dioxus.md index 6af142781..ede1b6a30 100644 --- a/website/src/content/blog/building-custom-state-hook-dioxus.md +++ b/website/src/content/blog/building-custom-state-hook-dioxus.md @@ -13,7 +13,7 @@ how are hooks made. ### ๐Ÿ‘‹ Preface **Dioxus** already comes with a few state management solutions, primarily in the form of hooks and powered by **Signals**. -I am not gonna deep into what hooks or signals are, so if you don't know what they are you may check the official docs first. +I am not gonna five into what hooks or signals are, so if you don't know what they are you may check the official docs first. You may be asking yourself why do we even need to build custom hooks if Dioxus already give us different tools for different use cases. And it actually just depends on your needs, for the majority of the cases the official tools will work just fine. From edfeb1c6bd6fac6b9f953f6f0873d025765f9dd6 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Sun, 18 Aug 2024 23:19:40 +0200 Subject: [PATCH 5/6] improvements --- .../blog/building-custom-state-hook-dioxus.md | 16 +++++++--------- website/src/layouts/BlogPostLayout.astro | 15 ++++++++++++--- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/website/src/content/blog/building-custom-state-hook-dioxus.md b/website/src/content/blog/building-custom-state-hook-dioxus.md index ede1b6a30..85269a804 100644 --- a/website/src/content/blog/building-custom-state-hook-dioxus.md +++ b/website/src/content/blog/building-custom-state-hook-dioxus.md @@ -13,7 +13,7 @@ how are hooks made. ### ๐Ÿ‘‹ Preface **Dioxus** already comes with a few state management solutions, primarily in the form of hooks and powered by **Signals**. -I am not gonna five into what hooks or signals are, so if you don't know what they are you may check the official docs first. +I am not gonna dive into what hooks or signals are, so if you don't know what they are you may check the official docs first. You may be asking yourself why do we even need to build custom hooks if Dioxus already give us different tools for different use cases. And it actually just depends on your needs, for the majority of the cases the official tools will work just fine. @@ -63,19 +63,17 @@ fn app() { #### `use_hook` -All hooks in Dioxus are built on top of a low level core hook called `use_hook`. -This one let us store a value that tied to life of the **Scope** it was created in, -which means that when the **Component** is dropped, our stored value will be dropped as well. +`use_hook` is the foundational core hook in which all hooks are built on top of. +It let us store value that is tied to life of the **Scope** it was created in, +which means that when the **Component** is dropped, the stored value will be dropped as well. -Every time the component where the store value is created is re-run it will give is access to the value we store, -but for this there is a catch, the stored value must be `Clone` so we can get a hold of it -on every render of the component. +It takes a closure to initialize our value, this be called when the component runs for the first time, this way the value is only created when it truly needs to be created. It also returns the value it created or a cloned value of it, which is why it's value requires to be `Clone`. But if the value is cloned it means that any change that we make to the cloned value will not be -persisted for the next change? Worry no more, we got smart pointers. +persisted for the next change? Correct, but this is why we are going to be using smart pointers. Here there is an example, even though our component will run as many times as it needs it will always hold the same value -it was created with, the `Default` of `T`. Because when the component runs what is cloned is the `Rc` and not `T`. +it was created with, the `Default` of `T`. Because when the component runs, what gets is not the `T` but the `Rc`. ```rs fn use_value() -> Rc> { use_hook(|| { diff --git a/website/src/layouts/BlogPostLayout.astro b/website/src/layouts/BlogPostLayout.astro index 0acd4df4e..461eae470 100644 --- a/website/src/layouts/BlogPostLayout.astro +++ b/website/src/layouts/BlogPostLayout.astro @@ -44,13 +44,21 @@ const formattedDate = (new Date(frontmatter.date)).toLocaleString().split(',')[0 main * :not(span, pre, code) { color: #d2d2d2; font-family: "Inter", system-ui, sans-serif; - line-height: 1.7; + line-height: 1.8; } main *:is(h1, h2, h3, h4, h5, h6) { color: white; font-family: "Inter", system-ui, sans-serif; - line-height: 1.7; + line-height: 1.8; + } + + main strong { + background: rgb(163, 163, 163); + border-radius: 6px; + padding: 2px 4px; + color: rgb(35, 35, 35); + font-weight: normal; } ul { @@ -62,9 +70,10 @@ const formattedDate = (new Date(frontmatter.date)).toLocaleString().split(',')[0 } *:not(pre) > code { - background: rgb(65, 65, 65); + background: rgb(109, 176, 239); border-radius: 6px; padding: 2px 4px; + color: rgb(24, 72, 117); } p { From 2e4ff745527e5b77d08992ebd3f66493bce433bd Mon Sep 17 00:00:00 2001 From: marc2332 Date: Wed, 21 Aug 2024 15:12:35 +0200 Subject: [PATCH 6/6] improvements --- .../blog/building-custom-state-hook-dioxus.md | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/website/src/content/blog/building-custom-state-hook-dioxus.md b/website/src/content/blog/building-custom-state-hook-dioxus.md index 85269a804..266d99ccf 100644 --- a/website/src/content/blog/building-custom-state-hook-dioxus.md +++ b/website/src/content/blog/building-custom-state-hook-dioxus.md @@ -12,7 +12,7 @@ how are hooks made. ### ๐Ÿ‘‹ Preface -**Dioxus** already comes with a few state management solutions, primarily in the form of hooks and powered by **Signals**. +**Dioxus** has built-in support for reactive state management in the form of hooks and **Signals**. I am not gonna dive into what hooks or signals are, so if you don't know what they are you may check the official docs first. You may be asking yourself why do we even need to build custom hooks if Dioxus already give us different tools for different use cases. @@ -24,8 +24,8 @@ For those who happen to be in this situation, as I was myself when building [Val ### ๐Ÿค Introduction -So, a bit of introduction. We will build a custom hook that give us a reactive `key-value` store, -where we can subscribe to the value of specific keys across multiple components. The hook will be called `use_value` +So, a bit of introduction. We will build a custom hook that let us have cross-component reactive `key-value` store. +The idea is that components subscribe to specific keys, so every time anyone modifies the value of that specific key, the component will rerun. We will call this hook `use_value` This is how it will look like: @@ -67,7 +67,7 @@ fn app() { It let us store value that is tied to life of the **Scope** it was created in, which means that when the **Component** is dropped, the stored value will be dropped as well. -It takes a closure to initialize our value, this be called when the component runs for the first time, this way the value is only created when it truly needs to be created. It also returns the value it created or a cloned value of it, which is why it's value requires to be `Clone`. +It takes a closure to initialize our value, this be called when the component runs for the first time, this way the value is only created when it truly needs to be created. It also returns the value it created or a cloned value of it if this isn't the first run anymore, is why it's value requires to be `Clone`. But if the value is cloned it means that any change that we make to the cloned value will not be persisted for the next change? Correct, but this is why we are going to be using smart pointers. @@ -84,11 +84,13 @@ fn use_value() -> Rc> { ### The not so basic Alright we got a dummy hook that all it does is store a value, but how do we share this value with an assigned key? -We just need to build a registry. - +We just need to build a registry where all subscribers self-register and self-unregister. ```rs +/// For every key we have in the registry +/// there will be a `RegistryEntry` where the value and +/// subscribers are stored. struct RegistryEntry { value: Signal, subscribers: HashSet @@ -99,11 +101,11 @@ struct Registry { } impl Registry { - /// Subscribe the given [Scope] in this registry to `key`. + /// Subscribe the given [ScopeId] in this registry to `key`. /// Will initialize the value to `T::default` if there wasn't one before. pub fn subscribe(&mut self, key: String, scope_id: ScopeId) -> { self.map.insert_or_get(key, || RegistryEntry { - value: Signal::new(T::default()), + value: Signal::new_in_scope(T::default(), ScopeId::ROOT), subscribers: HashSet::from([scope_id]) }) .subscribers.insert(scope_id) @@ -120,12 +122,13 @@ impl Registry { } } - /// Get the [Signal] assigned to the given `key`. + /// Get the [Signal] of the value assigned to the given `key`. pub fn get(&self, key: &str) -> Signal { self.map.get(key).copied().unwrap() } } +/// Access the value assigned to the given key. fn use_value(key: &str) -> Signal { use_hook(|| { // Access the registry and if it doesn't exist create it in the root scope @@ -136,9 +139,9 @@ fn use_value(key: &str) -> Signal { }); // Subscribe this component - registry.subscribe(current_scope_id().unwrap()); + registry.subscribe(key, current_scope_id().unwrap()); - // Return the Signal assigned to the given key + // Return value of the given key registry.get(key) }) }