@rasenjs/web
Isomorphic web components with routing that automatically switch between DOM and HTML rendering based on the execution environment.
Installation
npm install @rasenjs/web @rasenjs/router @rasenjs/core zodOverview
@rasenjs/web is a unified facade package that provides both basic elements and routing components for web rendering. It automatically exports the appropriate renderer based on the environment:
- Server-side (SSR): Uses
@rasenjs/html+@rasenjs/router-htmlto render HTML strings - Client-side (Browser): Uses
@rasenjs/dom+@rasenjs/router-domfor reactive DOM manipulation
This allows you to write isomorphic components with routing that work seamlessly in both environments without code changes.
How It Works
The package uses conditional exports in package.json:
{
"exports": {
".": {
"ssr": "./dist/html.js", // Server-side rendering
"browser": "./dist/dom.js", // Client-side rendering
"default": "./dist/dom.js" // Default to browser
}
}
}Build tools like Vite automatically select the correct export condition based on the environment.
Quick Start
Isomorphic Component
Write your component once, and it works in both SSR and client environments:
import { useReactiveRuntime } from '@rasenjs/reactive-vue'
import { div, p, button } from '@rasenjs/web'
import { ref } from '@vue/reactivity'
// Setup reactive runtime
useReactiveRuntime()
// Isomorphic component - works in both SSR and browser
export const Counter = () => {
const count = ref(0)
const increment = () => count.value++
return div(
{},
// a ref in a child position becomes a reactive text node on both targets
p({}, 'Count: ', count),
button({ onClick: increment }, 'Increment')
)
}Server-Side Rendering (SSR)
On the server, @rasenjs/web renders to HTML strings:
import { renderToString } from '@rasenjs/web'
import { Counter } from './Counter'
// Renders to HTML string
const html = renderToString(Counter())
// Result: '<div><p>Count: 0</p><button>Increment</button></div>'Client-Side Hydration
On the client, @rasenjs/web creates interactive DOM elements:
import { mount } from '@rasenjs/web'
import { Counter } from './Counter'
// Mounts and hydrates the component
mount(Counter(), document.getElementById('app'))
// Now the button is interactive and updates the counterAPI
@rasenjs/web re-exports all APIs from the appropriate renderer and router:
SSR Mode (Server)
Exports everything from @rasenjs/html + @rasenjs/router-html:
renderToString()- Render component to HTML stringdiv(),p(),button(), etc. - HTML element creatorsRouterView()- Render matched route to HTMLRouterLink()- Create<a>tag with href- All standard HTML elements
Browser Mode (Client)
Exports everything from @rasenjs/dom + @rasenjs/router-dom:
mount()- Mount component to DOMdiv(),p(),button(), etc. - DOM element creatorsRouterView()- Render matched route to DOMRouterLink()- Create interactive<a>with click handling- All standard HTML elements with event handling
Important Considerations
1. Event Handlers
Event handlers only work in the browser. On the server, they are ignored:
// ✅ Safe - works in browser, ignored in SSR
button({ onClick: handler }, 'Click me')
// ❌ Avoid - 'on' object would render as HTML attribute in SSR
button({ on: { click: handler } }, 'Click me')Best Practice: Use onClick, onInput, etc. (not on: { click } object).
2. Reactive Text
Pass a ref to a child position and both renderers keep it updated:
// ✅ Works in both SSR and browser — one text node, updated through the runtime
p({}, count)
p({}, 'Count: ', count)- SSR: the current value is written into the markup
- Browser: the same ref drives a subscription, so later writes update the node
A plain getter child — p(() => \Count: ${count.value}`)— does **not** subscribe in the DOM renderer, it is invoked once. Use a ref,text({ content: () => … }), or JSX (
Count: {count}
`, which the compiler binds for you).3. Import Path
Always import from @rasenjs/web, not from @rasenjs/dom or @rasenjs/html directly:
// ✅ Correct - automatic environment detection
import { div, mount } from '@rasenjs/web'
// Routing
### Define Routes
```typescript
import { createRouter, createMemoryHistory, template as tpl } from '@rasenjs/router'
import { z } from 'zod'
export const router = createRouter({
home: '/',
about: '/about',
user: tpl`/user/${{ id: z.string() }}`,
}, {
history: createMemoryHistory(),
})
export const { routes } = routerUse Router Components
import { createRouterView, createRouterLink, div, nav } from '@rasenjs/web'
import { router, routes } from './router'
// One view table + one link factory; both resolve to the DOM or the HTML
// implementation depending on the environment.
const RouterView = createRouterView(
router,
{
home: () => Home(),
about: () => About(),
user: (params) => User(params)
},
{ default: () => NotFound() }
)
const Link = createRouterLink(router)
export const App = () => div(
{},
nav({}, Link({ to: routes.home }, 'Home'), Link({ to: routes.about }, 'About')),
RouterView()
)In JSX the same components are written as <RouterView /> and <Link to={routes.home}>Home</Link>.
Use Cases
Universal Components with Routing
Build full-featured apps that work in any environment:
```typescriptRouterViewandRouterLink`
- Dynamic route parameters
- Hot module replacement (HMR)
Related Packages
@rasenjs/router- Core headless router@rasenjs/dom- DOM renderer (browser)@rasenjs/html- HTML renderer (SSR)@rasenjs/router-dom- DOM router components (internal)@rasenjs/router-html- HTML router components (internal)
SSR with Client Hydration and Routing
Perfect for server-rendered apps with client interactivity and navigation:
// server.ts
import { renderToString } from '@rasenjs/web'
import { router } from './router'
const html = (url: string) => {
router.push(url) // Set route from request URL
return `<!DOCTYPE html>
<html>
<body>
<div id="app">${renderToString(App())}</div>
<script type="module" src="/client.js"></script>
</body>
</html>`
}
// client.ts
import { mount } from '@rasenjs/web'
import { createBrowserHistory } from '@rasenjs/router'
import { router } from './router'
router.history = createBrowserHistory()
</html>`
// client.ts
import { mount } from '@rasenjs/web'
mount(App(), document.getElementById('app'))Complete Example
See the SSR example for a full implementation with:
- Server-side rendering
- Client-side hydration
- Routing with
RouterViewandRouterLink - Dynamic route parameters
- Hot module replacement (HMR)
Related Packages
@rasenjs/router- Core headless router@rasenjs/dom- DOM renderer (browser)@rasenjs/html- HTML renderer (SSR)@rasenjs/router-dom- DOM router components (internal)@rasenjs/router-html- HTML router components (internal)
License
MIT