diff --git a/.github/workflows/docs stable.yml b/.github/workflows/docs stable.yml index 98245fa89..a2d4e2c07 100644 --- a/.github/workflows/docs stable.yml +++ b/.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. diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d209c0c1a..dafe0384d 100644 --- a/.github/workflows/docs.yml +++ b/.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. diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index b80a7b1f5..eef04d521 100644 --- a/.github/workflows/macos.yml +++ b/.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 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b44aab843..b37e63aa4 100644 --- a/.github/workflows/main.yml +++ b/.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] diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 604562f6a..84891f403 100644 --- a/.github/workflows/playwright.yml +++ b/.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 diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 0dd8a1842..368df1630 100644 --- a/.github/workflows/windows.yml +++ b/.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 diff --git a/.gitignore b/.gitignore index bca0fe28f..b8025753e 100644 --- a/.gitignore +++ b/.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 diff --git a/Cargo.toml b/Cargo.toml index 880e9b129..81280d7df 100644 --- a/Cargo.toml +++ b/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 diff --git a/docs/guide/src/en/SUMMARY.md b/docs/guide/src/en/SUMMARY.md index 62ef5adf7..a44439200 100644 --- a/docs/guide/src/en/SUMMARY.md +++ b/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) diff --git a/docs/guide/src/en/__unused/advanced-guides/12-signals.md b/docs/guide/src/en/__unused/advanced-guides/12-signals.md index df13b91d8..7fbb56de7 100644 --- a/docs/guide/src/en/__unused/advanced-guides/12-signals.md +++ b/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 = || "".to_string(); +const TITLE: Atom = 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 = |_| {}; +const DICT: AtomFamily = 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. diff --git a/docs/guide/src/en/async/use_coroutine.md b/docs/guide/src/en/async/use_coroutine.md index 8d4b8f99a..d42905be5 100644 --- a/docs/guide/src/en/async/use_coroutine.md +++ b/docs/guide/src/en/async/use_coroutine.md @@ -143,7 +143,7 @@ async fn editor_service(rx: UnboundedReceiver) { 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 = |_| "default".to_string(); +static USERNAME: Atom = 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, 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 { diff --git a/docs/guide/src/en/async/use_effect.md b/docs/guide/src/en/async/use_effect.md new file mode 100644 index 000000000..7c20e3589 --- /dev/null +++ b/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 }) +} +``` diff --git a/docs/guide/src/en/best_practices/error_handling.md b/docs/guide/src/en/best_practices/error_handling.md index fad8f1c10..d16d04347 100644 --- a/docs/guide/src/en/best_practices/error_handling.md +++ b/docs/guide/src/en/best_practices/error_handling.md @@ -113,14 +113,14 @@ enum InputError { TooShort, } -static INPUT_ERROR: Atom = |_| InputError::None; +static INPUT_ERROR: Atom = 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 { diff --git a/docs/guide/src/en/interactivity/memoization.md b/docs/guide/src/en/interactivity/memoization.md new file mode 100644 index 000000000..32ec37ce5 --- /dev/null +++ b/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 }) +} +``` diff --git a/docs/guide/src/en/interactivity/sharing_state.md b/docs/guide/src/en/interactivity/sharing_state.md index 4c97e53bf..d71fbdec0 100644 --- a/docs/guide/src/en/interactivity/sharing_state.md +++ b/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>` here. If the context has been provided, the value is `Some(UseSharedState)`, which you can call `.read` or `.write` on, similarly to `UseRef`. Otherwise, the value is `None`. +> `use_shared_state` returns `Option>` here. If the context has been provided, the value is `Some(UseSharedState)`, 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): diff --git a/docs/guide/src/pt-br/async/use_coroutine.md b/docs/guide/src/pt-br/async/use_coroutine.md index d7edbd8cc..cb9eda3a0 100644 --- a/docs/guide/src/pt-br/async/use_coroutine.md +++ b/docs/guide/src/pt-br/async/use_coroutine.md @@ -105,7 +105,7 @@ async fn editor_service(rx: UnboundedReceiver) { 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 = |_| "default".to_string(); +static USERNAME: Atom = 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, 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 { diff --git a/docs/guide/src/pt-br/best_practices/error_handling.md b/docs/guide/src/pt-br/best_practices/error_handling.md index 7e027e9d0..2fec8d88d 100644 --- a/docs/guide/src/pt-br/best_practices/error_handling.md +++ b/docs/guide/src/pt-br/best_practices/error_handling.md @@ -113,14 +113,14 @@ enum InputError { TooShort, } -static INPUT_ERROR: Atom = |_| InputError::None; +static INPUT_ERROR: Atom = 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 { diff --git a/examples/fermi.rs b/examples/fermi.rs index 001890892..a15fc807f 100644 --- a/examples/fermi.rs +++ b/examples/fermi.rs @@ -7,11 +7,11 @@ fn main() { dioxus_desktop::launch(app) } -static NAME: Atom = |_| "world".to_string(); +static NAME: Atom = 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!["world".to_string()]; +static NAMES: AtomRef> = 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 { diff --git a/examples/shared_state.rs b/examples/shared_state.rs new file mode 100644 index 000000000..d129f75d1 --- /dev/null +++ b/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, +} + +impl CoolData { + pub fn new(data: HashMap) -> 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::(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::(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}" + }) +} diff --git a/packages/cli/src/config.rs b/packages/cli/src/config.rs index f9117f6e5..79e6c7321 100644 --- a/packages/cli/src/config.rs +++ b/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> { 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::(&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 { +fn acquire_dioxus_toml(dir: &Path) -> Option { // 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 diff --git a/packages/cli/src/main.rs b/packages/cli/src/main.rs index f2007aa43..86e90db96 100644 --- a/packages/cli/src/main.rs +++ b/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() diff --git a/packages/fermi/README.md b/packages/fermi/README.md index f276dbff5..ff6635b0f 100644 --- a/packages/fermi/README.md +++ b/packages/fermi/README.md @@ -1,4 +1,3 @@ -

Fermi âš›

@@ -6,7 +5,6 @@

- ------ +--- 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 = |_| 0; +static COUNT: Atom = 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 diff --git a/packages/fermi/src/atoms/atom.rs b/packages/fermi/src/atoms/atom.rs index 81e04ce0b..bba1f532f 100644 --- a/packages/fermi/src/atoms/atom.rs +++ b/packages/fermi/src/atoms/atom.rs @@ -1,24 +1,21 @@ use crate::{AtomId, AtomRoot, Readable, Writable}; -pub type Atom = fn(AtomBuilder) -> T; +pub struct Atom(pub fn(AtomBuilder) -> T); pub struct AtomBuilder; -impl Readable for Atom { +impl Readable for &'static Atom { fn read(&self, _root: AtomRoot) -> Option { 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::(), - } + *self as *const Atom as *const () } } -impl Writable for Atom { +impl Writable for &'static Atom { fn write(&self, _root: AtomRoot, _value: V) { todo!() } @@ -26,6 +23,22 @@ impl Writable for Atom { #[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> = Atom(|_| Vec::new()); + static TEST_ATOM_2: Atom> = Atom(|_| Vec::new()); + assert_ne!((&TEST_ATOM_1).unique_id(), (&TEST_ATOM_2).unique_id()); } diff --git a/packages/fermi/src/atoms/atomfamily.rs b/packages/fermi/src/atoms/atomfamily.rs index 29cecd4b2..45f6b8feb 100644 --- a/packages/fermi/src/atoms/atomfamily.rs +++ b/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 = fn(AtomFamilyBuilder) -> ImMap; +pub struct AtomFamily(pub fn(AtomFamilyBuilder) -> ImMap); -impl Readable> for AtomFamily { +impl Readable> for &'static AtomFamily { fn read(&self, _root: AtomRoot) -> Option> { todo!() } fn init(&self) -> ImMap { - (*self)(AtomFamilyBuilder) + self.0(AtomFamilyBuilder) } fn unique_id(&self) -> AtomId { - AtomId { - ptr: *self as *const (), - type_id: std::any::TypeId::of::(), - } + *self as *const AtomFamily as *const () } } -impl Writable> for AtomFamily { +impl Writable> for &'static AtomFamily { fn write(&self, _root: AtomRoot, _value: ImMap) { todo!() } diff --git a/packages/fermi/src/atoms/atomref.rs b/packages/fermi/src/atoms/atomref.rs index c103fa662..5e863ade2 100644 --- a/packages/fermi/src/atoms/atomref.rs +++ b/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 = fn(AtomRefBuilder) -> T; +pub struct AtomRef(pub fn(AtomRefBuilder) -> T); -impl Readable> for AtomRef { +impl Readable> for &'static AtomRef { fn read(&self, _root: AtomRoot) -> Option> { todo!() } fn init(&self) -> RefCell { - 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::(), - } + *self as *const AtomRef as *const () } } #[test] fn atom_compiles() { - static TEST_ATOM: AtomRef> = |_| vec![]; - dbg!(TEST_ATOM.init()); + static TEST_ATOM: AtomRef> = AtomRef(|_| vec![]); + dbg!((&TEST_ATOM).init()); } diff --git a/packages/fermi/src/hooks/atom_ref.rs b/packages/fermi/src/hooks/atom_ref.rs index 95732c491..74dd9268b 100644 --- a/packages/fermi/src/hooks/atom_ref.rs +++ b/packages/fermi/src/hooks/atom_ref.rs @@ -13,7 +13,10 @@ use std::{ /// /// /// -pub fn use_atom_ref(cx: &ScopeState, atom: AtomRef) -> &UseAtomRef { +pub fn use_atom_ref<'a, T: 'static>( + cx: &'a ScopeState, + atom: &'static AtomRef, +) -> &'a UseAtomRef { let root = use_atom_root(cx); &cx.use_hook(|| { diff --git a/packages/fermi/src/hooks/state.rs b/packages/fermi/src/hooks/state.rs index 15310cff2..071b18059 100644 --- a/packages/fermi/src/hooks/state.rs +++ b/packages/fermi/src/hooks/state.rs @@ -19,7 +19,7 @@ use std::{ /// static COUNT: Atom = |_| 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 { diff --git a/packages/fermi/src/root.rs b/packages/fermi/src/root.rs index 99e97bfbb..f77930c75 100644 --- a/packages/fermi/src/root.rs +++ b/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>, @@ -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::() - ), - } + slot.value.clone().downcast().unwrap() } else { let value = Rc::new(f.init()); let mut subscribers = HashSet::new(); diff --git a/packages/hooks/src/useeffect.rs b/packages/hooks/src/useeffect.rs index f9ef70b84..aec1f9c9a 100644 --- a/packages/hooks/src/useeffect.rs +++ b/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(cx: &ScopeState, dependencies: D, future: impl FnOnce(D::Out) -> F) diff --git a/packages/hooks/src/usememo.rs b/packages/hooks/src/usememo.rs index 8cc5540e6..d3d991ccc 100644 --- a/packages/hooks/src/usememo.rs +++ b/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(cx: &ScopeState, dependencies: D, callback: impl FnOnce(D::Out) -> T) -> &T diff --git a/packages/hooks/src/useref.rs b/packages/hooks/src/useref.rs index 1865dadb3..cf1b0306f 100644 --- a/packages/hooks/src/useref.rs +++ b/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::::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::::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::::new()); /// let val2 = use_ref(|| HashMap::::new()); /// let val3 = use_ref(|| HashMap::::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 UseRef { /// 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::::new()); /// /// @@ -214,7 +214,7 @@ impl UseRef { /// 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::::new()); /// /// diff --git a/playwrite-tests/fullstack.spec.js b/playwright-tests/fullstack.spec.js similarity index 100% rename from playwrite-tests/fullstack.spec.js rename to playwright-tests/fullstack.spec.js diff --git a/playwrite-tests/fullstack/.gitignore b/playwright-tests/fullstack/.gitignore similarity index 100% rename from playwrite-tests/fullstack/.gitignore rename to playwright-tests/fullstack/.gitignore diff --git a/playwrite-tests/fullstack/Cargo.toml b/playwright-tests/fullstack/Cargo.toml similarity index 93% rename from playwrite-tests/fullstack/Cargo.toml rename to playwright-tests/fullstack/Cargo.toml index a11bfa461..9cca0d2fa 100644 --- a/playwrite-tests/fullstack/Cargo.toml +++ b/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 diff --git a/playwrite-tests/fullstack/src/main.rs b/playwright-tests/fullstack/src/main.rs similarity index 97% rename from playwrite-tests/fullstack/src/main.rs rename to playwright-tests/fullstack/src/main.rs index b453d3f47..f7cc2e039 100644 --- a/playwrite-tests/fullstack/src/main.rs +++ b/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 diff --git a/playwrite-tests/liveview.spec.js b/playwright-tests/liveview.spec.js similarity index 100% rename from playwrite-tests/liveview.spec.js rename to playwright-tests/liveview.spec.js diff --git a/playwrite-tests/liveview/Cargo.toml b/playwright-tests/liveview/Cargo.toml similarity index 77% rename from playwrite-tests/liveview/Cargo.toml rename to playwright-tests/liveview/Cargo.toml index 8c4cad1ef..173ae1d81 100644 --- a/playwrite-tests/liveview/Cargo.toml +++ b/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 diff --git a/playwrite-tests/liveview/src/main.rs b/playwright-tests/liveview/src/main.rs similarity index 96% rename from playwrite-tests/liveview/src/main.rs rename to playwright-tests/liveview/src/main.rs index 42bd9a3e3..d28e3b7fd 100644 --- a/playwrite-tests/liveview/src/main.rs +++ b/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::*; diff --git a/playwrite-tests/web.spec.js b/playwright-tests/web.spec.js similarity index 100% rename from playwrite-tests/web.spec.js rename to playwright-tests/web.spec.js diff --git a/playwrite-tests/web/Cargo.toml b/playwright-tests/web/Cargo.toml similarity index 72% rename from playwrite-tests/web/Cargo.toml rename to playwright-tests/web/Cargo.toml index b1becd86e..22f4f4096 100644 --- a/playwrite-tests/web/Cargo.toml +++ b/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 diff --git a/playwrite-tests/web/src/main.rs b/playwright-tests/web/src/main.rs similarity index 95% rename from playwrite-tests/web/src/main.rs rename to playwright-tests/web/src/main.rs index d1881539c..c4850e943 100644 --- a/playwrite-tests/web/src/main.rs +++ b/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; diff --git a/playwright.config.js b/playwright.config.js index 8e64e14b1..978979b9c 100644 --- a/playwright.config.js +++ b/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", - } + }, ], });