Zumly with React
A view can be a React component. Zumly hands you an empty element; you mount a root in it and unmount it when the view goes away.
Each planet and the list are React components in their own views. Like a few times, open a planet, then come back: the count is still there.
The pattern
Make the view a function. It receives a context; mount a root in target.
import Zumly from 'zumly'
import 'zumly/style.css'
import { createRoot } from 'react-dom/client'
import Detail from './Detail'
const app = new Zumly({
mount: '.canvas',
initialView: 'home',
views: {
home: '<div class="z-view"><div class="zoom-me" data-to="detail" data-id="42">Open</div></div>',
detail: ({ target, props, onCleanup }) => {
const root = createRoot(target)
root.render(<Detail id={props.id} />)
onCleanup(() => root.unmount())
}
}
})
app.init()
targetis a fresh element that becomes the view. Zumly gives it thez-viewclass; add your own classes or a size to it if the view should not fill the canvas.propsholds the trigger'sdata-*attributes.data-id="42"arrives asprops.id, a string.onCleanupruns once, when the view is discarded. Unmount there; removing the DOM alone does not.
A list that opens a detail
Zumly is created once, in the component that owns the canvas, and destroyed with it. The list renders the triggers; any element with zoom-me and data-to works, whoever rendered it.
App.jsx
import { useEffect, useRef } from 'react'
import { createRoot } from 'react-dom/client'
import Zumly from 'zumly'
import 'zumly/style.css'
import List from './List'
import Detail from './Detail'
// Each view gets its own React root
const mountView = (render) => ({ target, props, onCleanup }) => {
const root = createRoot(target)
root.render(render(props))
onCleanup(() => root.unmount())
}
export default function App () {
const canvas = useRef(null)
useEffect(() => {
const app = new Zumly({
mount: canvas.current,
initialView: 'list',
views: {
list: mountView(() => <List />),
detail: mountView(props => <Detail id={props.id} />)
}
})
app.init()
return () => app.destroy() // removes its views too
}, [])
// The canvas fills its nearest positioned ancestor: give that a height
return <div style={{ position: 'relative', height: '100vh' }}>
<div ref={canvas} className="zumly-canvas" />
</div>
}
List.jsx
const items = [
{ id: '1', name: 'Engine' },
{ id: '2', name: 'Battery' },
{ id: '3', name: 'Brakes' }
]
export default function List () {
return <div className="list">
<h1>Subsystems</h1>
{items.map(item =>
<div key={item.id} className="zoom-me" data-to="detail" data-id={item.id}>{item.name}</div>
)}
</div>
}
When a view comes back
- The views behind the current one stay mounted, so their component state survives a zoom in and out. Nothing renders again.
- If something changed meanwhile (a store, a fetch), repaint in
onActivate(fn), which runs each time the view becomes current again. - Siblings you leave are discarded, unless
lateralNav: { keepAlive: true }keeps them while you stay at that level.
Sharing services and state
Redux, Zustand or a React context: pass it once in componentContext and every view receives it as context.
const app = new Zumly({
...,
componentContext: { store },
views: {
detail: ({ target, props, context, onCleanup }) => {
const root = createRoot(target)
root.render(<Provider store={context.store}><Detail id={props.id} /></Provider>)
onCleanup(() => root.unmount())
}
}
})
Things to know
- In
StrictModeReact runs the effect twice in development.destroy()takes the first instance's views with it, so the second one starts on a clean canvas. - Do not render another
z-viewinsidetarget:targetalready is the view. Zumly warns in the console if you do. - Views are plain DOM, not Shadow DOM: your global styles apply inside them.
- Zumly ships its own types;
ViewFunctionandViewContexttype the view context.
import { createRoot } from 'react-dom/client'
import type { ViewFunction } from 'zumly'
const detail: ViewFunction = ({ target, props, onCleanup }) => {
const root = createRoot(target)
root.render(<Detail id={String(props.id)} />)
onCleanup(() => root.unmount())
}
Next
Where views come from, in step 4. The same pattern for Vue · Svelte · Angular, and for radial layouts with Orbit.
Open this example ↗ Learn in six steps