Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamically added scripts are always async #3757

Open
mcmah309 opened this issue Feb 17, 2025 · 8 comments
Open

Dynamically added scripts are always async #3757

mcmah309 opened this issue Feb 17, 2025 · 8 comments
Labels
bug Something isn't working

Comments

@mcmah309
Copy link

Problem

Scripts are not being executed correctly

Steps To Reproduce

e.g.

use dioxus::prelude::*;

fn main() {
    dioxus::launch(App);
}

#[component]
fn App() -> Element {
    rsx! {
        BugExample {}
    }
}

#[component]
pub fn BugExample() -> Element {
    let code = r#"
    use dioxus::prelude::*;

    #[component]
    pub fn Example()  -> Element {}
    "#;
    use_effect(|| {
        // document::eval("hljs.highlightAll();"); // Alternative, also does not work
    });
    rsx! {
        div {
            pre {
                code { class: "language-rust", {code} }
            }
            document::Script { {"hljs.highlightAll();"} }
            document::Script { src: "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/rust.min.js" }
            document::Script { src: "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js" }
            document::Link {
                href: "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css",
                rel: "stylesheet",
            }
        }
    }
}

Results in

Uncaught ReferenceError: hljs is not defined
    at <anonymous>:1:1
    at createElementInHead (eval at <anonymous> (quaza_website-0acb08c20af679fe.js:5:25219), <anonymous>:5:254)
    at eval (eval at <anonymous> (quaza_website-0acb08c20af679fe.js:5:25219), <anonymous>:6:9)
    at eval (eval at <anonymous> (quaza_website-0acb08c20af679fe.js:5:25219), <anonymous>:7:7)
    at quaza_website-0acb08c20af679fe.js:5:5396
    at handleError (quaza_website-0acb08c20af679fe.js:4:727)
    at imports.wbg.__wbg_call_7cccdd69e0791ae2 (quaza_website-0acb08c20af679fe.js:5:5344)
    at quaza_website-ab75a3d067b9bc35.wasm.__wbg_call_7cccdd69e0791ae2 externref shim (quaza_website_bg-064fab76afa3a51b.wasm:0x10d100)
    at quaza_website-ab75a3d067b9bc35.wasm.js_sys::Function::call1::he541a474704b8a93 (quaza_website_bg-064fab76afa3a51b.wasm:0x104aa8)
    at quaza_website-ab75a3d067b9bc35.wasm.<dioxus_web::document::WebDocument as dioxus_document::document::Document>::eval::h0df3cb7e03d97a79 (quaza_website_bg-064fab76afa3a51b.wasm:0x1de38)

on web. And the formatting is not applied to the code block.

Running

hljs.highlightAll();

Directly in the console succeeds and the code block is formatted correctly.

Copying and pasting the html directly from the browser to a new html page and opening succeeds.

<html><head>
    <title>quaza_website</title>
    <meta content="text/html;charset=utf-8" http-equiv="Content-Type">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta charset="UTF-8">
  <link rel="preload" as="script" href="/./assets/quaza_website-51efad540bdeaf2e.js" crossorigin=""><link rel="preload" as="fetch" type="application/wasm" href="/./assets/quaza_website_bg-53f30410b95b57e0.wasm" crossorigin=""><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css"><script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/rust.min.js"></script><script>hljs.highlightAll();</script></head>
  <body>
    <div id="main" data-dioxus-id="0"><div><pre><code class="language-rust">
    use dioxus::prelude::*;

    #[component]
    pub fn Example()  -&gt; Element {}
    </code></pre><!--placeholder--><!--placeholder--><!--placeholder--><!--placeholder--></div></div>
  <script>
            // We can't use a module script here because we need to start the script immediately when streaming
            import("/./assets/quaza_website-51efad540bdeaf2e.js").then(
                ({ default: init }) => {
                init("/./assets/quaza_website_bg-53f30410b95b57e0.wasm").then((wasm) => {
                    if (wasm.__wbindgen_start == undefined) {
                    wasm.main();
                    }
                });
                }
            );
            </script>
            <style>
  /* Inter Font */
  @import url('https://fonts.googleapis.com/css2?family=Inter:[email protected]&display=swap');

  #dx-toast-template {
    display: none;
    visibility: hidden;
  }

  .dx-toast {
    position: absolute;
    top: 10px;
    right: 0;
    padding-right: 10px;
    user-select: none;
    transition: transform 0.2s ease;
    z-index: 2147483647;
  }

  .dx-toast .dx-toast-inner {
    transition: right 0.2s ease-out;
    position: fixed;

    background-color: #181B20;
    color: #ffffff;
    font-family: "Inter", sans-serif;

    display: grid;
    grid-template-columns: auto auto;
    min-width: 280px;
    min-height: 92px;
    width: min-content;
    border-radius: 5px;

  }

  .dx-toast:hover {
    cursor: pointer;
    transform: translateX(-5px);
  }

  .dx-toast .dx-toast-level-bar-container {
    height: 100%;
    width: 6px;
  }

  .dx-toast .dx-toast-level-bar-container .dx-toast-level-bar {
    width: 100%;
    height: 100%;
    border-radius: 5px 0px 0px 5px;
  }

  .dx-toast .dx-toast-content {
    padding: 13px;
  }

  .dx-toast .dx-toast-header {
    display: flex;
    flex-direction: row;
    justify-content: start;
    align-items: end;
    margin-bottom: 13px;
  }

  .dx-toast .dx-toast-header>svg {
    height: 22px;
    margin-right: 5px;
  }

  .dx-toast .dx-toast-header .dx-toast-header-text {
    font-size: 16px;
    font-weight: 700;
    padding: 0;
    margin: 0;
  }

  .dx-toast .dx-toast-msg {
    font-size: 14px;
    font-weight: 400;
    padding: 0;
    margin: 0;
  }

  .dx-toast-level-bar.info {
    background-color: #428EFF;
  }

  .dx-toast-level-bar.success {
    background-color: #42FF65;
  }

  .dx-toast-level-bar.error {
    background-color: #FF4242;
  }
</style>

<div id="dx-toast-template" class="dx-toast">
  <div class="dx-toast-inner" style="right:-300px;">
    <!-- Level/Color decor -->
    <div class="dx-toast-level-bar-container">
      <div class="dx-toast-level-bar info"></div>
    </div>

    <!-- Content -->
    <div class="dx-toast-content">
      <!-- Header -->
      <div class="dx-toast-header">
        <!-- Dioxus Logo -->
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" preserveAspectRatio="none">
          <path d="M22.158 1.783c0 3.077-.851 5.482-2.215 7.377s-3.32 3.557-5.447 5.33-4.425 3.657-6.252 6.195-3.102 5.515-3.102 9.532h4.699c0-3.077.853-5.377 2.217-7.272s3.32-3.557 5.447-5.33 4.425-3.657 6.252-6.195 3.102-5.62 3.102-9.637z" fill="#e96020"></path>
          <path d="M9.531 25.927c-.635 0-1.021.515-1.02 1.15s.385 1.151 1.02 1.15H22.47a1.151 1.151 0 1 0 0-2.301zm1.361-4.076c-.608 0-.954.558-.953 1.166s.346 1.035.953 1.035h10.217a1.101 1.101 0 1 0 0-2.201zm0-13.594a1.101 1.101 0 1 0 0 2.201h10.217c.607 0 .953-.598.953-1.205s-.345-.996-.953-.996zM9.531 4.021A1.15 1.15 0 0 0 8.38 5.17a1.15 1.15 0 0 0 1.15 1.15h12.94c.635 0 1.021-.498 1.02-1.133s-.386-1.166-1.02-1.166z" fill="#2d323b"></path>
          <path d="M5.142 1.783c0 4.016 1.275 7.099 3.102 9.637s4.125 4.422 6.252 6.195 4.083 3.656 5.447 5.551 2.215 3.974 2.215 7.051h4.701c0-4.016-1.275-7.038-3.102-9.576s-4.125-4.422-6.252-6.195-4.083-3.435-5.447-5.33S9.841 4.86 9.841 1.783z" fill="#00a8d6"></path>
        </svg>
        <!-- Toast Title Text -->
        <h3 class="dx-toast-header-text">Your app is being rebuilt.</h3>
      </div>

      <!-- Message -->
      <p class="dx-toast-msg">A non-hot-reloadable change occurred and we must rebuild.</p>
    </div>
  </div>
</div>

<script>
  const STORAGE_KEY = "SCHEDULED-DX-TOAST";
  let currentToast = null;
  let currentTimeout = null;

  // Show a toast, removing the previous one.
  function showDXToast(headerText, message, progressLevel, durationMs) {
    // Close current toast if exists.
    closeDXToast();

    // Clone template and add unique id.
    let toastTemplate = document.getElementById("dx-toast-template");
    let cloned = toastTemplate.cloneNode(true);
    let toastId = `dx-toast`;
    cloned.id = toastId;
    currentToast = cloned;

    let innerElem = currentToast.querySelector(`#${toastId} .dx-toast-inner`);

    // Set the progress level
    let progressBarElem = innerElem.querySelector(".dx-toast-inner .dx-toast-level-bar-container .dx-toast-level-bar");
    progressBarElem.className = `dx-toast-level-bar ${progressLevel}`;

    // Set header text
    let headerTextElem = innerElem.querySelector(".dx-toast-inner .dx-toast-header .dx-toast-header-text");
    headerTextElem.innerText = headerText;

    // Set message
    let messageElem = innerElem.querySelector(".dx-toast-inner .dx-toast-msg");
    messageElem.innerText = message;

    document.body.appendChild(currentToast);

    // Add listener to close toasts when clicked.
    // Safety: Calling `closeToast` removes the element and all event listeners with it.
    currentToast.addEventListener("click", closeDXToast);

    // Wait a bit of time so animation plays correctly.
    setTimeout(() => {
      innerElem.style.right = "0";

      currentTimeout = setTimeout(() => {
        closeDXToast();
      }, durationMs);
    }, 100);
  }

  // Schedule a toast to be displayed after reload.
  function scheduleDXToast(headerText, message, level, durationMs) {
    let data = {
      headerText,
      message,
      level,
      durationMs,
    };

    let jsonData = JSON.stringify(data);
    sessionStorage.setItem(STORAGE_KEY, jsonData);
  }

  // Close the current toast.
  function closeDXToast() {
    if (currentToast) {
      currentToast.remove();
    }
    clearTimeout(currentTimeout);
  }

  // Handle any scheduled toasts after reload.
  let potentialData = sessionStorage.getItem(STORAGE_KEY);
  if (potentialData) {
    sessionStorage.removeItem(STORAGE_KEY);
    let data = JSON.parse(potentialData);
    showDXToast(data.headerText, data.message, data.level, data.durationMs);
  }

</script>
            

</body></html>

Thus considering this and looking at the error. This is a Dioxus specific error.

Expected behavior

The formatted code block is displayed

Screenshots

Environment:

  • Dioxus version: 0.6.2
  • Rust version: 1.86.0 nightly
  • OS info: NixOS 25.05
  • App platform: web

Questionnaire

I'm interested in fixing this myself but don't know where to start.

@mcmah309 mcmah309 added the bug Something isn't working label Feb 17, 2025
@hackartists
Copy link
Contributor

@mcmah309 eval may not have access to some browser API.
As workaround, you can solve it by using script instead of use_effect.

rsx! {
    script { r#type: "module", "hljs.highlightAll();" }
}

@mcmah309
Copy link
Author

Thanks for the information.

eval may not have access to some browser API.

Is this behavior documented somewhere? If not, why does it only have access to some and which ones?

script { r#type: "module", "hljs.highlightAll();" }

Unfortunately this still results in a similar error:

Uncaught ReferenceError: hljs is not defined
    at <anonymous>:1:1
rust.min.js:28 Uncaught ReferenceError: hljs is not defined
    at rust.min.js:28:2
    at rust.min.js:28:35

But the raw html works fine if posted into regular .html file and loaded.

Note, in my original code I use:

document::Script { {"hljs.highlightAll();"} }

instead of use_effect, use_effect was commented out to show that is also does not work and has the same issue.

@ealmloff
Copy link
Member

The original solution with script tags seems like the same issue as #3756. The tag order is reversed, so the script that highlights the code is loaded before the library.

The alternative solution with eval should work as long as the script has loaded before the eval runs. use_effect just waits until the dom mutations are done. Running the effect inside of the head script's onload event requires support for #3758, but it does work with normal script tags:

use dioxus::prelude::*;

fn main() {
    dioxus::launch(App);
}

#[component]
fn App() -> Element {
    rsx! {
        BugExample {}
    }
}

#[component]
pub fn BugExample() -> Element {
    let code = r#"
    use dioxus::prelude::*;

    #[component]
    pub fn Example()  -> Element {}
    "#;
    let mut loaded = use_signal(|| 0);
    use_effect(move || {
        if loaded() > 1 {
            document::eval("hljs.highlightAll();");
        }
    });
    rsx! {
        div {
            pre {
                code { class: "language-rust", {code} }
            }
            script { src: "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/rust.min.js", onload: move |_| {
                loaded += 1;
            } }
            script { src: "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js", onload: move |_| {
                loaded += 1;
            } }
            document::Link {
                href: "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css",
                rel: "stylesheet",
            }
        }
    }
}

Closing this issue in favor of #3756 and #3758

@mcmah309
Copy link
Author

mcmah309 commented Feb 24, 2025

The original solution with script tags seems like the same issue as #3756.

Yes the solution is the same, but the issue is not as far as I can tell.

The tag order is reversed, so the script that highlights the code is loaded before the library.

The code actually takes this into account and reverses the order - see

document::Script { {"hljs.highlightAll();"} }
document::Script { src: "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/rust.min.js" }
document::Script { src: "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js" }
document::Link {
href: "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css",
rel: "stylesheet",
}

use_effect just waits until the dom mutations are done

Is there a dom event that triggers this? Or what do you mean by "mutations are done"

As a far as I can tell, this issue should not be closed. The head script tags are "being applied correctly" (reverse order accounted for), but the mentioned error persists. If I apply the raw tags to a separate page (just copying and pasting the resulting html from the browser), It works.

@ealmloff
Copy link
Member

ealmloff commented Feb 24, 2025

use_effect just waits until the dom mutations are done

Is there a dom event that triggers this? Or what do you mean by "mutations are done"

use_effect runs after dioxus has inserted all of the elements in the component into the DOM. There is not a dom event that triggers it. There is some information about how eval interacts with use_effect here in the essentials guide

As a far as I can tell, this issue should not be closed. The head script tags are "being applied correctly" (reverse order accounted for), but the mentioned error persists. If I apply the raw tags to a separate page (just copying and pasting the resulting html from the browser), It works.

Scripts that are added dynamically act differently than scripts that are present in the initial HTML. Dioxus is running something like this:

var script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js';
document.head.appendChild(script);
var script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/rust.min.js';
document.head.appendChild(script);

var script = document.createElement('script');
script.textContent = "hljs.highlightAll();"
document.head.appendChild(script);

If you disable the head components and run that script in the console, you should see the same error. Your original code does work if you run it in fullstack because the head elements are present in the initial HTML. It looks like there are html attributes you can add to the script like async: false to preserve the order, but the order of the head elements are already not well defined in Dioxus so I don't think they should be added by default.

@mcmah309
Copy link
Author

Makes sense thanks!

Scripts that are added dynamically act differently than scripts that are present in the initial HTML. Dioxus is running something like this:

var script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js';
document.head.appendChild(script);
var script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/rust.min.js';
document.head.appendChild(script);

var script = document.createElement('script');
script.textContent = "hljs.highlightAll();"
document.head.appendChild(script);

That is interesting, since doing it this way is the same as async: true. Thus maybe the default should be to explicitly set async: false, since this would likely be the intended behavior. Or at least mention it in the docs. I can create a small PR to document this if you want to keep this intended behavior.

@ealmloff
Copy link
Member

That is interesting, since doing it this way is the same as async: true. Thus maybe the default should be to explicitly set async: false, since this would likely be the intended behavior. Or at least mention it in the docs. I can create a small PR to document this if you want to keep this intended behavior.

I think this is the same fundamental design choice presented in #3756. The async: false only matters if the script tag loading order is well defined. If that ordering is already undefined with head components, I don't think it is important to preserve that undefined order with async: false by default.

Looking at other frameworks: React's head script requires you always set async: true explicitly because the loading order is undefined. That approach seems reasonable. We could require the same thing or just set it automatically (for SSR rendered head script element) and document that head scripts always act like async: true

@mcmah309
Copy link
Author

Looking at other frameworks: React's head script requires you always set async: true explicitly because the loading order is undefined. That approach seems reasonable. We could require the same thing or just set it automatically (for SSR rendered head script element) and document that head scripts always act like async: true

I think requiring the same thing would make sense. Having consistent behavior for SSR too would also be a less of a headache if anyone ever ran into this. It also might make sense to add defining async in rosetta as a compile time error and inform the user that it is always true.

Also given async: true and defer: true cannot be used together, we may want to prohibit defer as well and instead advise to use use_effect instead since

use_effect runs after dioxus has inserted all of the elements in the component into the DOM.

@ealmloff ealmloff reopened this Feb 24, 2025
@ealmloff ealmloff changed the title Scripts Are Not Executed Correctly Dynamically added scripts are always async Feb 24, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

3 participants