# Bug #11: NiiVue js_on_load Doesn't Re-run on Value Update
## Status: FIXED
**Date:** 2025-12-09
**Branch:** `fix/niivue-js-rerun`
**Fixed By:** Implementing `.then(fn=None, js=NIIVUE_UPDATE_JS)` pattern with correct `document.querySelector` context.
**Related:** Bug #10 (Fixed)
---
## TL;DR - ROOT CAUSE
**Gradio's `js_on_load` only runs ONCE when the component first mounts.**
When we update the `gr.HTML` value with new content (after segmentation), the `js_on_load` code does NOT re-execute. The HTML updates, but the JavaScript initialization never runs.
---
## Symptom
After successful DeepISLES inference on HF Spaces:
- Viewer shows "Loading viewer..." (initial HTML state)
- Status never changes to "Checking WebGL2..." or "Loading NiiVue..."
- No error message displayed
- No brain scan visible
**What IS working:**
- DeepISLES inference completes (~36 seconds)
- Slice Comparison (matplotlib 2D view) renders correctly
- Metrics JSON displays correctly
- Download button provides the prediction mask
- Initial HTML renders with data-* attributes
**What is NOT working:**
- js_on_load JavaScript doesn't re-run when value updates
- NiiVue never initializes after segmentation
---
## Evidence
### Gradio Documentation
From [Custom HTML Components Guide](https://www.gradio.app/guides/custom_HTML_components):
> "Event listeners attached in `js_on_load` are only attached **once** when the component is first rendered. If your component creates new elements dynamically that need event listeners, attach the event listener to a parent element..."
### Observed Behavior
1. Page loads → js_on_load runs → No volumeUrl → Shows "Waiting for segmentation..."
2. User clicks "Run Segmentation"
3. DeepISLES runs successfully
4. `run_segmentation()` returns new HTML with data-volume-url attribute
5. gr.HTML value updates with new HTML
6. **js_on_load does NOT re-run** ← THE BUG
7. Viewer shows "Loading viewer..." (static HTML, no JS executed)
### Server Logs (Working)
```text
INFO: Running segmentation for sub-stroke0001
INFO: DeepISLES subprocess completed in 35.73s
```
Inference works. The problem is client-side JavaScript execution.
---
## Code Flow Analysis
### Current Implementation (BROKEN)
```python
# components.py - js_on_load set once at component creation
niivue_viewer = gr.HTML(
label="Interactive 3D Viewer",
js_on_load=NIIVUE_ON_LOAD_JS, # Runs ONCE on mount
)
# app.py - returns new HTML value after segmentation
def run_segmentation(...):
# ... inference ...
niivue_html = create_niivue_html(dwi_url, mask_url)
return niivue_html, ... # Value updates, but js_on_load doesn't re-run
```
### Why It Fails
1. Component mounts → js_on_load runs (no data yet)
2. Value updates → HTML re-renders, js_on_load SKIPPED
3. New HTML has data-* attributes but no JS execution
---
## Proposed Solutions (Ranked)
### Solution 1: Use `js` Parameter on Event Handler (Recommended)
Gradio allows running JavaScript after an event completes:
```python
run_btn.click(
fn=run_segmentation,
inputs=[...],
outputs=[results["niivue_viewer"], ...],
).then(
fn=None, # MUST be explicit!
js=NIIVUE_UPDATE_JS, # ⚠️ CANNOT reuse NIIVUE_ON_LOAD_JS - different context!
)
```
**Pros:** Native Gradio pattern, runs after each update
**Cons:** Requires separate JS constant (see "Different JS Context" section below)
**⚠️ CRITICAL:** The `js` param does NOT have access to `element`. You must use
`document.querySelector()` instead. See the corrected JavaScript in the
"Recommended Implementation" section.
### Solution 2: MutationObserver in js_on_load
Watch for DOM changes and re-initialize. This approach IS valid because
`js_on_load` has access to `element`:
```javascript
// In js_on_load - 'element' IS available here
const initNiiVue = async () => {
const container = element.querySelector('.niivue-viewer') || element;
const volumeUrl = container.dataset.volumeUrl;
if (!volumeUrl) return;
// ... NiiVue initialization code ...
};
// Watch for attribute changes (when Python updates data-volume-url)
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'attributes' &&
mutation.attributeName.startsWith('data-')) {
initNiiVue();
break;
}
}
});
// Observe the element for attribute changes
observer.observe(element, {
attributes: true,
subtree: true,
attributeFilter: ['data-volume-url', 'data-mask-url']
});
// Initial check
initNiiVue();
```
**Pros:** Self-contained in js_on_load, no separate event wiring needed
**Cons:** More complex, relies on Gradio updating DOM attributes (may not work
if Gradio replaces the entire element instead of updating attributes)
### Solution 3: Use gradio-iframe Component
The `gradio-iframe` package allows JavaScript to execute normally:
```python
from gradio_iframe import iFrame
niivue_viewer = iFrame(
value=create_niivue_html_with_script(...), # Scripts execute in iframe
)
```
**Pros:** Scripts execute normally inside iframe
**Cons:** Additional dependency, iframe quirks
### Solution 4: Embed JS in HTML via data: URL iframe
Self-contained iframe with script:
```python
def create_niivue_html(...):
html_with_script = f''''''
encoded = base64.b64encode(html_with_script.encode()).decode()
return f''
```
**Pros:** No external dependency, scripts execute
**Cons:** Complex, potential CSP issues
### Solution 5: Custom Gradio Component
Build a proper `gradio_niivue` Svelte component:
```bash
gradio cc create NiiVue --template HTML
```
**Pros:** Most robust, proper lifecycle hooks
**Cons:** Significant development effort
---
## Investigation Steps
### Step 1: Test Solution 1 (js param on .then())
```python
run_btn.click(
fn=run_segmentation,
inputs=[...],
outputs=[...],
).then(
fn=None,
js="console.log('then JS ran'); console.log(document.querySelector('.niivue-viewer'));"
)
```
Verify:
- Does `js` run after value update?
- Does it have access to the updated DOM?
### Step 2: Test Solution 2 (MutationObserver)
Add observer to js_on_load and check if it triggers on value change.
### Step 3: Check Browser Console
Open DevTools and look for:
- JavaScript errors
- Console logs from js_on_load
- Network requests to NiiVue CDN
---
## Temporary Workaround
The 2D Slice Comparison view works correctly and provides adequate visualization for evaluation purposes while we fix the 3D viewer.
---
## Priority Assessment
**Severity:** P1 (High)
- 3D viewer is a key feature for the demo
- The fix we deployed doesn't fully work
- Blocks demo usability for 3D visualization
**Impact:**
- Users see "Loading viewer..." indefinitely
- 2D fallback still works
- Demo is partially functional
---
## Deep Web Research (2025-12-09)
### Relationship Between Bug #10 and Bug #11
**They are the SAME underlying issue with two symptoms:**
1. **Bug #10**: Gradio strips `