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"
}
}
@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
| Command | What it does |
|---|---|
npm run dev | Start the dev server with hot reload at localhost:5173 |
npm run build | Produce a static dist/ folder ready to deploy anywhere |
npm run preview | Preview 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:
- Attributes are camelCase:
onClick,tabIndex,htmlFor. BothclassandclassNamework. styletakes an object:style={{ color: 'red', fontSize: 16 }}โ numbers getpxwhere it makes sense.- A component returns a single root. Wrap siblings in a fragment โ
<>โฆ</>โ to avoid an extra wrapper. {expression}embeds any value.null,undefined, andfalserender nothing, so{isOpen && <Panel/>}works.
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>
)
}
The rest of the hooks
| Hook | Use 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 |
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'))
Deploy
npm run build produces a static dist/ folder โ plain HTML, JS, and CSS. It hosts anywhere, no server required.
| Host | Settings |
|---|---|
| Vercel | Framework preset Other ยท build npm run build ยท output dist |
| Netlify | build npm run build ยท publish directory dist |
| GitHub Pages | Deploy dist/ via Actions (see this repo's workflow) |
| Any static host | Upload the contents of dist/ |
index.html โ e.g. Vercel { "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] }, Netlify /* /index.html 200.API cheatsheet
| Import | What you get |
|---|---|
render, hydrate, unmount | Mount, hydrate server HTML, and tear down |
useState, useReducer, useRef | Component state |
useEffect, useLayoutEffect | Side effects |
useMemo, useCallback, memo | Performance |
createContext, useContext | Context |
ErrorBoundary | Contain render/effect errors |
createStore, StoreProvider, useSelector, useDispatch | Store |
Router, Routes, Route, Link, useParams, useNavigate | Routing |
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>