Gotchas
Things we learned building 14 apps. Read this before you hit the same walls.
1. Don't re-render for high-frequency updates
If you have a timer, audio progress bar, or animation that updates 4+ times per second, don't call renderApp(). It rebuilds the entire DOM — images flicker, network requests spike.
Patch only the elements that change:
// BAD — re-renders 50 rows to update one progress bar
setInterval(function() {
progress = audio.currentTime / audio.duration
renderApp()
}, 250)
// GOOD — patches 2 elements directly
setInterval(function() {
var fill = container.querySelector('.progress-fill')
var time = container.querySelector('.time-elapsed')
if (fill) fill.style.width = (audio.currentTime / audio.duration * 100) + '%'
if (time) time.textContent = formatTime(audio.currentTime)
}, 250)
2. Use the rawData argument
The shell passes parsed JSON-LD as the 4th argument to render. Use it instead of parsing textContent:
// OLD — race condition risk
render(subject, store, container) {
var data
try { data = JSON.parse(dataEl.textContent) } catch (e) { return }
}
// NEW — reliable
render(subject, store, container, rawData) {
var data = rawData
}
3. innerHTML is an XSS risk
html`<div innerHTML="${value}"></div>` sets innerHTML directly. Safe with trusted content. Dangerous with user input.
// SAFE — values go through textContent, no HTML parsing
html`<div>${userInput}</div>`
// DANGEROUS — parses HTML, can execute scripts
html`<div innerHTML="${userInput}"></div>`
// SAFE — sanitize first
var clean = userInput.replace(/<[^>]*>/g, '')
html`<div innerHTML="${clean}"></div>`
4. No </script> in JSON-LD data
If your JSON-LD contains the literal string </script>, the browser's HTML parser will corrupt the DOM when it's written to a script element's textContent. Strip or escape it.
5. store.set() triggers onChange synchronously
Don't call renderApp() after store.set() — it already ran via onChange:
// BAD — renders twice
store.set(node, 'title', 'New')
renderApp()
// GOOD — renders once
store.set(node, 'title', 'New')
Exception: multiple store.set() calls in sequence each trigger a render. For bulk updates, call renderApp() manually after the last one.
6. Use the local shell
Copy shell.js into your project. Don't load it from a CDN:
<script type="module" src="losos/shell.js"></script>
The shell provides built-in tab persistence. No MutationObserver needed.
7. File structure
my-app/
losos/ ← framework (copy these)
html.js
store.js
shell.js
lion/ ← JSON-LD store (used by shell)
index.js
panes/ ← your app code
my-pane.js
data.jsonld
index.html
8. API apps: bootstrap before shell
For apps that fetch data from an external API, load the shell after the data is ready:
fetch('https://api.example.com/data')
.then(function(r) { return r.json() })
.then(function(data) {
var jsonLd = transformToJsonLd(data)
window.__myData = jsonLd
document.getElementById('data').textContent = JSON.stringify(jsonLd)
document.getElementById('losos').textContent = ''
var s = document.createElement('script')
s.type = 'module'
s.src = 'losos/shell.js'
document.body.appendChild(s)
})
9. Every ${} in a tag must be an attribute value
Bare interpolations inside opening tags break attribute parsing silently:
<!-- BAD — bare interpolation corrupts the tag -->
<input type="checkbox" ${isDone ? 'checked' : ''}
onchange="${fn}" />
<!-- onchange gets stringified as text, not bound as event -->
<!-- GOOD — use attribute value form -->
<input type="checkbox" checked="${isDone}"
onchange="${fn}" />
<!-- false removes the attribute, true adds it -->
The rule: every ${} inside an opening tag must be in the form attr="${value}". LOSOS doesn't support <div ${expr}> spread syntax.