瀏覽代碼

Merge branch 'master' into binary-protocal

ealmloff 1 年之前
父節點
當前提交
856fb1874e

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

@@ -20,7 +20,7 @@ jobs:
     steps:
       # Do our best to cache the toolchain and node install steps
       - uses: actions/checkout@v4
-      - uses: actions/setup-node@v3
+      - uses: actions/setup-node@v4
         with:
           node-version: 16
       - name: Install Rust

+ 1 - 0
README.md

@@ -159,6 +159,7 @@ So... Dioxus is great, but why won't it work for me?
 
 
 ## Contributing
+- Check out the website [section on contributing](https://dioxuslabs.com/learn/0.4/contributing).
 - Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
 - Join the discord and ask questions!
 

+ 1 - 1
packages/cli/src/assets/dioxus.toml

@@ -23,7 +23,7 @@ title = "Dioxus | An elegant GUI library for Rust"
 
 index_on_404 = true
 
-watch_path = ["src"]
+watch_path = ["src", "examples"]
 
 # include `assets` in web platform
 [web.resource]

+ 1 - 0
packages/cli/src/builder.rs

@@ -254,6 +254,7 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
     let mut cmd = subprocess::Exec::cmd("cargo")
         .cwd(&config.crate_dir)
         .arg("build")
+        .arg("--quiet")
         .arg("--message-format=json");
 
     if config.release {

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

@@ -105,7 +105,7 @@ impl Default for DioxusConfig {
                 },
                 proxy: Some(vec![]),
                 watcher: WebWatcherConfig {
-                    watch_path: Some(vec![PathBuf::from("src")]),
+                    watch_path: Some(vec![PathBuf::from("src"), PathBuf::from("examples")]),
                     reload_html: Some(false),
                     index_on_404: Some(true),
                 },

+ 7 - 7
packages/cli/src/server/mod.rs

@@ -32,7 +32,7 @@ async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
         .watcher
         .watch_path
         .clone()
-        .unwrap_or_else(|| vec![PathBuf::from("src")]);
+        .unwrap_or_else(|| vec![PathBuf::from("src"), PathBuf::from("examples")]);
 
     let watcher_config = config.clone();
     let mut watcher = notify::recommended_watcher(move |info: notify::Result<notify::Event>| {
@@ -121,12 +121,12 @@ async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
     .unwrap();
 
     for sub_path in allow_watch_path {
-        watcher
-            .watch(
-                &config.crate_dir.join(sub_path),
-                notify::RecursiveMode::Recursive,
-            )
-            .unwrap();
+        if let Err(err) = watcher.watch(
+            &config.crate_dir.join(sub_path),
+            notify::RecursiveMode::Recursive,
+        ) {
+            log::error!("Failed to watch path: {}", err);
+        }
     }
     Ok(watcher)
 }

+ 1 - 0
packages/core-macro/Cargo.toml

@@ -19,6 +19,7 @@ syn = { version = "2.0", features = ["full", "extra-traits"] }
 dioxus-rsx = { workspace = true }
 dioxus-core = { workspace = true }
 constcat = "0.3.0"
+prettyplease = "0.2.15"
 
 # testing
 [dev-dependencies]

+ 292 - 146
packages/core-macro/src/component_body_deserializers/inline_props.rs

@@ -30,166 +30,312 @@ impl ToTokens for InlinePropsDeserializerOutput {
 impl DeserializerArgs<InlinePropsDeserializerOutput> for InlinePropsDeserializerArgs {
     fn to_output(&self, component_body: &ComponentBody) -> Result<InlinePropsDeserializerOutput> {
         Ok(InlinePropsDeserializerOutput {
-            comp_fn: Self::get_function(component_body),
-            props_struct: Self::get_props_struct(component_body),
+            comp_fn: get_function(component_body),
+            props_struct: get_props_struct(component_body),
         })
     }
 }
 
-impl InlinePropsDeserializerArgs {
-    fn get_props_struct(component_body: &ComponentBody) -> ItemStruct {
-        let ComponentBody { item_fn, .. } = component_body;
-        let ItemFn { vis, sig, .. } = item_fn;
-        let Signature {
-            inputs,
-            ident: fn_ident,
-            generics,
-            ..
-        } = sig;
-
-        // Skip first arg since that's the context
-        let struct_fields = inputs.iter().skip(1).map(move |f| {
-            match f {
-                FnArg::Receiver(_) => unreachable!(), // Unreachable because of ComponentBody parsing
-                FnArg::Typed(pt) => {
-                    let arg_pat = &pt.pat; // Pattern (identifier)
-                    let arg_colon = &pt.colon_token;
-                    let arg_ty = &pt.ty; // Type
-                    let arg_attrs = &pt.attrs; // Attributes
-
-                    quote! {
-                        #(#arg_attrs)
-                        *
-                        #vis #arg_pat #arg_colon #arg_ty
-                    }
+fn get_props_struct(component_body: &ComponentBody) -> ItemStruct {
+    let ComponentBody { item_fn, .. } = component_body;
+    let ItemFn { vis, sig, .. } = item_fn;
+    let Signature {
+        inputs,
+        ident: fn_ident,
+        generics,
+        ..
+    } = sig;
+
+    // Skip first arg since that's the context
+    let struct_fields = inputs.iter().skip(1).map(move |f| {
+        match f {
+            FnArg::Receiver(_) => unreachable!(), // Unreachable because of ComponentBody parsing
+            FnArg::Typed(pt) => {
+                let arg_pat = &pt.pat; // Pattern (identifier)
+                let arg_colon = &pt.colon_token;
+                let arg_ty = &pt.ty; // Type
+                let arg_attrs = &pt.attrs; // Attributes
+
+                quote! {
+                    #(#arg_attrs)
+                    *
+                    #vis #arg_pat #arg_colon #arg_ty
                 }
             }
-        });
+        }
+    });
 
-        let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
-
-        let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
-            Some(lt)
-        } else {
-            None
-        };
-
-        let struct_attrs = if first_lifetime.is_some() {
-            quote! { #[derive(Props)] }
-        } else {
-            quote! { #[derive(Props, PartialEq)] }
-        };
-
-        let struct_generics = if first_lifetime.is_some() {
-            let struct_generics: Punctuated<GenericParam, Comma> = component_body
-                .item_fn
-                .sig
-                .generics
-                .params
-                .iter()
-                .map(|it| match it {
-                    GenericParam::Type(tp) => {
-                        let mut tp = tp.clone();
-                        tp.bounds.push(parse_quote!( 'a ));
-
-                        GenericParam::Type(tp)
-                    }
-                    _ => it.clone(),
-                })
-                .collect();
-
-            quote! { <#struct_generics> }
-        } else {
-            quote! { #generics }
-        };
-
-        parse_quote! {
-            #struct_attrs
-            #[allow(non_camel_case_types)]
-            #vis struct #struct_ident #struct_generics
-            {
-                #(#struct_fields),*
-            }
+    let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
+
+    let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
+        Some(lt)
+    } else {
+        None
+    };
+
+    let struct_attrs = if first_lifetime.is_some() {
+        quote! { #[derive(Props)] }
+    } else {
+        quote! { #[derive(Props, PartialEq)] }
+    };
+
+    let struct_generics = if first_lifetime.is_some() {
+        let struct_generics: Punctuated<GenericParam, Comma> = component_body
+            .item_fn
+            .sig
+            .generics
+            .params
+            .iter()
+            .map(|it| match it {
+                GenericParam::Type(tp) => {
+                    let mut tp = tp.clone();
+                    tp.bounds.push(parse_quote!( 'a ));
+
+                    GenericParam::Type(tp)
+                }
+                _ => it.clone(),
+            })
+            .collect();
+
+        quote! { <#struct_generics> }
+    } else {
+        quote! { #generics }
+    };
+
+    parse_quote! {
+        #struct_attrs
+        #[allow(non_camel_case_types)]
+        #vis struct #struct_ident #struct_generics
+        {
+            #(#struct_fields),*
         }
     }
+}
+
+fn get_props_docs(fn_ident: &Ident, inputs: Vec<&FnArg>) -> Vec<Attribute> {
+    if inputs.len() <= 1 {
+        return Vec::new();
+    }
 
-    fn get_function(component_body: &ComponentBody) -> ItemFn {
-        let ComponentBody {
-            item_fn,
-            cx_pat_type,
-            ..
-        } = component_body;
-        let ItemFn {
-            attrs: fn_attrs,
-            vis,
-            sig,
-            block: fn_block,
-        } = item_fn;
-        let Signature {
-            inputs,
-            ident: fn_ident,
-            generics,
-            output: fn_output,
-            asyncness,
-            ..
-        } = sig;
-        let Generics { where_clause, .. } = generics;
-
-        let cx_pat = &cx_pat_type.pat;
-        let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
-
-        // Skip first arg since that's the context
-        let struct_field_names = inputs.iter().skip(1).filter_map(|f| match f {
+    let arg_docs = inputs
+        .iter()
+        .filter_map(|f| match f {
             FnArg::Receiver(_) => unreachable!(), // ComponentBody prohibits receiver parameters.
-            FnArg::Typed(t) => Some(&t.pat),
+            FnArg::Typed(pt) => {
+                let arg_doc = pt
+                    .attrs
+                    .iter()
+                    .filter_map(|attr| {
+                        // TODO: Error reporting
+                        // Check if the path of the attribute is "doc"
+                        if !is_attr_doc(attr) {
+                            return None;
+                        };
+
+                        let Meta::NameValue(meta_name_value) = &attr.meta else {
+                            return None;
+                        };
+
+                        let Expr::Lit(doc_lit) = &meta_name_value.value else {
+                            return None;
+                        };
+
+                        let Lit::Str(doc_lit_str) = &doc_lit.lit else {
+                            return None;
+                        };
+
+                        Some(doc_lit_str.value())
+                    })
+                    .fold(String::new(), |mut doc, next_doc_line| {
+                        doc.push('\n');
+                        doc.push_str(&next_doc_line);
+                        doc
+                    });
+
+                Some((
+                    &pt.pat,
+                    &pt.ty,
+                    pt.attrs.iter().find_map(|attr| {
+                        if attr.path() != &parse_quote!(deprecated) {
+                            return None;
+                        }
+
+                        let res = crate::utils::DeprecatedAttribute::from_meta(&attr.meta);
+
+                        match res {
+                            Err(e) => panic!("{}", e.to_string()),
+                            Ok(v) => Some(v),
+                        }
+                    }),
+                    arg_doc,
+                ))
+            }
+        })
+        .collect::<Vec<_>>();
+
+    let mut props_docs = Vec::with_capacity(5);
+    let props_def_link = fn_ident.to_string() + "Props";
+    let header =
+        format!("# Props\n*For details, see the [props struct definition]({props_def_link}).*");
+
+    props_docs.push(parse_quote! {
+        #[doc = #header]
+    });
+
+    for (arg_name, arg_type, deprecation, input_arg_doc) in arg_docs {
+        let arg_name = arg_name.into_token_stream().to_string();
+        let arg_type = crate::utils::format_type_string(arg_type);
+
+        let input_arg_doc = keep_up_to_n_consecutive_chars(input_arg_doc.trim(), 2, '\n')
+            .replace("\n\n", "</p><p>");
+        let prop_def_link = format!("{props_def_link}::{arg_name}");
+        let mut arg_doc = format!("- [`{arg_name}`]({prop_def_link}) : `{arg_type}`");
+
+        if let Some(deprecation) = deprecation {
+            arg_doc.push_str("<p>👎 Deprecated");
+
+            if let Some(since) = deprecation.since {
+                arg_doc.push_str(&format!(" since {since}"));
+            }
+
+            if let Some(note) = deprecation.note {
+                let note = keep_up_to_n_consecutive_chars(&note, 1, '\n').replace('\n', " ");
+                let note = keep_up_to_n_consecutive_chars(&note, 1, '\t').replace('\t', " ");
+
+                arg_doc.push_str(&format!(": {note}"));
+            }
+
+            arg_doc.push_str("</p>");
+
+            if !input_arg_doc.is_empty() {
+                arg_doc.push_str("<hr/>");
+            }
+        }
+
+        if !input_arg_doc.is_empty() {
+            arg_doc.push_str(&format!("<p>{input_arg_doc}</p>"));
+        }
+
+        props_docs.push(parse_quote! {
+            #[doc = #arg_doc]
         });
+    }
+
+    props_docs
+}
+
+fn get_function(component_body: &ComponentBody) -> ItemFn {
+    let ComponentBody {
+        item_fn,
+        cx_pat_type,
+        ..
+    } = component_body;
+    let ItemFn {
+        attrs: fn_attrs,
+        vis,
+        sig,
+        block: fn_block,
+    } = item_fn;
+    let Signature {
+        inputs,
+        ident: fn_ident,
+        generics,
+        output: fn_output,
+        asyncness,
+        ..
+    } = sig;
+    let Generics { where_clause, .. } = generics;
+
+    let cx_pat = &cx_pat_type.pat;
+    let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
+
+    // Skip first arg since that's the context
+    let struct_field_names = inputs.iter().skip(1).filter_map(|f| match f {
+        FnArg::Receiver(_) => unreachable!(), // ComponentBody prohibits receiver parameters.
+        FnArg::Typed(pt) => Some(&pt.pat),
+    });
+
+    let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
+        Some(lt)
+    } else {
+        None
+    };
+
+    let (scope_lifetime, fn_generics) = if let Some(lt) = first_lifetime {
+        (quote! { #lt, }, generics.clone())
+    } else {
+        let lifetime: LifetimeParam = parse_quote! { 'a };
+
+        let mut fn_generics = generics.clone();
+        fn_generics
+            .params
+            .insert(0, GenericParam::Lifetime(lifetime.clone()));
 
-        let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
-            Some(lt)
-        } else {
-            None
-        };
-
-        let (scope_lifetime, fn_generics) = if let Some(lt) = first_lifetime {
-            (quote! { #lt, }, generics.clone())
-        } else {
-            let lifetime: LifetimeParam = parse_quote! { 'a };
-
-            let mut fn_generics = generics.clone();
-            fn_generics
-                .params
-                .insert(0, GenericParam::Lifetime(lifetime.clone()));
-
-            (quote! { #lifetime, }, fn_generics)
-        };
-
-        let generics_no_bounds = {
-            let mut generics = generics.clone();
-            generics.params = generics
-                .params
-                .iter()
-                .map(|it| match it {
-                    GenericParam::Type(tp) => {
-                        let mut tp = tp.clone();
-                        tp.bounds.clear();
-
-                        GenericParam::Type(tp)
-                    }
-                    _ => it.clone(),
-                })
-                .collect();
-
-            generics
-        };
-
-        parse_quote! {
-            #(#fn_attrs)*
-            #asyncness #vis fn #fn_ident #fn_generics (#cx_pat: Scope<#scope_lifetime #struct_ident #generics_no_bounds>) #fn_output
-            #where_clause
-            {
-                let #struct_ident { #(#struct_field_names),* } = &#cx_pat.props;
-                #fn_block
+        (quote! { #lifetime, }, fn_generics)
+    };
+
+    let generics_no_bounds = {
+        let mut generics = generics.clone();
+        generics.params = generics
+            .params
+            .iter()
+            .map(|it| match it {
+                GenericParam::Type(tp) => {
+                    let mut tp = tp.clone();
+                    tp.bounds.clear();
+
+                    GenericParam::Type(tp)
+                }
+                _ => it.clone(),
+            })
+            .collect();
+
+        generics
+    };
+
+    let props_docs = get_props_docs(fn_ident, inputs.iter().skip(1).collect());
+
+    parse_quote! {
+        #(#fn_attrs)*
+        #(#props_docs)*
+        #asyncness #vis fn #fn_ident #fn_generics (#cx_pat: Scope<#scope_lifetime #struct_ident #generics_no_bounds>) #fn_output
+        #where_clause
+        {
+            let #struct_ident { #(#struct_field_names),* } = &#cx_pat.props;
+            #fn_block
+        }
+    }
+}
+
+/// Checks if the attribute is a `#[doc]` attribute.
+fn is_attr_doc(attr: &Attribute) -> bool {
+    attr.path() == &parse_quote!(doc)
+}
+
+fn keep_up_to_n_consecutive_chars(
+    input: &str,
+    n_of_consecutive_chars_allowed: usize,
+    target_char: char,
+) -> String {
+    let mut output = String::new();
+    let mut prev_char: Option<char> = None;
+    let mut consecutive_count = 0;
+
+    for c in input.chars() {
+        match prev_char {
+            Some(prev) if c == target_char && prev == target_char => {
+                if consecutive_count < n_of_consecutive_chars_allowed {
+                    output.push(c);
+                    consecutive_count += 1;
+                }
+            }
+            _ => {
+                output.push(c);
+                prev_char = Some(c);
+                consecutive_count = 1;
             }
         }
     }
+
+    output
 }

+ 1 - 0
packages/core-macro/src/lib.rs

@@ -12,6 +12,7 @@ use syn::{parse_macro_input, Path, Token};
 mod component_body;
 mod component_body_deserializers;
 mod props;
+mod utils;
 
 // mod rsx;
 use crate::component_body::ComponentBody;

+ 8 - 5
packages/core-macro/src/props/mod.rs

@@ -701,6 +701,14 @@ Finally, call `.build()` to create the instance of `{name}`.
         }
 
         pub fn field_impl(&self, field: &FieldInfo) -> Result<TokenStream, Error> {
+            let FieldInfo {
+                name: field_name,
+                ty: field_type,
+                ..
+            } = field;
+            if *field_name == "key" {
+                return Err(Error::new_spanned(field_name, "Naming a prop `key` is not allowed because the name can conflict with the built in key attribute. See https://dioxuslabs.com/learn/0.4/reference/dynamic_rendering#rendering-lists for more information about keys"));
+            }
             let StructInfo {
                 ref builder_name, ..
             } = *self;
@@ -715,11 +723,6 @@ Finally, call `.build()` to create the instance of `{name}`.
             });
             let reconstructing = self.included_fields().map(|f| f.name);
 
-            let FieldInfo {
-                name: field_name,
-                ty: field_type,
-                ..
-            } = field;
             let mut ty_generics: Vec<syn::GenericArgument> = self
                 .generics
                 .params

+ 129 - 0
packages/core-macro/src/utils.rs

@@ -0,0 +1,129 @@
+use quote::ToTokens;
+use syn::parse::{Parse, ParseStream};
+use syn::spanned::Spanned;
+use syn::{parse_quote, Expr, Lit, Meta, Token, Type};
+
+const FORMATTED_TYPE_START: &str = "static TY_AFTER_HERE:";
+const FORMATTED_TYPE_END: &str = "= todo!();";
+
+/// Attempts to convert the given literal to a string.
+/// Converts ints and floats to their base 10 counterparts.
+///
+/// Returns `None` if the literal is [`Lit::Verbatim`] or if the literal is [`Lit::ByteStr`]
+/// and the byte string could not be converted to UTF-8.
+pub fn lit_to_string(lit: Lit) -> Option<String> {
+    match lit {
+        Lit::Str(l) => Some(l.value()),
+        Lit::ByteStr(l) => String::from_utf8(l.value()).ok(),
+        Lit::Byte(l) => Some(String::from(l.value() as char)),
+        Lit::Char(l) => Some(l.value().to_string()),
+        Lit::Int(l) => Some(l.base10_digits().to_string()),
+        Lit::Float(l) => Some(l.base10_digits().to_string()),
+        Lit::Bool(l) => Some(l.value().to_string()),
+        Lit::Verbatim(_) => None,
+        _ => None,
+    }
+}
+
+pub fn format_type_string(ty: &Type) -> String {
+    let ty_unformatted = ty.into_token_stream().to_string();
+    let ty_unformatted = ty_unformatted.trim();
+
+    // This should always be valid syntax.
+    // Not Rust code, but syntax, which is the only thing that `syn` cares about.
+    let Ok(file_unformatted) = syn::parse_file(&format!(
+        "{FORMATTED_TYPE_START}{ty_unformatted}{FORMATTED_TYPE_END}"
+    )) else {
+        return ty_unformatted.to_string();
+    };
+
+    let file_formatted = prettyplease::unparse(&file_unformatted);
+
+    let file_trimmed = file_formatted.trim();
+    let start_removed = file_trimmed.trim_start_matches(FORMATTED_TYPE_START);
+    let end_removed = start_removed.trim_end_matches(FORMATTED_TYPE_END);
+    let ty_formatted = end_removed.trim();
+
+    ty_formatted.to_string()
+}
+
+/// Represents the `#[deprecated]` attribute.
+///
+/// You can use the [`DeprecatedAttribute::from_meta`] function to try to parse an attribute to this struct.
+#[derive(Default)]
+pub struct DeprecatedAttribute {
+    pub since: Option<String>,
+    pub note: Option<String>,
+}
+
+impl DeprecatedAttribute {
+    /// Returns `None` if the given attribute was not a valid form of the `#[deprecated]` attribute.
+    pub fn from_meta(meta: &Meta) -> syn::Result<Self> {
+        if meta.path() != &parse_quote!(deprecated) {
+            return Err(syn::Error::new(
+                meta.span(),
+                "attribute path is not `deprecated`",
+            ));
+        }
+
+        match &meta {
+            Meta::Path(_) => Ok(Self::default()),
+            Meta::NameValue(name_value) => {
+                let Expr::Lit(expr_lit) = &name_value.value else {
+                    return Err(syn::Error::new(
+                        name_value.span(),
+                        "literal in `deprecated` value must be a string",
+                    ));
+                };
+
+                Ok(Self {
+                    since: None,
+                    note: lit_to_string(expr_lit.lit.clone()).map(|s| s.trim().to_string()),
+                })
+            }
+            Meta::List(list) => {
+                let parsed = list.parse_args::<DeprecatedAttributeArgsParser>()?;
+
+                Ok(Self {
+                    since: parsed.since.map(|s| s.trim().to_string()),
+                    note: parsed.note.map(|s| s.trim().to_string()),
+                })
+            }
+        }
+    }
+}
+
+mod kw {
+    use syn::custom_keyword;
+    custom_keyword!(since);
+    custom_keyword!(note);
+}
+
+struct DeprecatedAttributeArgsParser {
+    since: Option<String>,
+    note: Option<String>,
+}
+
+impl Parse for DeprecatedAttributeArgsParser {
+    fn parse(input: ParseStream) -> syn::Result<Self> {
+        let mut since: Option<String> = None;
+        let mut note: Option<String> = None;
+
+        if input.peek(kw::since) {
+            input.parse::<kw::since>()?;
+            input.parse::<Token![=]>()?;
+
+            since = lit_to_string(input.parse()?);
+        }
+
+        if input.peek(Token![,]) && input.peek2(kw::note) {
+            input.parse::<Token![,]>()?;
+            input.parse::<kw::note>()?;
+            input.parse::<Token![=]>()?;
+
+            note = lit_to_string(input.parse()?);
+        }
+
+        Ok(Self { since, note })
+    }
+}

+ 7 - 0
packages/desktop/src/lib.rs

@@ -166,6 +166,7 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
     // iOS panics if we create a window before the event loop is started
     let props = Rc::new(Cell::new(Some(props)));
     let cfg = Rc::new(Cell::new(Some(cfg)));
+    let mut is_visible_before_start = true;
 
     event_loop.run(move |window_event, event_loop, control_flow| {
         *control_flow = ControlFlow::Poll;
@@ -219,6 +220,8 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
                 // Create a dom
                 let dom = VirtualDom::new_with_props(root, props);
 
+                is_visible_before_start = cfg.window.window.visible;
+
                 let handler = create_new_window(
                     cfg,
                     event_loop,
@@ -335,6 +338,10 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
                 EventData::Ipc(msg) if msg.method() == "initialize" => {
                     let view = webviews.get_mut(&event.1).unwrap();
                     send_edits(view.dom.rebuild(), &view.desktop_context);
+                    view.desktop_context
+                        .webview
+                        .window()
+                        .set_visible(is_visible_before_start);
                 }
 
                 EventData::Ipc(msg) if msg.method() == "browser_open" => {

+ 1 - 1
packages/desktop/src/webview.rs

@@ -13,7 +13,7 @@ pub(crate) fn build(
     proxy: EventLoopProxy<UserWindowEvent>,
 ) -> (WebView, WebContext, EditQueue) {
     let builder = cfg.window.clone();
-    let window = builder.build(event_loop).unwrap();
+    let window = builder.with_visible(false).build(event_loop).unwrap();
     let file_handler = cfg.file_drop_handler.take();
     let custom_head = cfg.custom_head.clone();
     let index_file = cfg.custom_index.clone();

+ 9 - 2
packages/fullstack/src/launch.rs

@@ -121,8 +121,15 @@ impl<Props: Clone + serde::Serialize + serde::de::DeserializeOwned + Send + Sync
     #[cfg(feature = "web")]
     /// Launch the web application
     pub fn launch_web(self) {
-        let cfg = self.web_cfg.hydrate(true);
-        dioxus_web::launch_with_props(self.component, get_root_props_from_document().unwrap(), cfg);
+        #[cfg(not(feature = "ssr"))]
+        {
+            let cfg = self.web_cfg.hydrate(true);
+            dioxus_web::launch_with_props(
+                self.component,
+                get_root_props_from_document().unwrap(),
+                cfg,
+            );
+        }
     }
 
     #[cfg(feature = "desktop")]

+ 7 - 0
packages/fullstack/src/lib.rs

@@ -64,3 +64,10 @@ pub mod prelude {
 
     pub use hooks::{server_cached::server_cached, server_future::use_server_future};
 }
+
+// Warn users about overlapping features
+#[cfg(all(feature = "ssr", feature = "web"))]
+compile_error!("The `ssr` feature (enabled by `warp`, `axum`, or `salvo`) and `web` feature are overlapping. Please choose one or the other.");
+
+#[cfg(all(feature = "ssr", feature = "desktop"))]
+compile_error!("The `ssr` feature (enabled by `warp`, `axum`, or `salvo`) and `desktop` feature are overlapping. Please choose one or the other.");

+ 5 - 0
packages/router/Cargo.toml

@@ -19,6 +19,7 @@ thiserror = { workspace = true }
 futures-util = { workspace = true }
 urlencoding = "2.1.3"
 serde = { version = "1", features = ["derive"], optional = true }
+serde_json = { version = "1.0.91", optional = true }
 url = "2.3.1"
 wasm-bindgen = { workspace = true, optional = true }
 web-sys = { version = "0.3.60", optional = true, features = [
@@ -26,18 +27,22 @@ web-sys = { version = "0.3.60", optional = true, features = [
 ] }
 js-sys = { version = "0.3.63", optional = true }
 gloo-utils = { version = "0.1.6", optional = true }
+dioxus-liveview = { workspace = true, optional = true }
 dioxus-ssr = { workspace = true, optional = true }
 tokio = { workspace = true, features = ["full"], optional = true }
 
 [features]
 default = ["web"]
 ssr = ["dioxus-ssr", "tokio"]
+liveview = ["dioxus-liveview", "tokio", "dep:serde", "serde_json"]
 wasm_test = []
 serde = ["dep:serde", "gloo-utils/serde"]
 web = ["gloo", "web-sys", "wasm-bindgen", "gloo-utils", "js-sys"]
 
 [dev-dependencies]
+axum = { version = "0.6.1", features = ["ws"] }
 dioxus = { path = "../dioxus" }
+dioxus-liveview = { workspace = true, features = ["axum"] }
 dioxus-ssr = { path = "../ssr" }
 criterion = { version = "0.5", features = ["async_tokio", "html_reports"] }
 

+ 64 - 8
packages/router/examples/simple_routes.rs

@@ -2,6 +2,47 @@ use dioxus::prelude::*;
 use dioxus_router::prelude::*;
 use std::str::FromStr;
 
+#[cfg(feature = "liveview")]
+#[tokio::main]
+async fn main() {
+    use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router};
+
+    let listen_address: std::net::SocketAddr = ([127, 0, 0, 1], 3030).into();
+    let view = dioxus_liveview::LiveViewPool::new();
+    let app = Router::new()
+        .fallback(get(move || async move {
+            Html(format!(
+                r#"
+                    <!DOCTYPE html>
+                    <html>
+                        <head></head>
+                        <body><div id="main"></div></body>
+                        {glue}
+                    </html>
+                "#,
+                glue = dioxus_liveview::interpreter_glue(&format!("ws://{listen_address}/ws"))
+            ))
+        }))
+        .route(
+            "/ws",
+            get(move |ws: WebSocketUpgrade| async move {
+                ws.on_upgrade(move |socket| async move {
+                    _ = view
+                        .launch(dioxus_liveview::axum_socket(socket), Root)
+                        .await;
+                })
+            }),
+        );
+
+    println!("Listening on http://{listen_address}");
+
+    axum::Server::bind(&listen_address.to_string().parse().unwrap())
+        .serve(app.into_make_service())
+        .await
+        .unwrap();
+}
+
+#[cfg(not(feature = "liveview"))]
 fn main() {
     #[cfg(not(target_arch = "wasm32"))]
     dioxus_desktop::launch(Root);
@@ -10,21 +51,26 @@ fn main() {
     dioxus_web::launch(root);
 }
 
+#[cfg(feature = "liveview")]
 #[component]
 fn Root(cx: Scope) -> Element {
-    render! {
-        Router::<Route> {}
-    }
+    let history = LiveviewHistory::new(cx);
+    render! { Router::<Route> {
+        config: || RouterConfig::default().history(history),
+    } }
+}
+
+#[cfg(not(feature = "liveview"))]
+#[component]
+fn Root(cx: Scope) -> Element {
+    render! { Router::<Route> {} }
 }
 
 #[component]
 fn UserFrame(cx: Scope, user_id: usize) -> Element {
     render! {
-        pre {
-            "UserFrame{{\n\tuser_id:{user_id}\n}}"
-        }
-        div {
-            background_color: "rgba(0,0,0,50%)",
+        pre { "UserFrame{{\n\tuser_id:{user_id}\n}}" }
+        div { background_color: "rgba(0,0,0,50%)",
             "children:"
             Outlet::<Route> {}
         }
@@ -88,6 +134,16 @@ fn Route3(cx: Scope, dynamic: String) -> Element {
             to: Route::Route2 { user_id: 8888 },
             "hello world link"
         }
+        button {
+            disabled: !navigator.can_go_back(),
+            onclick: move |_| { navigator.go_back(); },
+            "go back"
+        }
+        button {
+            disabled: !navigator.can_go_forward(),
+            onclick: move |_| { navigator.go_forward(); },
+            "go forward"
+        }
         button {
             onclick: move |_| { navigator.push("https://www.google.com"); },
             "google link"

+ 1 - 1
packages/router/src/components/history_buttons.rs

@@ -142,7 +142,7 @@ pub fn GoForwardButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
         }
     };
 
-    let disabled = !router.can_go_back();
+    let disabled = !router.can_go_forward();
 
     render! {
         button {

+ 14 - 0
packages/router/src/components/link.rs

@@ -11,6 +11,8 @@ use crate::navigation::NavigationTarget;
 use crate::prelude::Routable;
 use crate::utils::use_router_internal::use_router_internal;
 
+use url::Url;
+
 /// Something that can be converted into a [`NavigationTarget`].
 #[derive(Clone)]
 pub enum IntoRoutable {
@@ -53,6 +55,18 @@ impl From<&str> for IntoRoutable {
     }
 }
 
+impl From<Url> for IntoRoutable {
+    fn from(url: Url) -> Self {
+        IntoRoutable::FromStr(url.to_string())
+    }
+}
+
+impl From<&Url> for IntoRoutable {
+    fn from(url: &Url) -> Self {
+        IntoRoutable::FromStr(url.to_string())
+    }
+}
+
 /// The properties for a [`Link`].
 #[derive(Props)]
 pub struct LinkProps<'a> {

+ 1 - 1
packages/router/src/contexts/router.rs

@@ -232,7 +232,7 @@ impl RouterContext {
             IntoRoutable::FromStr(url) => {
                 let parsed_route: NavigationTarget<Rc<dyn Any>> = match self.route_from_str(&url) {
                     Ok(route) => NavigationTarget::Internal(route),
-                    Err(err) => NavigationTarget::External(err),
+                    Err(_) => NavigationTarget::External(url),
                 };
                 parsed_route
             }

+ 441 - 0
packages/router/src/history/liveview.rs

@@ -0,0 +1,441 @@
+use super::HistoryProvider;
+use crate::routable::Routable;
+use dioxus::prelude::*;
+use serde::{Deserialize, Serialize};
+use std::sync::{Mutex, RwLock};
+use std::{collections::BTreeMap, rc::Rc, str::FromStr, sync::Arc};
+
+/// A [`HistoryProvider`] that evaluates history through JS.
+pub struct LiveviewHistory<R: Routable>
+where
+    <R as FromStr>::Err: std::fmt::Display,
+{
+    action_tx: tokio::sync::mpsc::UnboundedSender<Action<R>>,
+    timeline: Arc<Mutex<Timeline<R>>>,
+    updater_callback: Arc<RwLock<Arc<dyn Fn() + Send + Sync>>>,
+}
+
+struct Timeline<R: Routable>
+where
+    <R as FromStr>::Err: std::fmt::Display,
+{
+    current_index: usize,
+    routes: BTreeMap<usize, R>,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+struct State {
+    index: usize,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+struct Session<R: Routable>
+where
+    <R as FromStr>::Err: std::fmt::Display,
+{
+    #[serde(with = "routes")]
+    routes: BTreeMap<usize, R>,
+    last_visited: usize,
+}
+
+#[derive(Serialize, Deserialize)]
+struct SessionStorage {
+    liveview: Option<String>,
+}
+
+enum Action<R: Routable> {
+    GoBack,
+    GoForward,
+    Push(R),
+    Replace(R),
+    External(String),
+}
+
+impl<R: Routable> Timeline<R>
+where
+    <R as FromStr>::Err: std::fmt::Display,
+{
+    fn new(initial_path: R) -> Self {
+        Self {
+            current_index: 0,
+            routes: BTreeMap::from([(0, initial_path)]),
+        }
+    }
+
+    fn init(
+        &mut self,
+        route: R,
+        state: Option<State>,
+        session: Option<Session<R>>,
+        depth: usize,
+    ) -> State {
+        if let Some(session) = session {
+            self.routes = session.routes;
+            if state.is_none() {
+                // top of stack
+                let last_visited = session.last_visited;
+                self.routes.retain(|&lhs, _| lhs <= last_visited);
+            }
+        };
+        let state = match state {
+            Some(state) => {
+                self.current_index = state.index;
+                state
+            }
+            None => {
+                let index = depth - 1;
+                self.current_index = index;
+                State { index }
+            }
+        };
+        self.routes.insert(state.index, route);
+        state
+    }
+
+    fn update(&mut self, route: R, state: Option<State>) -> State {
+        if let Some(state) = state {
+            self.current_index = state.index;
+            self.routes.insert(self.current_index, route);
+            state
+        } else {
+            self.push(route)
+        }
+    }
+
+    fn push(&mut self, route: R) -> State {
+        // top of stack
+        let index = self.current_index + 1;
+        self.current_index = index;
+        self.routes.insert(index, route);
+        self.routes.retain(|&rhs, _| index >= rhs);
+        State {
+            index: self.current_index,
+        }
+    }
+
+    fn replace(&mut self, route: R) -> State {
+        self.routes.insert(self.current_index, route);
+        State {
+            index: self.current_index,
+        }
+    }
+
+    fn current_route(&self) -> &R {
+        &self.routes[&self.current_index]
+    }
+
+    fn session(&self) -> Session<R> {
+        Session {
+            routes: self.routes.clone(),
+            last_visited: self.current_index,
+        }
+    }
+}
+
+impl<R: Routable> LiveviewHistory<R>
+where
+    <R as FromStr>::Err: std::fmt::Display,
+{
+    /// Create a [`LiveviewHistory`] in the given scope.
+    /// When using a [`LiveviewHistory`] in combination with use_eval, history must be untampered with.
+    ///
+    /// # Panics
+    ///
+    /// Panics if not in a Liveview context.
+    pub fn new(cx: &ScopeState) -> Self {
+        Self::new_with_initial_path(
+            cx,
+            "/".parse().unwrap_or_else(|err| {
+                panic!("index route does not exist:\n{}\n use LiveviewHistory::new_with_initial_path to set a custom path", err)
+            }),
+        )
+    }
+
+    /// Create a [`LiveviewHistory`] in the given scope, starting at `initial_path`.
+    /// When using a [`LiveviewHistory`] in combination with use_eval, history must be untampered with.
+    ///
+    /// # Panics
+    ///
+    /// Panics if not in a Liveview context.
+    pub fn new_with_initial_path(cx: &ScopeState, initial_path: R) -> Self {
+        let (action_tx, action_rx) = tokio::sync::mpsc::unbounded_channel::<Action<R>>();
+        let action_rx = Arc::new(Mutex::new(action_rx));
+        let timeline = Arc::new(Mutex::new(Timeline::new(initial_path)));
+        let updater_callback: Arc<RwLock<Arc<dyn Fn() + Send + Sync>>> =
+            Arc::new(RwLock::new(Arc::new(|| {})));
+
+        let eval_provider = cx
+            .consume_context::<Rc<dyn EvalProvider>>()
+            .expect("evaluator not provided");
+
+        let create_eval = Rc::new(move |script: &str| {
+            eval_provider
+                .new_evaluator(script.to_string())
+                .map(UseEval::new)
+        }) as Rc<dyn Fn(&str) -> Result<UseEval, EvalError>>;
+
+        // Listen to server actions
+        cx.push_future({
+            let timeline = timeline.clone();
+            let action_rx = action_rx.clone();
+            let create_eval = create_eval.clone();
+            async move {
+                let mut action_rx = action_rx.lock().expect("unpoisoned mutex");
+                loop {
+                    let eval = action_rx.recv().await.expect("sender to exist");
+                    let _ = match eval {
+                        Action::GoBack => create_eval(
+                            r#"
+                                // this triggers a PopState event
+                                history.back();
+                            "#,
+                        ),
+                        Action::GoForward => create_eval(
+                            r#"
+                                // this triggers a PopState event
+                                history.forward();
+                            "#,
+                        ),
+                        Action::Push(route) => {
+                            let mut timeline = timeline.lock().expect("unpoisoned mutex");
+                            let state = timeline.push(route.clone());
+                            let state = serde_json::to_string(&state).expect("serializable state");
+                            let session = serde_json::to_string(&timeline.session())
+                                .expect("serializable session");
+                            create_eval(&format!(
+                                r#"
+                                // this does not trigger a PopState event
+                                history.pushState({state}, "", "{route}");
+                                sessionStorage.setItem("liveview", '{session}');
+                            "#
+                            ))
+                        }
+                        Action::Replace(route) => {
+                            let mut timeline = timeline.lock().expect("unpoisoned mutex");
+                            let state = timeline.replace(route.clone());
+                            let state = serde_json::to_string(&state).expect("serializable state");
+                            let session = serde_json::to_string(&timeline.session())
+                                .expect("serializable session");
+                            create_eval(&format!(
+                                r#"
+                                // this does not trigger a PopState event
+                                history.replaceState({state}, "", "{route}");
+                                sessionStorage.setItem("liveview", '{session}');
+                            "#
+                            ))
+                        }
+                        Action::External(url) => create_eval(&format!(
+                            r#"
+                                location.href = "{url}";
+                            "#
+                        )),
+                    };
+                }
+            }
+        });
+
+        // Listen to browser actions
+        cx.push_future({
+            let updater = updater_callback.clone();
+            let timeline = timeline.clone();
+            let create_eval = create_eval.clone();
+            async move {
+                let popstate_eval = {
+                    let init_eval = create_eval(
+                        r#"
+                        return [
+                          document.location.pathname + "?" + document.location.search + "\#" + document.location.hash,
+                          history.state,
+                          JSON.parse(sessionStorage.getItem("liveview")),
+                          history.length,
+                        ];
+                    "#,
+                    ).expect("failed to load state").await.expect("serializable state");
+                    let (route, state, session, depth) = serde_json::from_value::<(
+                        String,
+                        Option<State>,
+                        Option<Session<R>>,
+                        usize,
+                    )>(init_eval).expect("serializable state");
+                    let Ok(route) = R::from_str(&route.to_string()) else {
+                        return;
+                    };
+                    let mut timeline = timeline.lock().expect("unpoisoned mutex");
+                    let state = timeline.init(route.clone(), state, session, depth);
+                    let state = serde_json::to_string(&state).expect("serializable state");
+                    let session = serde_json::to_string(&timeline.session())
+                        .expect("serializable session");
+
+                    // Call the updater callback
+                    (updater.read().unwrap())();
+
+                    create_eval(&format!(r#"
+                        // this does not trigger a PopState event
+                        history.replaceState({state}, "", "{route}");
+                        sessionStorage.setItem("liveview", '{session}');
+
+                        window.addEventListener("popstate", (event) => {{
+                          dioxus.send([
+                            document.location.pathname + "?" + document.location.search + "\#" + document.location.hash,
+                            event.state,
+                          ]);
+                        }});
+                    "#)).expect("failed to initialize popstate")
+                };
+
+                loop {
+                    let event = match popstate_eval.recv().await {
+                        Ok(event) => event,
+                        Err(_) => continue,
+                    };
+                    let (route, state) = serde_json::from_value::<(String, Option<State>)>(event).expect("serializable state");
+                    let Ok(route) = R::from_str(&route.to_string()) else {
+                        return;
+                    };
+                    let mut timeline = timeline.lock().expect("unpoisoned mutex");
+                    let state = timeline.update(route.clone(), state);
+                    let state = serde_json::to_string(&state).expect("serializable state");
+                    let session = serde_json::to_string(&timeline.session())
+                        .expect("serializable session");
+
+                    let _ = create_eval(&format!(
+                        r#"
+                        // this does not trigger a PopState event
+                        history.replaceState({state}, "", "{route}");
+                        sessionStorage.setItem("liveview", '{session}');
+                    "#));
+
+                    // Call the updater callback
+                    (updater.read().unwrap())();
+                }
+            }
+        });
+
+        Self {
+            action_tx,
+            timeline,
+            updater_callback,
+        }
+    }
+}
+
+impl<R: Routable> HistoryProvider<R> for LiveviewHistory<R>
+where
+    <R as FromStr>::Err: std::fmt::Display,
+{
+    fn go_back(&mut self) {
+        let _ = self.action_tx.send(Action::GoBack);
+    }
+
+    fn go_forward(&mut self) {
+        let _ = self.action_tx.send(Action::GoForward);
+    }
+
+    fn push(&mut self, route: R) {
+        let _ = self.action_tx.send(Action::Push(route));
+    }
+
+    fn replace(&mut self, route: R) {
+        let _ = self.action_tx.send(Action::Replace(route));
+    }
+
+    fn external(&mut self, url: String) -> bool {
+        let _ = self.action_tx.send(Action::External(url));
+        true
+    }
+
+    fn current_route(&self) -> R {
+        let timeline = self.timeline.lock().expect("unpoisoned mutex");
+        timeline.current_route().clone()
+    }
+
+    fn can_go_back(&self) -> bool {
+        let timeline = self.timeline.lock().expect("unpoisoned mutex");
+        // Check if the one before is contiguous (i.e., not an external page)
+        let visited_indices: Vec<usize> = timeline.routes.keys().cloned().collect();
+        visited_indices
+            .iter()
+            .position(|&rhs| timeline.current_index == rhs)
+            .map_or(false, |index| {
+                index > 0 && visited_indices[index - 1] == timeline.current_index - 1
+            })
+    }
+
+    fn can_go_forward(&self) -> bool {
+        let timeline = self.timeline.lock().expect("unpoisoned mutex");
+        // Check if the one after is contiguous (i.e., not an external page)
+        let visited_indices: Vec<usize> = timeline.routes.keys().cloned().collect();
+        visited_indices
+            .iter()
+            .rposition(|&rhs| timeline.current_index == rhs)
+            .map_or(false, |index| {
+                index < visited_indices.len() - 1
+                    && visited_indices[index + 1] == timeline.current_index + 1
+            })
+    }
+
+    fn updater(&mut self, callback: Arc<dyn Fn() + Send + Sync>) {
+        let mut updater_callback = self.updater_callback.write().unwrap();
+        *updater_callback = callback;
+    }
+}
+
+mod routes {
+    use crate::prelude::Routable;
+    use core::str::FromStr;
+    use serde::de::{MapAccess, Visitor};
+    use serde::{ser::SerializeMap, Deserializer, Serializer};
+    use std::collections::BTreeMap;
+
+    pub fn serialize<S, R>(routes: &BTreeMap<usize, R>, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+        R: Routable,
+    {
+        let mut map = serializer.serialize_map(Some(routes.len()))?;
+        for (index, route) in routes.iter() {
+            map.serialize_entry(&index.to_string(), &route.to_string())?;
+        }
+        map.end()
+    }
+
+    pub fn deserialize<'de, D, R>(deserializer: D) -> Result<BTreeMap<usize, R>, D::Error>
+    where
+        D: Deserializer<'de>,
+        R: Routable,
+        <R as FromStr>::Err: std::fmt::Display,
+    {
+        struct BTreeMapVisitor<R> {
+            marker: std::marker::PhantomData<R>,
+        }
+
+        impl<'de, R> Visitor<'de> for BTreeMapVisitor<R>
+        where
+            R: Routable,
+            <R as FromStr>::Err: std::fmt::Display,
+        {
+            type Value = BTreeMap<usize, R>;
+
+            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
+                formatter.write_str("a map with indices and routable values")
+            }
+
+            fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
+            where
+                M: MapAccess<'de>,
+            {
+                let mut routes = BTreeMap::new();
+                while let Some((index, route)) = map.next_entry::<String, String>()? {
+                    let index = index.parse::<usize>().map_err(serde::de::Error::custom)?;
+                    let route = R::from_str(&route).map_err(serde::de::Error::custom)?;
+                    routes.insert(index, route);
+                }
+                Ok(routes)
+            }
+        }
+
+        deserializer.deserialize_map(BTreeMapVisitor {
+            marker: std::marker::PhantomData,
+        })
+    }
+}

+ 5 - 0
packages/router/src/history/mod.rs

@@ -22,6 +22,11 @@ pub use web::*;
 #[cfg(feature = "web")]
 pub(crate) mod web_history;
 
+#[cfg(feature = "liveview")]
+mod liveview;
+#[cfg(feature = "liveview")]
+pub use liveview::*;
+
 // #[cfg(feature = "web")]
 // mod web_hash;
 // #[cfg(feature = "web")]

+ 22 - 8
packages/signals/src/effect.rs

@@ -1,16 +1,28 @@
 use core::{self, fmt::Debug};
-use std::cell::RefCell;
 use std::fmt::{self, Formatter};
-use std::rc::Rc;
 //
 use dioxus_core::prelude::*;
 
 use crate::use_signal;
 use crate::{dependency::Dependency, CopyValue};
 
-#[derive(Default, Clone)]
+#[derive(Copy, Clone, PartialEq)]
 pub(crate) struct EffectStack {
-    pub(crate) effects: Rc<RefCell<Vec<Effect>>>,
+    pub(crate) effects: CopyValue<Vec<Effect>>,
+}
+
+impl Default for EffectStack {
+    fn default() -> Self {
+        Self {
+            effects: CopyValue::new_in_scope(Vec::new(), ScopeId::ROOT),
+        }
+    }
+}
+
+impl EffectStack {
+    pub(crate) fn current(&self) -> Option<Effect> {
+        self.effects.read().last().copied()
+    }
 }
 
 pub(crate) fn get_effect_stack() -> EffectStack {
@@ -18,7 +30,7 @@ pub(crate) fn get_effect_stack() -> EffectStack {
         Some(rt) => rt,
         None => {
             let store = EffectStack::default();
-            provide_root_context(store.clone());
+            provide_root_context(store);
             store
         }
     }
@@ -57,6 +69,7 @@ pub fn use_effect_with_dependencies<D: Dependency>(
 pub struct Effect {
     pub(crate) source: ScopeId,
     pub(crate) callback: CopyValue<Box<dyn FnMut()>>,
+    pub(crate) effect_stack: EffectStack,
 }
 
 impl Debug for Effect {
@@ -67,7 +80,7 @@ impl Debug for Effect {
 
 impl Effect {
     pub(crate) fn current() -> Option<Self> {
-        get_effect_stack().effects.borrow().last().copied()
+        get_effect_stack().effects.read().last().copied()
     }
 
     /// Create a new effect. The effect will be run immediately and whenever any signal it reads changes.
@@ -77,6 +90,7 @@ impl Effect {
         let myself = Self {
             source: current_scope_id().expect("in a virtual dom"),
             callback: CopyValue::new(Box::new(callback)),
+            effect_stack: get_effect_stack(),
         };
 
         myself.try_run();
@@ -88,11 +102,11 @@ impl Effect {
     pub fn try_run(&self) {
         if let Some(mut callback) = self.callback.try_write() {
             {
-                get_effect_stack().effects.borrow_mut().push(*self);
+                self.effect_stack.effects.write().push(*self);
             }
             callback();
             {
-                get_effect_stack().effects.borrow_mut().pop();
+                self.effect_stack.effects.write().pop();
             }
         }
     }

+ 4 - 2
packages/signals/src/selector.rs

@@ -78,19 +78,21 @@ pub fn selector<R: PartialEq>(mut f: impl FnMut() -> R + 'static) -> ReadOnlySig
     let effect = Effect {
         source: current_scope_id().expect("in a virtual dom"),
         callback: CopyValue::invalid(),
+        effect_stack: get_effect_stack(),
     };
 
     {
-        get_effect_stack().effects.borrow_mut().push(effect);
+        get_effect_stack().effects.write().push(effect);
     }
     state.inner.value.set(SignalData {
         subscribers: Default::default(),
         effect_subscribers: Default::default(),
         update_any: schedule_update_any().expect("in a virtual dom"),
         value: f(),
+        effect_stack: get_effect_stack(),
     });
     {
-        get_effect_stack().effects.borrow_mut().pop();
+        get_effect_stack().effects.write().pop();
     }
 
     effect.callback.value.set(Box::new(move || {

+ 5 - 2
packages/signals/src/signal.rs

@@ -11,7 +11,7 @@ use dioxus_core::{
     ScopeId, ScopeState,
 };
 
-use crate::{CopyValue, Effect};
+use crate::{get_effect_stack, CopyValue, Effect, EffectStack};
 
 /// Creates a new Signal. Signals are a Copy state management solution with automatic dependency tracking.
 ///
@@ -82,6 +82,7 @@ pub(crate) struct SignalData<T> {
     pub(crate) subscribers: Rc<RefCell<Vec<ScopeId>>>,
     pub(crate) effect_subscribers: Rc<RefCell<Vec<Effect>>>,
     pub(crate) update_any: Arc<dyn Fn(ScopeId)>,
+    pub(crate) effect_stack: EffectStack,
     pub(crate) value: T,
 }
 
@@ -144,6 +145,7 @@ impl<T: 'static> Signal<T> {
                 effect_subscribers: Default::default(),
                 update_any: schedule_update_any().expect("in a virtual dom"),
                 value,
+                effect_stack: get_effect_stack(),
             }),
         }
     }
@@ -157,6 +159,7 @@ impl<T: 'static> Signal<T> {
                     effect_subscribers: Default::default(),
                     update_any: schedule_update_any().expect("in a virtual dom"),
                     value,
+                    effect_stack: get_effect_stack(),
                 },
                 owner,
             ),
@@ -172,7 +175,7 @@ impl<T: 'static> Signal<T> {
     /// If the signal has been dropped, this will panic.
     pub fn read(&self) -> Ref<T> {
         let inner = self.inner.read();
-        if let Some(effect) = Effect::current() {
+        if let Some(effect) = inner.effect_stack.current() {
             let mut effect_subscribers = inner.effect_subscribers.borrow_mut();
             if !effect_subscribers.contains(&effect) {
                 effect_subscribers.push(effect);