On this page
Get started How to use State & effects Store Router Server rendering Deploy
๐ŸŸ Documentation

Build with Danio

Everything you need to ship a real app โ€” install, components, hooks, the store, the router, server rendering, and deployment. If you know React, skim the headings; the model is the same. If you don't, this reads top to bottom.

Get started

The fastest way to begin is the scaffolder. It sets up a Vite project already wired for Danio, with an example app, editor autocomplete, and TypeScript types.

# create a new app
npm create danio@latest my-app
cd my-app
npm install
npm run dev

Your app is now running at http://localhost:5173 with hot reload. Open src/App.jsx and start editing.

Adding Danio to an existing project

Already have a Vite project? Install the package and point the JSX compiler at Danio's runtime.

npm install danio-js
// vite.config.js
import { defineConfig } from 'vite'

export default defineConfig({
  esbuild: { jsx: 'automatic', jsxImportSource: 'danio-js' },
})

For editor autocomplete and type-checking, add a jsconfig.json (or tsconfig.json):

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "danio-js",
    "moduleResolution": "Bundler"
  }
}
Danio ships its own TypeScript definitions โ€” there's no @types/* package to install.

Your first component

A component is a plain function that returns markup. Mount it into the page with render.

import { render } from 'danio-js'

function Hello({ name }) {
  return <h1>Hello, {name}</h1>
}

render(<Hello name="world" />, document.getElementById('root'))

render(element, container) mounts into any DOM node. Call it again on the same node to update in place; call unmount(container) to tear it down and run cleanups.

Run & build

CommandWhat it does
npm run devStart the dev server with hot reload at localhost:5173
npm run buildProduce a static dist/ folder ready to deploy anywhere
npm run previewPreview the production build locally

How to use

The rest of this page is the working knowledge for building an app: JSX, state and effects, lists, context, the store, and the router. Every API mirrors React, so most of it will already feel familiar.

JSX

JSX is HTML-like syntax that compiles to function calls. A few rules:

State with useState

useState gives a component memory. It returns the current value and a setter; calling the setter re-renders the component.

import { useState } from 'danio-js'

function Counter() {
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(count + 1)}>{count}</button>
}

When the next value depends on the previous one, pass a function โ€” it always sees the latest value, even across rapid updates:

setCount((n) => n + 1)

Side effects with useEffect

useEffect runs code after the DOM is updated โ€” timers, subscriptions, fetches. Return a function to clean up. The dependency array controls when it re-runs.

import { useState, useEffect } from 'danio-js'

function Clock() {
  const [now, setNow] = useState(() => new Date())

  useEffect(() => {
    const id = setInterval(() => setNow(new Date()), 1000)
    return () => clearInterval(id)   // cleanup on unmount
  }, [])                             // [] โ†’ run once

  return <time>{now.toLocaleTimeString()}</time>
}

Lists & keys

Render a list with .map(). Give each item a stable key โ€” a unique id from your data, not the array index โ€” so reordering moves DOM nodes instead of rebuilding them (which would lose focus, selection, and scroll position).

function TodoList({ todos }) {
  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  )
}
Danio warns you in development if a dynamic list is missing keys โ€” so you'll catch it before it bites.

The rest of the hooks

HookUse it for
useReducer(fn, init)State with more structure than a single value
useMemo(fn, deps)Cache an expensive computed value between renders
useCallback(fn, deps)Keep a function's identity stable (pair with memo)
useRef(initial)A mutable box that survives renders; holds DOM nodes
useLayoutEffect(fn, deps)Like useEffect, but before paint โ€” to measure or adjust layout
The one rule of hooks: call them in the same order every render โ€” never inside an if, a loop, or after an early return. Danio warns when the order changes.

Context

Pass values deep into the tree without threading props through every level.

import { createContext, useContext } from 'danio-js'

const Theme = createContext('light')

function App() {
  return <Theme.Provider value="dark"><Toolbar /></Theme.Provider>
}

function Toolbar() {
  const theme = useContext(Theme)  // 'dark'
  return <div className={theme}>โ€ฆ</div>
}

Performance: memo

By default, when a component re-renders, its children re-render too. memo skips a child whose props haven't changed (compared shallowly).

import { memo } from 'danio-js'

const Row = memo(function Row({ label }) {
  return <li>{label}</li>
})
memo is defeated by props that are new objects every render โ€” inline functions, object literals, arrays. Stabilise them with useCallback/useMemo, or reach for it only on expensive components and long lists.

Error boundaries

A thrown error would otherwise blank the whole page. An ErrorBoundary contains the failure to one subtree and shows a fallback instead. It catches errors in both rendering and effects.

import { ErrorBoundary } from 'danio-js'

<ErrorBoundary fallback={(err, reset) => (
  <div>Something broke: {err.message} <button onClick={reset}>Retry</button></div>
)}>
  <Dashboard />
</ErrorBoundary>

The store

For app-wide state, Danio ships a Redux-style store: a single state object, updated by pure reducers, read through selectors. Wrap your app in a StoreProvider, then use useSelector and useDispatch.

import { createStore, StoreProvider, useSelector, useDispatch } from 'danio-js'

function reducer(state = { count: 0 }, action) {
  switch (action.type) {
    case 'inc': return { count: state.count + 1 }
    default: return state
  }
}

const store = createStore(reducer)

function Count() {
  const count = useSelector((s) => s.count)
  const dispatch = useDispatch()
  return <button onClick={() => dispatch({ type: 'inc' })}>{count}</button>
}

render(<StoreProvider store={store}><Count /></StoreProvider>, root)

useSelector only re-renders when the slice it returns actually changes. combineReducers, applyMiddleware, and the thunk and logger middleware are all included for larger apps.

The router

The router maps the URL to a screen using the History API โ€” no page reloads. Wrap your app in Router, declare Routes, and navigate with Link.

import { Router, Routes, Route, Link, useParams } from 'danio-js'

function App() {
  return (
    <Router>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/todos/42">A todo</Link>
      </nav>
      <Routes>
        <Route path="/" component={Home} />
        <Route path="/todos/:id" component={TodoDetail} />
      </Routes>
    </Router>
  )
}

function TodoDetail() {
  const { id } = useParams()   // '42'
  return <h1>Todo {id}</h1>
}

Also available: useNavigate for programmatic navigation, useLocation, and useSearchParams.

Server rendering (SSR)

For SEO and a fast first paint, render your app to HTML on the server, then make it interactive on the client. Danio ships both halves.

On the server โ€” renderToString runs in plain Node with no DOM. It runs your components (so useState initial values, useMemo, and useContext all resolve) and returns HTML. Effects don't run and state doesn't change โ€” there's a single render.

// server.js
import { renderToString } from 'danio-js/server'

app.get('*', (req, res) => {
  const html = renderToString(<App />)
  res.send(`<div id="root">` + html + `</div>`)
})

On the client โ€” hydrate adopts the server markup instead of rebuilding it, reusing each node and attaching the parts HTML can't carry (event handlers, refs, form values).

// client.js
import { hydrate } from 'danio-js'
hydrate(<App />, document.getElementById('root'))
Render the same tree on both sides. If the markup and the components disagree, Danio warns in development and rebuilds the mismatched node on the client rather than crashing. Streaming SSR and React Server Components aren't supported yet.

Deploy

npm run build produces a static dist/ folder โ€” plain HTML, JS, and CSS. It hosts anywhere, no server required.

HostSettings
VercelFramework preset Other ยท build npm run build ยท output dist
Netlifybuild npm run build ยท publish directory dist
GitHub PagesDeploy dist/ via Actions (see this repo's workflow)
Any static hostUpload the contents of dist/
A Danio app is a single-page app, so configure your host to rewrite unknown paths to index.html โ€” e.g. Vercel { "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] }, Netlify /* /index.html 200.

API cheatsheet

ImportWhat you get
render, hydrate, unmountMount, hydrate server HTML, and tear down
useState, useReducer, useRefComponent state
useEffect, useLayoutEffectSide effects
useMemo, useCallback, memoPerformance
createContext, useContextContext
ErrorBoundaryContain render/effect errors
createStore, StoreProvider, useSelector, useDispatchStore
Router, Routes, Route, Link, useParams, useNavigateRouting
renderToString (from danio-js/server)Server rendering

Using Danio without a build step

JSX is a convenience, not a requirement. h is the same function the compiler calls, so you can write the tree by hand and open an HTML file directly โ€” no npm, no bundler.

<script type="module">
  import { h, render, useState } from 'https://esm.sh/danio-js'

  function Counter() {
    const [n, setN] = useState(0)
    return h('button', { onClick: () => setN(n + 1) }, 'clicked ', n)
  }
  render(h(Counter), document.body)
</script>