Merge pull request #1112 from DianQK/master

Fix the issue of duplicate unique ID for atoms using `newtype`.
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2023-07-14 15:36:41 -07:00
commit c9dbae8d92
15 files changed
+80 -90

No files matched your search

+3 -1
View File
@@ -33,4 +33,6 @@ jobs:
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- run: cargo test --all --tests - run: |
cargo test --all --tests
cargo test --package fermi --release
+1
View File
@@ -85,4 +85,5 @@ jobs:
set RUST_BACKTRACE=1 set RUST_BACKTRACE=1
cargo build --all --tests --examples cargo build --all --tests --examples
cargo test --all --tests cargo test --all --tests
cargo test --package fermi --release
shell: cmd shell: cmd
@@ -94,8 +94,10 @@ Calling `deref` or `deref_mut` is actually more complex than it seems. When a va
Sometimes you want a signal to propagate across your app, either through far-away siblings or through deeply-nested components. In these cases, we use Dirac: Dioxus's first-class state management toolkit. Dirac atoms automatically implement the Signal API. This component will bind the input element to the `TITLE` atom. Sometimes you want a signal to propagate across your app, either through far-away siblings or through deeply-nested components. In these cases, we use Dirac: Dioxus's first-class state management toolkit. Dirac atoms automatically implement the Signal API. This component will bind the input element to the `TITLE` atom.
```rust, no_run ```rust, no_run
const TITLE: Atom<String> = || "".to_string(); const TITLE: Atom<String> = Atom(|| "".to_string());
const Provider: Component = |cx|{ const Provider: Component = |cx|{
let title = use_signal(cx, &TITLE); let title = use_signal(cx, &TITLE);
render!(input { value: title }) render!(input { value: title })
@@ -131,7 +133,8 @@ By default, Dioxus is limited when you use iter/map. With the `For` component, y
Dioxus automatically understands how to use your signals when mixed with iterators through `Deref`/`DerefMut`. This lets you efficiently map collections while avoiding the re-rendering of lists. In essence, signals act as a hint to Dioxus on how to avoid un-necessary checks and renders, making your app faster. Dioxus automatically understands how to use your signals when mixed with iterators through `Deref`/`DerefMut`. This lets you efficiently map collections while avoiding the re-rendering of lists. In essence, signals act as a hint to Dioxus on how to avoid un-necessary checks and renders, making your app faster.
```rust, no_run ```rust, no_run
const DICT: AtomFamily<String, String> = |_| {}; const DICT: AtomFamily<String, String> = AtomFamily(|_| {});
const List: Component = |cx|{ const List: Component = |cx|{
let dict = use_signal(cx, &DICT); let dict = use_signal(cx, &DICT);
cx.render(rsx!( cx.render(rsx!(
@@ -142,14 +145,6 @@ const List: Component = |cx|{
}; };
``` ```
## Remote Signals
Apps that use signals will enjoy a pleasant hybrid of server-side and client-side rendering.
```rust, no_run
```
## How does it work? ## How does it work?
Signals internally use Dioxus' asynchronous rendering infrastructure to perform updates out of the tree. Signals internally use Dioxus' asynchronous rendering infrastructure to perform updates out of the tree.
+4 -4
View File
@@ -143,7 +143,7 @@ async fn editor_service(rx: UnboundedReceiver<EditorCommand>) {
We can combine coroutines with [Fermi](https://docs.rs/fermi/latest/fermi/index.html) to emulate Redux Toolkit's Thunk system with much less headache. This lets us store all of our app's state _within_ a task and then simply update the "view" values stored in Atoms. It cannot be understated how powerful this technique is: we get all the perks of native Rust tasks with the optimizations and ergonomics of global state. This means your _actual_ state does not need to be tied up in a system like Fermi or Redux – the only Atoms that need to exist are those that are used to drive the display/UI. We can combine coroutines with [Fermi](https://docs.rs/fermi/latest/fermi/index.html) to emulate Redux Toolkit's Thunk system with much less headache. This lets us store all of our app's state _within_ a task and then simply update the "view" values stored in Atoms. It cannot be understated how powerful this technique is: we get all the perks of native Rust tasks with the optimizations and ergonomics of global state. This means your _actual_ state does not need to be tied up in a system like Fermi or Redux – the only Atoms that need to exist are those that are used to drive the display/UI.
```rust, no_run ```rust, no_run
static USERNAME: Atom<String> = |_| "default".to_string(); static USERNAME: Atom<String> = Atom(|_| "default".to_string());
fn app(cx: Scope) -> Element { fn app(cx: Scope) -> Element {
let atoms = use_atom_root(cx); let atoms = use_atom_root(cx);
@@ -156,7 +156,7 @@ fn app(cx: Scope) -> Element {
} }
fn Banner(cx: Scope) -> Element { fn Banner(cx: Scope) -> Element {
let username = use_read(cx, USERNAME); let username = use_read(cx, &USERNAME);
cx.render(rsx!{ cx.render(rsx!{
h1 { "Welcome back, {username}" } h1 { "Welcome back, {username}" }
@@ -174,8 +174,8 @@ enum SyncAction {
} }
async fn sync_service(mut rx: UnboundedReceiver<SyncAction>, atoms: AtomRoot) { async fn sync_service(mut rx: UnboundedReceiver<SyncAction>, atoms: AtomRoot) {
let username = atoms.write(USERNAME); let username = atoms.write(&USERNAME);
let errors = atoms.write(ERRORS); let errors = atoms.write(&ERRORS);
while let Ok(msg) = rx.next().await { while let Ok(msg) = rx.next().await {
match msg { match msg {
@@ -113,14 +113,14 @@ enum InputError {
TooShort, TooShort,
} }
static INPUT_ERROR: Atom<InputError> = |_| InputError::None; static INPUT_ERROR: Atom<InputError> = Atom(|_| InputError::None);
``` ```
Then, in our top level component, we want to explicitly handle the possible error state for this part of the tree. Then, in our top level component, we want to explicitly handle the possible error state for this part of the tree.
```rust, no_run ```rust, no_run
fn TopLevel(cx: Scope) -> Element { fn TopLevel(cx: Scope) -> Element {
let error = use_read(cx, INPUT_ERROR); let error = use_read(cx, &INPUT_ERROR);
match error { match error {
TooLong => return cx.render(rsx!{ "FAILED: Too long!" }), TooLong => return cx.render(rsx!{ "FAILED: Too long!" }),
@@ -134,7 +134,7 @@ Now, whenever a downstream component has an error in its actions, it can simply
```rust, no_run ```rust, no_run
fn Commandline(cx: Scope) -> Element { fn Commandline(cx: Scope) -> Element {
let set_error = use_set(cx, INPUT_ERROR); let set_error = use_set(cx, &INPUT_ERROR);
cx.render(rsx!{ cx.render(rsx!{
input { input {
+4 -4
View File
@@ -105,7 +105,7 @@ async fn editor_service(rx: UnboundedReceiver<EditorCommand>) {
Podemos combinar corrotinas com `Fermi` para emular o sistema `Thunk` do **Redux Toolkit** com muito menos dor de cabeça. Isso nos permite armazenar todo o estado do nosso aplicativo _dentro_ de uma tarefa e, em seguida, simplesmente atualizar os valores de "visualização" armazenados em `Atoms`. Não pode ser subestimado o quão poderosa é essa técnica: temos todas as vantagens das tarefas nativas do Rust com as otimizações e ergonomia do estado global. Isso significa que seu estado _real_ não precisa estar vinculado a um sistema como `Fermi` ou `Redux` – os únicos `Atoms` que precisam existir são aqueles que são usados para controlar a interface. Podemos combinar corrotinas com `Fermi` para emular o sistema `Thunk` do **Redux Toolkit** com muito menos dor de cabeça. Isso nos permite armazenar todo o estado do nosso aplicativo _dentro_ de uma tarefa e, em seguida, simplesmente atualizar os valores de "visualização" armazenados em `Atoms`. Não pode ser subestimado o quão poderosa é essa técnica: temos todas as vantagens das tarefas nativas do Rust com as otimizações e ergonomia do estado global. Isso significa que seu estado _real_ não precisa estar vinculado a um sistema como `Fermi` ou `Redux` – os únicos `Atoms` que precisam existir são aqueles que são usados para controlar a interface.
```rust, no_run ```rust, no_run
static USERNAME: Atom<String> = |_| "default".to_string(); static USERNAME: Atom<String> = Atom(|_| "default".to_string());
fn app(cx: Scope) -> Element { fn app(cx: Scope) -> Element {
let atoms = use_atom_root(cx); let atoms = use_atom_root(cx);
@@ -118,7 +118,7 @@ fn app(cx: Scope) -> Element {
} }
fn Banner(cx: Scope) -> Element { fn Banner(cx: Scope) -> Element {
let username = use_read(cx, USERNAME); let username = use_read(cx, &USERNAME);
cx.render(rsx!{ cx.render(rsx!{
h1 { "Welcome back, {username}" } h1 { "Welcome back, {username}" }
@@ -134,8 +134,8 @@ enum SyncAction {
} }
async fn sync_service(mut rx: UnboundedReceiver<SyncAction>, atoms: AtomRoot) { async fn sync_service(mut rx: UnboundedReceiver<SyncAction>, atoms: AtomRoot) {
let username = atoms.write(USERNAME); let username = atoms.write(&USERNAME);
let errors = atoms.write(ERRORS); let errors = atoms.write(&ERRORS);
while let Ok(msg) = rx.next().await { while let Ok(msg) = rx.next().await {
match msg { match msg {
@@ -113,14 +113,14 @@ enum InputError {
TooShort, TooShort,
} }
static INPUT_ERROR: Atom<InputError> = |_| InputError::None; static INPUT_ERROR: Atom<InputError> = Atom(|_| InputError::None);
``` ```
Então, em nosso componente de nível superior, queremos tratar explicitamente o possível estado de erro para esta parte da árvore. Então, em nosso componente de nível superior, queremos tratar explicitamente o possível estado de erro para esta parte da árvore.
```rust, no_run ```rust, no_run
fn TopLevel(cx: Scope) -> Element { fn TopLevel(cx: Scope) -> Element {
let error = use_read(cx, INPUT_ERROR); let error = use_read(cx, &INPUT_ERROR);
match error { match error {
TooLong => return cx.render(rsx!{ "FAILED: Too long!" }), TooLong => return cx.render(rsx!{ "FAILED: Too long!" }),
@@ -134,7 +134,7 @@ Agora, sempre que um componente _downstream_ tiver um erro em suas ações, ele
```rust, no_run ```rust, no_run
fn Commandline(cx: Scope) -> Element { fn Commandline(cx: Scope) -> Element {
let set_error = use_set(cx, INPUT_ERROR); let set_error = use_set(cx, &INPUT_ERROR);
cx.render(rsx!{ cx.render(rsx!{
input { input {
+5 -5
View File
@@ -7,11 +7,11 @@ fn main() {
dioxus_desktop::launch(app) dioxus_desktop::launch(app)
} }
static NAME: Atom<String> = |_| "world".to_string(); static NAME: Atom<String> = Atom(|_| "world".to_string());
fn app(cx: Scope) -> Element { fn app(cx: Scope) -> Element {
use_init_atom_root(cx); use_init_atom_root(cx);
let name = use_read(cx, NAME); let name = use_read(cx, &NAME);
cx.render(rsx! { cx.render(rsx! {
div { "hello {name}!" } div { "hello {name}!" }
@@ -21,7 +21,7 @@ fn app(cx: Scope) -> Element {
} }
fn Child(cx: Scope) -> Element { fn Child(cx: Scope) -> Element {
let set_name = use_set(cx, NAME); let set_name = use_set(cx, &NAME);
cx.render(rsx! { cx.render(rsx! {
button { button {
@@ -31,10 +31,10 @@ fn Child(cx: Scope) -> Element {
}) })
} }
static NAMES: AtomRef<Vec<String>> = |_| vec!["world".to_string()]; static NAMES: AtomRef<Vec<String>> = AtomRef(|_| vec!["world".to_string()]);
fn ChildWithRef(cx: Scope) -> Element { fn ChildWithRef(cx: Scope) -> Element {
let names = use_atom_ref(cx, NAMES); let names = use_atom_ref(cx, &NAMES);
cx.render(rsx! { cx.render(rsx! {
div { div {
+10 -10
View File
@@ -1,4 +1,3 @@
<div align="center"> <div align="center">
<h1>Fermi ⚛</h1> <h1>Fermi ⚛</h1>
<p> <p>
@@ -6,7 +5,6 @@
</p> </p>
</div> </div>
<div align="center"> <div align="center">
<!-- Crates version --> <!-- Crates version -->
<a href="https://crates.io/crates/fermi"> <a href="https://crates.io/crates/fermi">
@@ -30,21 +28,21 @@
</a> </a>
</div> </div>
----- ---
Fermi is a global state management solution for Dioxus that's as easy as `use_state`. Fermi is a global state management solution for Dioxus that's as easy as `use_state`.
Inspired by atom-based state management solutions, all state in Fermi starts as an `atom`: Inspired by atom-based state management solutions, all state in Fermi starts as an `atom`:
```rust, ignore ```rust, ignore
static NAME: Atom<&str> = |_| "Dioxus"; static NAME: Atom<&str> = Atom(|_| "Dioxus");
``` ```
From anywhere in our app, we can read the value of our atom: From anywhere in our app, we can read the value of our atom:
```rust, ignores ```rust, ignore
fn NameCard(cx: Scope) -> Element { fn NameCard(cx: Scope) -> Element {
let name = use_read(cx, NAME); let name = use_read(cx, &NAME);
cx.render(rsx!{ h1 { "Hello, {name}"} }) cx.render(rsx!{ h1 { "Hello, {name}"} })
} }
``` ```
@@ -53,7 +51,7 @@ We can also set the value of our atom, also from anywhere in our app:
```rust, ignore ```rust, ignore
fn NameCard(cx: Scope) -> Element { fn NameCard(cx: Scope) -> Element {
let set_name = use_set(cx, NAME); let set_name = use_set(cx, &NAME);
cx.render(rsx!{ cx.render(rsx!{
button { button {
onclick: move |_| set_name("Fermi"), onclick: move |_| set_name("Fermi"),
@@ -66,10 +64,10 @@ fn NameCard(cx: Scope) -> Element {
If needed, we can update the atom's value, based on itself: If needed, we can update the atom's value, based on itself:
```rust, ignore ```rust, ignore
static COUNT: Atom<i32> = |_| 0; static COUNT: Atom<i32> = Atom(|_| 0);
fn Counter(cx: Scope) -> Element { fn Counter(cx: Scope) -> Element {
let mut count = use_atom_state(cx, COUNT); let mut count = use_atom_state(cx, &COUNT);
cx.render(rsx!{ cx.render(rsx!{
p { p {
@@ -86,6 +84,7 @@ fn Counter(cx: Scope) -> Element {
It's that simple! It's that simple!
## Installation ## Installation
Fermi is currently under construction, so you have to use the `master` branch to get started. Fermi is currently under construction, so you have to use the `master` branch to get started.
```toml ```toml
@@ -93,10 +92,10 @@ Fermi is currently under construction, so you have to use the `master` branch to
fermi = { git = "https://github.com/dioxuslabs/dioxus" } fermi = { git = "https://github.com/dioxuslabs/dioxus" }
``` ```
## Running examples ## Running examples
The examples here use Dioxus Desktop to showcase their functionality. To run an example, use The examples here use Dioxus Desktop to showcase their functionality. To run an example, use
```sh ```sh
$ cargo run --example fermi $ cargo run --example fermi
``` ```
@@ -104,6 +103,7 @@ $ cargo run --example fermi
## Features ## Features
Broadly our feature set required to be released includes: Broadly our feature set required to be released includes:
- [x] Support for Atoms - [x] Support for Atoms
- [x] Support for AtomRef (for values that aren't `Clone`) - [x] Support for AtomRef (for values that aren't `Clone`)
- [ ] Support for Atom Families - [ ] Support for Atom Families
+23 -10
View File
@@ -1,24 +1,21 @@
use crate::{AtomId, AtomRoot, Readable, Writable}; use crate::{AtomId, AtomRoot, Readable, Writable};
pub type Atom<T> = fn(AtomBuilder) -> T; pub struct Atom<T>(pub fn(AtomBuilder) -> T);
pub struct AtomBuilder; pub struct AtomBuilder;
impl<V: 'static> Readable<V> for Atom<V> { impl<V> Readable<V> for &'static Atom<V> {
fn read(&self, _root: AtomRoot) -> Option<V> { fn read(&self, _root: AtomRoot) -> Option<V> {
todo!() todo!()
} }
fn init(&self) -> V { fn init(&self) -> V {
(*self)(AtomBuilder) self.0(AtomBuilder)
} }
fn unique_id(&self) -> AtomId { fn unique_id(&self) -> AtomId {
AtomId { *self as *const Atom<V> as *const ()
ptr: *self as *const (),
type_id: std::any::TypeId::of::<V>(),
}
} }
} }
impl<V: 'static> Writable<V> for Atom<V> { impl<V> Writable<V> for &'static Atom<V> {
fn write(&self, _root: AtomRoot, _value: V) { fn write(&self, _root: AtomRoot, _value: V) {
todo!() todo!()
} }
@@ -26,6 +23,22 @@ impl<V: 'static> Writable<V> for Atom<V> {
#[test] #[test]
fn atom_compiles() { fn atom_compiles() {
static TEST_ATOM: Atom<&str> = |_| "hello"; static TEST_ATOM: Atom<&str> = Atom(|_| "hello");
dbg!(TEST_ATOM.init()); dbg!((&TEST_ATOM).init());
}
#[test]
fn atom_is_unique() {
static TEST_ATOM_1: Atom<&str> = Atom(|_| "hello");
static TEST_ATOM_2: Atom<&str> = Atom(|_| "hello");
assert_eq!((&TEST_ATOM_1).unique_id(), (&TEST_ATOM_1).unique_id());
assert_ne!((&TEST_ATOM_1).unique_id(), (&TEST_ATOM_2).unique_id());
}
#[test]
fn atom_is_unique_2() {
struct S(String);
static TEST_ATOM_1: Atom<Vec<S>> = Atom(|_| Vec::new());
static TEST_ATOM_2: Atom<Vec<String>> = Atom(|_| Vec::new());
assert_ne!((&TEST_ATOM_1).unique_id(), (&TEST_ATOM_2).unique_id());
} }
+5 -8
View File
@@ -2,26 +2,23 @@ use crate::{AtomId, AtomRoot, Readable, Writable};
use im_rc::HashMap as ImMap; use im_rc::HashMap as ImMap;
pub struct AtomFamilyBuilder; pub struct AtomFamilyBuilder;
pub type AtomFamily<K, V> = fn(AtomFamilyBuilder) -> ImMap<K, V>; pub struct AtomFamily<K, V>(pub fn(AtomFamilyBuilder) -> ImMap<K, V>);
impl<K, V: 'static> Readable<ImMap<K, V>> for AtomFamily<K, V> { impl<K, V> Readable<ImMap<K, V>> for &'static AtomFamily<K, V> {
fn read(&self, _root: AtomRoot) -> Option<ImMap<K, V>> { fn read(&self, _root: AtomRoot) -> Option<ImMap<K, V>> {
todo!() todo!()
} }
fn init(&self) -> ImMap<K, V> { fn init(&self) -> ImMap<K, V> {
(*self)(AtomFamilyBuilder) self.0(AtomFamilyBuilder)
} }
fn unique_id(&self) -> AtomId { fn unique_id(&self) -> AtomId {
AtomId { *self as *const AtomFamily<K, V> as *const ()
ptr: *self as *const (),
type_id: std::any::TypeId::of::<V>(),
}
} }
} }
impl<K, V: 'static> Writable<ImMap<K, V>> for AtomFamily<K, V> { impl<K, V> Writable<ImMap<K, V>> for &'static AtomFamily<K, V> {
fn write(&self, _root: AtomRoot, _value: ImMap<K, V>) { fn write(&self, _root: AtomRoot, _value: ImMap<K, V>) {
todo!() todo!()
} }
+6 -9
View File
@@ -2,27 +2,24 @@ use crate::{AtomId, AtomRoot, Readable};
use std::cell::RefCell; use std::cell::RefCell;
pub struct AtomRefBuilder; pub struct AtomRefBuilder;
pub type AtomRef<T> = fn(AtomRefBuilder) -> T; pub struct AtomRef<T>(pub fn(AtomRefBuilder) -> T);
impl<V: 'static> Readable<RefCell<V>> for AtomRef<V> { impl<V> Readable<RefCell<V>> for &'static AtomRef<V> {
fn read(&self, _root: AtomRoot) -> Option<RefCell<V>> { fn read(&self, _root: AtomRoot) -> Option<RefCell<V>> {
todo!() todo!()
} }
fn init(&self) -> RefCell<V> { fn init(&self) -> RefCell<V> {
RefCell::new((*self)(AtomRefBuilder)) RefCell::new(self.0(AtomRefBuilder))
} }
fn unique_id(&self) -> AtomId { fn unique_id(&self) -> AtomId {
AtomId { *self as *const AtomRef<V> as *const ()
ptr: *self as *const (),
type_id: std::any::TypeId::of::<V>(),
}
} }
} }
#[test] #[test]
fn atom_compiles() { fn atom_compiles() {
static TEST_ATOM: AtomRef<Vec<String>> = |_| vec![]; static TEST_ATOM: AtomRef<Vec<String>> = AtomRef(|_| vec![]);
dbg!(TEST_ATOM.init()); dbg!((&TEST_ATOM).init());
} }
+4 -1
View File
@@ -13,7 +13,10 @@ use std::{
/// ///
/// ///
/// ///
pub fn use_atom_ref<T: 'static>(cx: &ScopeState, atom: AtomRef<T>) -> &UseAtomRef<T> { pub fn use_atom_ref<'a, T: 'static>(
cx: &'a ScopeState,
atom: &'static AtomRef<T>,
) -> &'a UseAtomRef<T> {
let root = use_atom_root(cx); let root = use_atom_root(cx);
&cx.use_hook(|| { &cx.use_hook(|| {
+1 -1
View File
@@ -19,7 +19,7 @@ use std::{
/// static COUNT: Atom<u32> = |_| 0; /// static COUNT: Atom<u32> = |_| 0;
/// ///
/// fn Example(cx: Scope) -> Element { /// fn Example(cx: Scope) -> Element {
/// let mut count = use_atom_state(cx, COUNT); /// let mut count = use_atom_state(cx, &COUNT);
/// ///
/// cx.render(rsx! { /// cx.render(rsx! {
/// div { /// div {
+3 -21
View File
@@ -1,21 +1,11 @@
use std::{ use std::{any::Any, cell::RefCell, collections::HashMap, rc::Rc, sync::Arc};
any::{Any, TypeId},
cell::RefCell,
collections::HashMap,
rc::Rc,
sync::Arc,
};
use dioxus_core::ScopeId; use dioxus_core::ScopeId;
use im_rc::HashSet; use im_rc::HashSet;
use crate::Readable; use crate::Readable;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub type AtomId = *const ();
pub struct AtomId {
pub ptr: *const (),
pub type_id: TypeId,
}
pub struct AtomRoot { pub struct AtomRoot {
pub atoms: RefCell<HashMap<AtomId, Slot>>, pub atoms: RefCell<HashMap<AtomId, Slot>>,
@@ -54,15 +44,7 @@ impl AtomRoot {
// initialize the value if it's not already initialized // initialize the value if it's not already initialized
if let Some(slot) = atoms.get_mut(&f.unique_id()) { if let Some(slot) = atoms.get_mut(&f.unique_id()) {
slot.subscribers.insert(scope); slot.subscribers.insert(scope);
match slot.value.clone().downcast() { slot.value.clone().downcast().unwrap()
Ok(res) => res,
Err(e) => panic!(
"Downcasting atom failed: {:?}. Has typeid of {:?} but needs typeid of {:?}",
f.unique_id(),
e.type_id(),
TypeId::of::<V>()
),
}
} else { } else {
let value = Rc::new(f.init()); let value = Rc::new(f.init());
let mut subscribers = HashSet::new(); let mut subscribers = HashSet::new();