Selaa lähdekoodia

Merge branch 'master' into router-typesafe

Evan Almloff 1 vuosi sitten
vanhempi
commit
4c3fb1a33f
42 muutettua tiedostoa jossa 327 lisäystä ja 168 poistoa
  1. 1 1
      .github/workflows/docs stable.yml
  2. 1 1
      .github/workflows/docs.yml
  3. 3 1
      .github/workflows/macos.yml
  4. 1 1
      .github/workflows/main.yml
  5. 1 1
      .github/workflows/playwright.yml
  6. 1 0
      .github/workflows/windows.yml
  7. 2 2
      .gitignore
  8. 4 9
      Cargo.toml
  9. 2 0
      docs/guide/src/en/SUMMARY.md
  10. 5 10
      docs/guide/src/en/__unused/advanced-guides/12-signals.md
  11. 4 4
      docs/guide/src/en/async/use_coroutine.md
  12. 41 0
      docs/guide/src/en/async/use_effect.md
  13. 3 3
      docs/guide/src/en/best_practices/error_handling.md
  14. 19 0
      docs/guide/src/en/interactivity/memoization.md
  15. 3 3
      docs/guide/src/en/interactivity/sharing_state.md
  16. 4 4
      docs/guide/src/pt-br/async/use_coroutine.md
  17. 3 3
      docs/guide/src/pt-br/best_practices/error_handling.md
  18. 5 5
      examples/fermi.rs
  19. 77 0
      examples/shared_state.rs
  20. 20 8
      packages/cli/src/config.rs
  21. 1 1
      packages/cli/src/main.rs
  22. 10 10
      packages/fermi/README.md
  23. 23 10
      packages/fermi/src/atoms/atom.rs
  24. 5 8
      packages/fermi/src/atoms/atomfamily.rs
  25. 6 9
      packages/fermi/src/atoms/atomref.rs
  26. 4 1
      packages/fermi/src/hooks/atom_ref.rs
  27. 1 1
      packages/fermi/src/hooks/state.rs
  28. 3 21
      packages/fermi/src/root.rs
  29. 23 7
      packages/hooks/src/useeffect.rs
  30. 15 9
      packages/hooks/src/usememo.rs
  31. 15 15
      packages/hooks/src/useref.rs
  32. 0 0
      playwright-tests/fullstack.spec.js
  33. 0 0
      playwright-tests/fullstack/.gitignore
  34. 1 1
      playwright-tests/fullstack/Cargo.toml
  35. 1 1
      playwright-tests/fullstack/src/main.rs
  36. 0 0
      playwright-tests/liveview.spec.js
  37. 2 2
      playwright-tests/liveview/Cargo.toml
  38. 1 1
      playwright-tests/liveview/src/main.rs
  39. 0 0
      playwright-tests/web.spec.js
  40. 2 2
      playwright-tests/web/Cargo.toml
  41. 1 1
      playwright-tests/web/src/main.rs
  42. 13 12
      playwright.config.js

+ 1 - 1
.github/workflows/docs stable.yml

@@ -29,7 +29,7 @@ jobs:
           # cd fermi && mdbook build -d ../nightly/fermi && cd ..
 
       - name: Deploy 🚀
-        uses: JamesIves/github-pages-deploy-action@v4.4.2
+        uses: JamesIves/github-pages-deploy-action@v4.4.3
         with:
           branch: gh-pages # The branch the action should deploy to.
           folder: docs/nightly # The folder the action should deploy.

+ 1 - 1
.github/workflows/docs.yml

@@ -34,7 +34,7 @@ jobs:
           # cd fermi && mdbook build -d ../nightly/fermi && cd ..
 
       - name: Deploy 🚀
-        uses: JamesIves/github-pages-deploy-action@v4.4.2
+        uses: JamesIves/github-pages-deploy-action@v4.4.3
         with:
           branch: gh-pages # The branch the action should deploy to.
           folder: docs/nightly # The folder the action should deploy.

+ 3 - 1
.github/workflows/macos.yml

@@ -33,4 +33,6 @@ jobs:
       - uses: dtolnay/rust-toolchain@stable
       - uses: Swatinem/rust-cache@v2
       - uses: actions/checkout@v3
-      - run: cargo test --all --tests
+      - run: |
+          cargo test --all --tests
+          cargo test --package fermi --release

+ 1 - 1
.github/workflows/main.yml

@@ -13,7 +13,7 @@ on:
       - lib.rs
       - Cargo.toml
       - Makefile.toml
-      - playwrite-tests/**
+      - playwright-tests/**
 
   pull_request:
     types: [opened, synchronize, reopened, ready_for_review]

+ 1 - 1
.github/workflows/playwright.yml

@@ -8,7 +8,7 @@ jobs:
   test:
     if: github.event.pull_request.draft == false
     timeout-minutes: 60
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-20.04
     steps:
     - uses: actions/checkout@v3
     - uses: actions/setup-node@v3

+ 1 - 0
.github/workflows/windows.yml

@@ -85,4 +85,5 @@ jobs:
           set RUST_BACKTRACE=1
           cargo build --all --tests --examples
           cargo test --all --tests
+          cargo test --package fermi --release
         shell: cmd

+ 2 - 2
.gitignore

@@ -1,6 +1,6 @@
 /target
-/playwrite-tests/web/dist
-/playwrite-tests/fullstack/dist
+/playwright-tests/web/dist
+/playwright-tests/fullstack/dist
 /dist
 Cargo.lock
 .DS_Store

+ 4 - 9
Cargo.toml

@@ -39,10 +39,10 @@ members = [
     # Full project examples
     "examples/tailwind",
     "examples/PWA-example",
-    # Playwrite tests
-    "playwrite-tests/liveview",
-    "playwrite-tests/web",
-    "playwrite-tests/fullstack",
+    # Playwright tests
+    "playwright-tests/liveview",
+    "playwright-tests/web",
+    "playwright-tests/fullstack",
 ]
 exclude = ["examples/mobile_demo"]
 
@@ -120,8 +120,3 @@ fern = { version = "0.6.0", features = ["colored"] }
 thiserror = "1.0.30"
 env_logger = "0.10.0"
 simple_logger = "4.0.0"
-
-[profile.release]
-opt-level = 3
-lto = true
-debug = true

+ 2 - 0
docs/guide/src/en/SUMMARY.md

@@ -21,10 +21,12 @@
   - [Hooks & Component State](interactivity/hooks.md)
   - [User Input](interactivity/user_input.md)
   - [Sharing State](interactivity/sharing_state.md)
+  - [Memoization](interactivity/memoization.md)
   - [Custom Hooks](interactivity/custom_hooks.md)
   - [Dynamic Rendering](interactivity/dynamic_rendering.md)
   - [Routing](interactivity/router.md)
 - [Async](async/index.md)
+  - [UseEffect](async/use_effect.md)
   - [UseFuture](async/use_future.md)
   - [UseCoroutine](async/use_coroutine.md)
   - [Spawning Futures](async/spawn.md)

+ 5 - 10
docs/guide/src/en/__unused/advanced-guides/12-signals.md

@@ -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.
 
+
 ```rust, no_run
-const TITLE: Atom<String> = || "".to_string();
+const TITLE: Atom<String> = Atom(|| "".to_string());
+
 const Provider: Component = |cx|{
     let title = use_signal(cx, &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.
 
 ```rust, no_run
-const DICT: AtomFamily<String, String> = |_| {};
+const DICT: AtomFamily<String, String> = AtomFamily(|_| {});
+
 const List: Component = |cx|{
     let dict = use_signal(cx, &DICT);
     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?
 
 Signals internally use Dioxus' asynchronous rendering infrastructure to perform updates out of the tree.

+ 4 - 4
docs/guide/src/en/async/use_coroutine.md

@@ -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.
 
 ```rust, no_run
-static USERNAME: Atom<String> = |_| "default".to_string();
+static USERNAME: Atom<String> = Atom(|_| "default".to_string());
 
 fn app(cx: Scope) -> Element {
     let atoms = use_atom_root(cx);
@@ -156,7 +156,7 @@ fn app(cx: Scope) -> Element {
 }
 
 fn Banner(cx: Scope) -> Element {
-    let username = use_read(cx, USERNAME);
+    let username = use_read(cx, &USERNAME);
 
     cx.render(rsx!{
         h1 { "Welcome back, {username}" }
@@ -174,8 +174,8 @@ enum SyncAction {
 }
 
 async fn sync_service(mut rx: UnboundedReceiver<SyncAction>, atoms: AtomRoot) {
-    let username = atoms.write(USERNAME);
-    let errors = atoms.write(ERRORS);
+    let username = atoms.write(&USERNAME);
+    let errors = atoms.write(&ERRORS);
 
     while let Ok(msg) = rx.next().await {
         match msg {

+ 41 - 0
docs/guide/src/en/async/use_effect.md

@@ -0,0 +1,41 @@
+# UseEffect
+
+[`use_effect`](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_effect.html) lets you run a callback that returns a future, which will be re-run when its [dependencies](#dependencies) change. This is useful to syncrhonize with external events.
+
+## Dependencies
+
+You can make the callback re-run when some value changes. For example, you might want to fetch a user's data only when the user id changes. You can provide a tuple of "dependencies" to the hook. It will automatically re-run it when any of those dependencies change.
+
+## Example
+
+```rust, no_run
+#[inline_props]
+fn Profile(cx: Scope, id: usize) -> Element {
+    let name = use_state(cx, || None);
+
+    // Only fetch the user data when the id changes.
+    use_effect(cx, (id,), |(id,)| {
+        to_owned![name];
+        async move {
+            let user = fetch_user(id).await;
+            name.set(user.name);
+        }
+    });
+
+    // Because the dependencies are empty, this will only run once.
+    // An empty tuple is always equal to an empty tuple.
+    use_effect(cx, (), |()| async move {
+        println!("Hello, World!");
+    });
+
+    let name = name.get().clone().unwrap_or("Loading...".to_string());
+
+    render!(
+        p { "{name}" }
+    )
+}
+
+fn app(cx: Scope) -> Element {
+    render!(Profile { id: 0 })
+}
+```

+ 3 - 3
docs/guide/src/en/best_practices/error_handling.md

@@ -113,14 +113,14 @@ enum InputError {
     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.
 
 ```rust, no_run
 fn TopLevel(cx: Scope) -> Element {
-    let error = use_read(cx, INPUT_ERROR);
+    let error = use_read(cx, &INPUT_ERROR);
 
     match error {
         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
 fn Commandline(cx: Scope) -> Element {
-    let set_error = use_set(cx, INPUT_ERROR);
+    let set_error = use_set(cx, &INPUT_ERROR);
 
     cx.render(rsx!{
         input {

+ 19 - 0
docs/guide/src/en/interactivity/memoization.md

@@ -0,0 +1,19 @@
+# Memoization
+
+[`use_memo`](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_memo.html) let's you memorize values and thus save computation time. This is useful for expensive calculations.
+
+```rust, no_run
+#[inline_props]
+fn Calculator(cx: Scope, number: usize) -> Element {
+    let bigger_number = use_memo(cx, (number,), |(number,)| {
+        // This will only be calculated when `number` has changed.
+        number * 100
+    });
+    render!(
+        p { "{bigger_number}" }
+    )
+}
+fn app(cx: Scope) -> Element {
+    render!(Calculator { number: 0 })
+}
+```

+ 3 - 3
docs/guide/src/en/interactivity/sharing_state.md

@@ -32,7 +32,7 @@ Finally, a third component will render the other two as children. It will be res
 
 ![Meme Editor Screenshot: An old plastic skeleton sitting on a park bench. Caption: "me waiting for a language feature"](./images/meme_editor_screenshot.png)
 
-## Using Context
+## Using Shared State
 
 Sometimes, some state needs to be shared between multiple components far down the tree, and passing it down through props is very inconvenient.
 
@@ -42,7 +42,7 @@ Suppose now that we want to implement a dark mode toggle for our app. To achieve
 
 Now, we could write another `use_state` in the top component, and pass `is_dark_mode` down to every component through props. But think about what will happen as the app grows in complexity – almost every component that renders any CSS is going to need to know if dark mode is enabled or not – so they'll all need the same dark mode prop. And every parent component will need to pass it down to them. Imagine how messy and verbose that would get, especially if we had components several levels deep!
 
-Dioxus offers a better solution than this "prop drilling" – providing context. The [`use_context_provider`](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_context_provider.html) hook is similar to `use_ref`, but it makes it available through [`use_context`](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_context.html) for all children components.
+Dioxus offers a better solution than this "prop drilling" – providing context. The [`use_shared_state_provider`](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_shared_state_provider.html) hook is similar to `use_ref`, but it makes it available through [`use_shared_state`](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_shared_state.html) for all children components.
 
 First, we have to create a struct for our dark mode configuration:
 
@@ -62,7 +62,7 @@ As a result, any child component of `App` (direct or not), can access the `DarkM
 {{#include ../../../examples/meme_editor_dark_mode.rs:use_context}}
 ```
 
-> `use_context` returns `Option<UseSharedState<DarkMode>>` here. If the context has been provided, the value is `Some(UseSharedState<DarkMode>)`, which you can call `.read` or `.write` on, similarly to `UseRef`. Otherwise, the value is `None`.
+> `use_shared_state` returns `Option<UseSharedState<DarkMode>>` here. If the context has been provided, the value is `Some(UseSharedState<DarkMode>)`, which you can call `.read` or `.write` on, similarly to `UseRef`. Otherwise, the value is `None`.
 
 For example, here's how we would implement the dark mode toggle, which both reads the context (to determine what color it should render) and writes to it (to toggle dark mode):
 

+ 4 - 4
docs/guide/src/pt-br/async/use_coroutine.md

@@ -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.
 
 ```rust, no_run
-static USERNAME: Atom<String> = |_| "default".to_string();
+static USERNAME: Atom<String> = Atom(|_| "default".to_string());
 
 fn app(cx: Scope) -> Element {
     let atoms = use_atom_root(cx);
@@ -118,7 +118,7 @@ fn app(cx: Scope) -> Element {
 }
 
 fn Banner(cx: Scope) -> Element {
-    let username = use_read(cx, USERNAME);
+    let username = use_read(cx, &USERNAME);
 
     cx.render(rsx!{
         h1 { "Welcome back, {username}" }
@@ -134,8 +134,8 @@ enum SyncAction {
 }
 
 async fn sync_service(mut rx: UnboundedReceiver<SyncAction>, atoms: AtomRoot) {
-    let username = atoms.write(USERNAME);
-    let errors = atoms.write(ERRORS);
+    let username = atoms.write(&USERNAME);
+    let errors = atoms.write(&ERRORS);
 
     while let Ok(msg) = rx.next().await {
         match msg {

+ 3 - 3
docs/guide/src/pt-br/best_practices/error_handling.md

@@ -113,14 +113,14 @@ enum InputError {
     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.
 
 ```rust, no_run
 fn TopLevel(cx: Scope) -> Element {
-    let error = use_read(cx, INPUT_ERROR);
+    let error = use_read(cx, &INPUT_ERROR);
 
     match error {
         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
 fn Commandline(cx: Scope) -> Element {
-    let set_error = use_set(cx, INPUT_ERROR);
+    let set_error = use_set(cx, &INPUT_ERROR);
 
     cx.render(rsx!{
         input {

+ 5 - 5
examples/fermi.rs

@@ -7,11 +7,11 @@ fn main() {
     dioxus_desktop::launch(app)
 }
 
-static NAME: Atom<String> = |_| "world".to_string();
+static NAME: Atom<String> = Atom(|_| "world".to_string());
 
 fn app(cx: Scope) -> Element {
     use_init_atom_root(cx);
-    let name = use_read(cx, NAME);
+    let name = use_read(cx, &NAME);
 
     cx.render(rsx! {
         div { "hello {name}!" }
@@ -21,7 +21,7 @@ fn app(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! {
         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 {
-    let names = use_atom_ref(cx, NAMES);
+    let names = use_atom_ref(cx, &NAMES);
 
     cx.render(rsx! {
         div {

+ 77 - 0
examples/shared_state.rs

@@ -0,0 +1,77 @@
+#![allow(non_snake_case)]
+
+use std::collections::HashMap;
+
+use dioxus::prelude::*;
+
+fn main() {
+    dioxus_desktop::launch(App);
+}
+
+#[derive(Default)]
+struct CoolData {
+    data: HashMap<usize, String>,
+}
+
+impl CoolData {
+    pub fn new(data: HashMap<usize, String>) -> Self {
+        Self { data }
+    }
+
+    pub fn view(&self, id: &usize) -> Option<&String> {
+        self.data.get(id)
+    }
+
+    pub fn set(&mut self, id: usize, data: String) {
+        self.data.insert(id, data);
+    }
+}
+
+#[rustfmt::skip]
+pub fn App(cx: Scope) -> Element {
+    use_shared_state_provider(cx, || CoolData::new(HashMap::from([
+        (0, "Hello, World!".to_string()),
+        (1, "Dioxus is amazing!".to_string())
+    ])));
+
+    render!(
+        DataEditor {
+            id: 0
+        }
+        DataEditor {
+            id: 1
+        }
+        DataView {
+            id: 0
+        }
+        DataView {
+            id: 1
+        }
+    )
+}
+
+#[inline_props]
+fn DataEditor(cx: Scope, id: usize) -> Element {
+    let cool_data = use_shared_state::<CoolData>(cx).unwrap().read();
+
+    let my_data = &cool_data.view(id).unwrap();
+
+    render!(p {
+        "{my_data}"
+    })
+}
+
+#[inline_props]
+fn DataView(cx: Scope, id: usize) -> Element {
+    let cool_data = use_shared_state::<CoolData>(cx).unwrap();
+
+    let oninput = |e: FormEvent| cool_data.write().set(*id, e.value.clone());
+
+    let cool_data = cool_data.read();
+    let my_data = &cool_data.view(id).unwrap();
+
+    render!(input {
+        oninput: oninput,
+        value: "{my_data}"
+    })
+}

+ 20 - 8
packages/cli/src/config.rs

@@ -1,6 +1,9 @@
 use crate::error::Result;
 use serde::{Deserialize, Serialize};
-use std::{collections::HashMap, path::PathBuf};
+use std::{
+    collections::HashMap,
+    path::{Path, PathBuf},
+};
 
 #[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct DioxusConfig {
@@ -19,27 +22,36 @@ fn default_plugin() -> toml::Value {
 impl DioxusConfig {
     pub fn load() -> crate::error::Result<Option<DioxusConfig>> {
         let Ok(crate_dir) = crate::cargo::crate_root() else { return Ok(None); };
+        let crate_dir = crate_dir.as_path();
 
-        // we support either `Dioxus.toml` or `Cargo.toml`
         let Some(dioxus_conf_file) = acquire_dioxus_toml(crate_dir) else {
             return Ok(None);
         };
 
+        let dioxus_conf_file = dioxus_conf_file.as_path();
         toml::from_str::<DioxusConfig>(&std::fs::read_to_string(dioxus_conf_file)?)
-            .map_err(|_| crate::Error::Unique("Dioxus.toml parse failed".into()))
+            .map_err(|err| {
+                let error_location = dioxus_conf_file
+                    .strip_prefix(crate_dir)
+                    .unwrap_or(dioxus_conf_file)
+                    .display();
+                crate::Error::Unique(format!("{error_location} {err}"))
+            })
             .map(Some)
     }
 }
 
-fn acquire_dioxus_toml(dir: PathBuf) -> Option<PathBuf> {
+fn acquire_dioxus_toml(dir: &Path) -> Option<PathBuf> {
     // prefer uppercase
-    if dir.join("Dioxus.toml").is_file() {
-        return Some(dir.join("Dioxus.toml"));
+    let uppercase_conf = dir.join("Dioxus.toml");
+    if uppercase_conf.is_file() {
+        return Some(uppercase_conf);
     }
 
     // lowercase is fine too
-    if dir.join("dioxus.toml").is_file() {
-        return Some(dir.join("Dioxus.toml"));
+    let lowercase_conf = dir.join("dioxus.toml");
+    if lowercase_conf.is_file() {
+        return Some(lowercase_conf);
     }
 
     None

+ 1 - 1
packages/cli/src/main.rs

@@ -14,7 +14,7 @@ async fn main() -> anyhow::Result<()> {
     set_up_logging();
 
     let _dioxus_config = DioxusConfig::load()
-        .map_err(|e| anyhow!("Failed to load `Dioxus.toml` because: {e}"))?
+        .map_err(|e| anyhow!("Failed to load Dioxus config because: {e}"))?
         .unwrap_or_else(|| {
             log::warn!("You appear to be creating a Dioxus project from scratch; we will use the default config");
             DioxusConfig::default()

+ 10 - 10
packages/fermi/README.md

@@ -1,4 +1,3 @@
-
 <div align="center">
   <h1>Fermi ⚛</h1>
   <p>
@@ -6,7 +5,6 @@
   </p>
 </div>
 
-
 <div align="center">
   <!-- Crates version -->
   <a href="https://crates.io/crates/fermi">
@@ -30,21 +28,21 @@
   </a>
 </div>
 
------
+---
 
 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`:
 
 ```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:
 
-```rust, ignores
+```rust, ignore
 fn NameCard(cx: Scope) -> Element {
-    let name = use_read(cx, NAME);
+    let name = use_read(cx, &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
 fn NameCard(cx: Scope) -> Element {
-    let set_name = use_set(cx, NAME);
+    let set_name = use_set(cx, &NAME);
     cx.render(rsx!{
         button {
             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:
 
 ```rust, ignore
-static COUNT: Atom<i32> = |_| 0;
+static COUNT: Atom<i32> = Atom(|_| 0);
 
 fn Counter(cx: Scope) -> Element {
-    let mut count = use_atom_state(cx, COUNT);
+    let mut count = use_atom_state(cx, &COUNT);
 
     cx.render(rsx!{
         p {
@@ -86,6 +84,7 @@ fn Counter(cx: Scope) -> Element {
 It's that simple!
 
 ## Installation
+
 Fermi is currently under construction, so you have to use the `master` branch to get started.
 
 ```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" }
 ```
 
-
 ## Running examples
 
 The examples here use Dioxus Desktop to showcase their functionality. To run an example, use
+
 ```sh
 $ cargo run --example fermi
 ```
@@ -104,6 +103,7 @@ $ cargo run --example fermi
 ## Features
 
 Broadly our feature set required to be released includes:
+
 - [x] Support for Atoms
 - [x] Support for AtomRef (for values that aren't `Clone`)
 - [ ] Support for Atom Families

+ 23 - 10
packages/fermi/src/atoms/atom.rs

@@ -1,24 +1,21 @@
 use crate::{AtomId, AtomRoot, Readable, Writable};
 
-pub type Atom<T> = fn(AtomBuilder) -> T;
+pub struct Atom<T>(pub fn(AtomBuilder) -> T);
 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> {
         todo!()
     }
     fn init(&self) -> V {
-        (*self)(AtomBuilder)
+        self.0(AtomBuilder)
     }
     fn unique_id(&self) -> AtomId {
-        AtomId {
-            ptr: *self as *const (),
-            type_id: std::any::TypeId::of::<V>(),
-        }
+        *self as *const Atom<V> as *const ()
     }
 }
 
-impl<V: 'static> Writable<V> for Atom<V> {
+impl<V> Writable<V> for &'static Atom<V> {
     fn write(&self, _root: AtomRoot, _value: V) {
         todo!()
     }
@@ -26,6 +23,22 @@ impl<V: 'static> Writable<V> for Atom<V> {
 
 #[test]
 fn atom_compiles() {
-    static TEST_ATOM: Atom<&str> = |_| "hello";
-    dbg!(TEST_ATOM.init());
+    static TEST_ATOM: Atom<&str> = Atom(|_| "hello");
+    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
packages/fermi/src/atoms/atomfamily.rs

@@ -2,26 +2,23 @@ use crate::{AtomId, AtomRoot, Readable, Writable};
 use im_rc::HashMap as ImMap;
 
 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>> {
         todo!()
     }
 
     fn init(&self) -> ImMap<K, V> {
-        (*self)(AtomFamilyBuilder)
+        self.0(AtomFamilyBuilder)
     }
 
     fn unique_id(&self) -> AtomId {
-        AtomId {
-            ptr: *self as *const (),
-            type_id: std::any::TypeId::of::<V>(),
-        }
+        *self as *const AtomFamily<K, V> as *const ()
     }
 }
 
-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>) {
         todo!()
     }

+ 6 - 9
packages/fermi/src/atoms/atomref.rs

@@ -2,27 +2,24 @@ use crate::{AtomId, AtomRoot, Readable};
 use std::cell::RefCell;
 
 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>> {
         todo!()
     }
 
     fn init(&self) -> RefCell<V> {
-        RefCell::new((*self)(AtomRefBuilder))
+        RefCell::new(self.0(AtomRefBuilder))
     }
 
     fn unique_id(&self) -> AtomId {
-        AtomId {
-            ptr: *self as *const (),
-            type_id: std::any::TypeId::of::<V>(),
-        }
+        *self as *const AtomRef<V> as *const ()
     }
 }
 
 #[test]
 fn atom_compiles() {
-    static TEST_ATOM: AtomRef<Vec<String>> = |_| vec![];
-    dbg!(TEST_ATOM.init());
+    static TEST_ATOM: AtomRef<Vec<String>> = AtomRef(|_| vec![]);
+    dbg!((&TEST_ATOM).init());
 }

+ 4 - 1
packages/fermi/src/hooks/atom_ref.rs

@@ -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);
 
     &cx.use_hook(|| {

+ 1 - 1
packages/fermi/src/hooks/state.rs

@@ -19,7 +19,7 @@ use std::{
 /// static COUNT: Atom<u32> = |_| 0;
 ///
 /// fn Example(cx: Scope) -> Element {
-///     let mut count = use_atom_state(cx, COUNT);
+///     let mut count = use_atom_state(cx, &COUNT);
 ///
 ///     cx.render(rsx! {
 ///         div {

+ 3 - 21
packages/fermi/src/root.rs

@@ -1,21 +1,11 @@
-use std::{
-    any::{Any, TypeId},
-    cell::RefCell,
-    collections::HashMap,
-    rc::Rc,
-    sync::Arc,
-};
+use std::{any::Any, cell::RefCell, collections::HashMap, rc::Rc, sync::Arc};
 
 use dioxus_core::ScopeId;
 use im_rc::HashSet;
 
 use crate::Readable;
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
-pub struct AtomId {
-    pub ptr: *const (),
-    pub type_id: TypeId,
-}
+pub type AtomId = *const ();
 
 pub struct AtomRoot {
     pub atoms: RefCell<HashMap<AtomId, Slot>>,
@@ -54,15 +44,7 @@ impl AtomRoot {
         // initialize the value if it's not already initialized
         if let Some(slot) = atoms.get_mut(&f.unique_id()) {
             slot.subscribers.insert(scope);
-            match slot.value.clone().downcast() {
-                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>()
-                ),
-            }
+            slot.value.clone().downcast().unwrap()
         } else {
             let value = Rc::new(f.init());
             let mut subscribers = HashSet::new();

+ 23 - 7
packages/hooks/src/useeffect.rs

@@ -9,17 +9,33 @@ use crate::UseFutureDep;
 /// If a future is pending when the dependencies change, the previous future
 /// will be allowed to continue
 ///
-/// - dependencies: a tuple of references to values that are PartialEq + Clone
+/// - dependencies: a tuple of references to values that are `PartialEq` + `Clone`
 ///
 /// ## Examples
 ///
-/// ```rust, ignore
-///
+/// ```rust, no_run
 /// #[inline_props]
-/// fn app(cx: Scope, name: &str) -> Element {
-///     use_effect(cx, (name,), |(name,)| async move {
-///         set_title(name);
-///     }))
+/// fn Profile(cx: Scope, id: usize) -> Element {
+///     let name = use_state(cx, || None);
+///
+///     // Only fetch the user data when the id changes.
+///     use_effect(cx, (id,), |(id,)| {
+///         to_owned![name];
+///         async move {
+///             let user = fetch_user(id).await;
+///             name.set(user.name);
+///         }
+///     });
+///
+///     let name = name.get().clone().unwrap_or("Loading...".to_string());
+///
+///     render!(
+///         p { "{name}" }
+///     )
+/// }
+///
+/// fn app(cx: Scope) -> Element {
+///     render!(Profile { id: 0 })
 /// }
 /// ```
 pub fn use_effect<T, F, D>(cx: &ScopeState, dependencies: D, future: impl FnOnce(D::Out) -> F)

+ 15 - 9
packages/hooks/src/usememo.rs

@@ -2,21 +2,27 @@ use dioxus_core::ScopeState;
 
 use crate::UseFutureDep;
 
-/// A hook that provides a callback that executes after the hooks have been applied
+/// A hook that provides a callback that executes if the dependencies change.
+/// This is useful to avoid running computation-expensive calculations even when the data doesn't change.
 ///
-/// Whenever the hooks dependencies change, the callback will be re-evaluated.
-///
-/// - dependencies: a tuple of references to values that are PartialEq + Clone
+/// - dependencies: a tuple of references to values that are `PartialEq` + `Clone`
 ///
 /// ## Examples
 ///
-/// ```rust, ignore
+/// ```rust, no_run
 ///
 /// #[inline_props]
-/// fn app(cx: Scope, name: &str) -> Element {
-///     use_memo(cx, (name,), |(name,)| {
-///         expensive_computation(name);
-///     }))
+/// fn Calculator(cx: Scope, number: usize) -> Element {
+///     let bigger_number = use_memo(cx, (number,), |(number,)| {
+///         // This will only be calculated when `number` has changed.
+///         number * 100
+///     });
+///     render!(
+///         p { "{bigger_number}" }
+///     )
+/// }
+/// fn app(cx: Scope) -> Element {
+///     render!(Calculator { number: 0 })
 /// }
 /// ```
 pub fn use_memo<T, D>(cx: &ScopeState, dependencies: D, callback: impl FnOnce(D::Out) -> T) -> &T

+ 15 - 15
packages/hooks/src/useref.rs

@@ -16,7 +16,7 @@ use std::{
 /// writes through the `write` method. Whenever `write` is called, the component
 /// that initialized the hook will be marked as "dirty".
 ///
-/// ```rust, ignore
+/// ```rust, no_run
 /// let val = use_ref(|| HashMap::<u32, String>::new());
 ///
 /// // using `write` will give us a `RefMut` to the inner value, which we can call methods on
@@ -26,7 +26,7 @@ use std::{
 ///
 /// You can avoid this default behavior with `write_silent`
 ///
-/// ```ignore
+/// ```rust, no_run
 /// // with `write_silent`, the component will not be re-rendered
 /// val.write_silent().insert(2, "goodbye".to_string());
 /// ```
@@ -35,7 +35,7 @@ use std::{
 ///
 /// To read values out of the refcell, you can use the `read` method which will retrun a `Ref`.
 ///
-/// ```rust, ignore
+/// ```rust, no_run
 /// let map: Ref<_> = val.read();
 ///
 /// let item = map.get(&1);
@@ -43,7 +43,7 @@ use std::{
 ///
 /// To get an &T out of the RefCell, you need to "reborrow" through the Ref:
 ///
-/// ```rust, ignore
+/// ```rust, no_run
 /// let read = val.read();
 /// let map = &*read;
 /// ```
@@ -54,10 +54,10 @@ use std::{
 /// Typically this will be a collection like a HashMap or a Vec. To create new
 /// elements from the collection, we can use `read()` directly in our rsx!.
 ///
-/// ```rust, ignore
+/// ```rust, no_run
 /// rsx!{
 ///     val.read().iter().map(|(k, v)| {
-///         rsx!{ key: "{k}", value: "{v}" }
+///         rsx!{ key: "{k}", "{v}" }
 ///     })
 /// }
 /// ```
@@ -66,9 +66,9 @@ use std::{
 /// "render" inside the iterator. For some cases you might need to collect into
 /// a temporary Vec.
 ///
-/// ```rust, ignore
+/// ```rust, no_run
 /// let items = val.read().iter().map(|(k, v)| {
-///     cx.render(rsx!{ key: "{k}", value: "{v}" })
+///     cx.render(rsx!{ key: "{k}", "{v}" })
 /// });
 ///
 /// // collect into a Vec
@@ -80,9 +80,9 @@ use std::{
 ///
 /// To access values from a `UseRef` in an async context, you need to detach it
 /// from the current scope's lifetime, making it a `'static` value. This is done
-/// by simply calling `ToOnwed` or `Clone`.
+/// by simply calling `to_owned` or `clone`.
 ///
-/// ```rust, ignore
+/// ```rust, no_run
 /// let val = use_ref(|| HashMap::<u32, String>::new());
 ///
 /// cx.spawn({
@@ -95,15 +95,15 @@ use std::{
 /// ```
 ///
 /// If you're working with lots of values like UseState and UseRef, you can use the
-/// `clone!` macro to make it easier to write the above code.
+/// `to_owned!` macro to make it easier to write the above code.
 ///
-/// ```rust, ignore
+/// ```rust, no_run
 /// let val1 = use_ref(|| HashMap::<u32, String>::new());
 /// let val2 = use_ref(|| HashMap::<u32, String>::new());
 /// let val3 = use_ref(|| HashMap::<u32, String>::new());
 ///
 /// cx.spawn({
-///     clone![val1, val2, val3];
+///     to_owned![val1, val2, val3];
 ///     async move {
 ///         some_work().await;
 ///         val.write().insert(1, "hello".to_string());
@@ -194,7 +194,7 @@ impl<T> UseRef<T> {
     /// Note: You can always "reborrow" the value through the RefCell.
     /// This method just does it for you automatically.
     ///
-    /// ```rust, ignore
+    /// ```rust, no_run
     /// let val = use_ref(|| HashMap::<u32, String>::new());
     ///
     ///
@@ -214,7 +214,7 @@ impl<T> UseRef<T> {
     /// Note: You can always "reborrow" the value through the RefCell.
     /// This method just does it for you automatically.
     ///
-    /// ```rust, ignore
+    /// ```rust, no_run
     /// let val = use_ref(|| HashMap::<u32, String>::new());
     ///
     ///

+ 0 - 0
playwrite-tests/fullstack.spec.js → playwright-tests/fullstack.spec.js


+ 0 - 0
playwrite-tests/fullstack/.gitignore → playwright-tests/fullstack/.gitignore


+ 1 - 1
playwrite-tests/fullstack/Cargo.toml → playwright-tests/fullstack/Cargo.toml

@@ -1,5 +1,5 @@
 [package]
-name = "dioxus-playwrite-fullstack-test"
+name = "dioxus-playwright-fullstack-test"
 version = "0.1.0"
 edition = "2021"
 publish = false

+ 1 - 1
playwrite-tests/fullstack/src/main.rs → playwright-tests/fullstack/src/main.rs

@@ -1,4 +1,4 @@
-// This test is used by playwrite configured in the root of the repo
+// This test is used by playwright configured in the root of the repo
 // Tests:
 // - Server functions
 // - SSR

+ 0 - 0
playwrite-tests/liveview.spec.js → playwright-tests/liveview.spec.js


+ 2 - 2
playwrite-tests/liveview/Cargo.toml → playwright-tests/liveview/Cargo.toml

@@ -1,8 +1,8 @@
 [package]
-name = "dioxus-playwrite-liveview-test"
+name = "dioxus-playwright-liveview-test"
 version = "0.0.1"
 edition = "2021"
-description = "Playwrite test for Dioxus Liveview"
+description = "Playwright test for Dioxus Liveview"
 license = "MIT/Apache-2.0"
 publish = false
 

+ 1 - 1
playwrite-tests/liveview/src/main.rs → playwright-tests/liveview/src/main.rs

@@ -1,4 +1,4 @@
-// This test is used by playwrite configured in the root of the repo
+// This test is used by playwright configured in the root of the repo
 
 use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router};
 use dioxus::prelude::*;

+ 0 - 0
playwrite-tests/web.spec.js → playwright-tests/web.spec.js


+ 2 - 2
playwrite-tests/web/Cargo.toml → playwright-tests/web/Cargo.toml

@@ -1,8 +1,8 @@
 [package]
-name = "dioxus-playwrite-web-test"
+name = "dioxus-playwright-web-test"
 version = "0.0.1"
 edition = "2021"
-description = "Playwrite test for Dioxus Web"
+description = "Playwright test for Dioxus Web"
 license = "MIT/Apache-2.0"
 publish = false
 

+ 1 - 1
playwrite-tests/web/src/main.rs → playwright-tests/web/src/main.rs

@@ -1,4 +1,4 @@
-// This test is used by playwrite configured in the root of the repo
+// This test is used by playwright configured in the root of the repo
 
 use dioxus::prelude::*;
 use dioxus_web::use_eval;

+ 13 - 12
playwright.config.js

@@ -1,6 +1,6 @@
 // @ts-check
-const { defineConfig, devices } = require('@playwright/test');
-const path = require('path');
+const { defineConfig, devices } = require("@playwright/test");
+const path = require("path");
 
 /**
  * Read environment variables from file.
@@ -12,7 +12,7 @@ const path = require('path');
  * @see https://playwright.dev/docs/test-configuration
  */
 module.exports = defineConfig({
-  testDir: './playwrite-tests',
+  testDir: "./playwright-tests",
   /* Run tests in files in parallel */
   fullyParallel: true,
   /* Fail the build on CI if you accidentally left test.only in the source code. */
@@ -22,21 +22,21 @@ module.exports = defineConfig({
   /* Opt out of parallel tests on CI. */
   workers: process.env.CI ? 1 : undefined,
   /* Reporter to use. See https://playwright.dev/docs/test-reporters */
-  reporter: 'html',
+  reporter: "html",
   /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
   use: {
     /* Base URL to use in actions like `await page.goto('/')`. */
     // baseURL: 'http://127.0.0.1:3000',
 
     /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
-    trace: 'on-first-retry',
+    trace: "on-first-retry",
   },
 
   /* Configure projects for major browsers */
   projects: [
     {
-      name: 'chromium',
-      use: { ...devices['Desktop Chrome'] },
+      name: "chromium",
+      use: { ...devices["Desktop Chrome"] },
     },
 
     // {
@@ -73,15 +73,16 @@ module.exports = defineConfig({
   /* Run your local dev server before starting the tests */
   webServer: [
     {
-      command: 'cargo run --package dioxus-playwrite-liveview-test --bin dioxus-playwrite-liveview-test',
+      command:
+        "cargo run --package dioxus-playwright-liveview-test --bin dioxus-playwright-liveview-test",
       port: 3030,
       timeout: 10 * 60 * 1000,
       reuseExistingServer: !process.env.CI,
       stdout: "pipe",
     },
     {
-      cwd: path.join(process.cwd(), 'playwrite-tests', 'web'),
-      command: 'dioxus serve',
+      cwd: path.join(process.cwd(), "playwright-tests", "web"),
+      command: "dioxus serve",
       port: 8080,
       timeout: 10 * 60 * 1000,
       reuseExistingServer: !process.env.CI,
@@ -89,11 +90,11 @@ module.exports = defineConfig({
     },
     {
       cwd: path.join(process.cwd(), 'playwrite-tests', 'fullstack'),
-      command: 'dioxus build --features web --release\ncargo run --release --features ssr --no-default-features',
+      command: 'dioxus build --features web --release\ncargo run --release --features ssr',
       port: 3333,
       timeout: 10 * 60 * 1000,
       reuseExistingServer: !process.env.CI,
       stdout: "pipe",
-    }
+    },
   ],
 });