The Golden Rule
"Users expect to find things the way they left them."
This is the only rule you need to understand DATAOS. Everything else follows from it.
When a user interacts with your application—filtering a list, typing in a search box, selecting a tab—they expect that state to persist. Not in JavaScript. Not in Redux. In the DOM itself.
Traditional React (Wrong)
const [filter, setFilter] = useState('')
// User types "hello"
// State is in JavaScript
// DOM shows "hello" as a reflection
// Two sources of truth → sync bugs
stateless (Correct)
// User types "hello"
// State is in <input value="hello">
// React reads it when needed
// One source of truth → impossible to desync
const state = useDomState({
filter: {
selector: '#filter',
extract: el => el.value
}
})
Analyzing Your DOM
Before writing any stateless code, analyze your UI. Ask: "What would a user expect to persist if they refreshed the page?"
The 3-Question Test
-
Is it visible?
If it's rendered in the DOM, it should be in the DOM, not duplicated in JavaScript.
-
Does it affect what's shown?
Filters, search terms, selected tabs—these determine what the user sees. Store them as data attributes or input values.
-
Would the user be surprised if it changed?
If yes, it's state. Put it in the DOM.
💡 Pro Tip: View Source
Open your browser's inspector. If you can see the state there (in attributes, values, classes), you're doing DATAOS correctly.
Identifying State in the DOM
State lives in attributes, not JavaScript variables. Here's where to look:
| UI Element | Where State Lives | Example |
|---|---|---|
| Text input | value or data-value |
<input value="search term"> |
| Checkbox | checked or data-checked |
<input type="checkbox" checked> |
| Selected tab | data-active or aria-selected |
<button data-active="true"> |
| Filter tags | data-filter on each tag |
<span data-filter="urgent"> |
| Visible/hidden | hidden attribute or data-hidden |
<div hidden> |
| Sort order | data-sort on container |
<div data-sort="date-desc"> |
Custom Data Attributes
Use data-* attributes for anything that's not a standard HTML attribute:
<!-- Filter state -->
<div data-filter-category="work" data-filter-priority="high">
<!-- View mode -->
<div data-view="grid">
<!-- Expanded/collapsed -->
<details data-expanded="true">
Backend Design
Your backend should receive and return state as simple data structures. No sessions. No cookies (for state). Just data.
The DATAOS Backend Pattern
Traditional Backend (Wrong)
// Backend maintains user session state
app.get('/cards', (req, res) => {
const filter = req.session.filter // State in server!
const cards = db.query(filter)
res.json(cards)
})
DATAOS Backend (Correct)
// Backend receives state, returns data
app.post('/cards/query', (req, res) => {
const { filter, sort, limit } = req.body // State from client!
const cards = db.query({ filter, sort, limit })
res.json(cards)
})
Key Principles
- Stateless functions: Each request contains all state needed to process it
- Explicit state: State is passed in request body/params, never implied
- Pure functions: Same input → same output, every time
- No server-side sessions for UI state: Session for auth, not for filters/sorting
Collecting State from the DOM
const manifest = {
filters: {
selector: '[data-filter]',
extract: el => el.dataset.filter
},
sort: {
selector: '[data-sort]',
extract: el => el.dataset.sort
},
search: {
selector: '#search',
extract: el => el.value
}
}
function loadCards() {
const state = extractDomState(manifest)
// Send state to backend
const response = await fetch('/cards/query', {
method: 'POST',
body: JSON.stringify(state)
})
return response.json()
}
Real-World Examples
Example 1: Filtered List
HTML
<!-- Filter controls -->
<div id="filters">
<button data-filter="all" data-active="true">All</button>
<button data-filter="urgent">Urgent</button>
<button data-filter="completed">Completed</button>
</div>
<!-- Cards -->
<div id="cards">
<div data-status="urgent">Urgent card</div>
<div data-status="completed">Done card</div>
</div>
React Component
const manifest = {
activeFilter: {
selector: '[data-active="true"]',
extract: el => el.dataset.filter
}
}
function CardList() {
const { activeFilter } = useDomState(manifest)
// Filter logic happens server-side or in rendering
// DOM already has the state!
return (
<div>
{/* Cards filtered by activeFilter */}
</div>
)
}
Example 2: Search with Debouncing
const manifest = {
search: {
selector: '#search-input',
extract: el => el.value
}
}
function SearchableList() {
const { search } = useDomState(manifest)
const [results, setResults] = useState([])
useEffect(() => {
const timer = setTimeout(() => {
// Send search state to backend
fetch('/search', {
method: 'POST',
body: JSON.stringify({ query: search })
})
.then(r => r.json())
.then(setResults)
}, 300)
return () => clearTimeout(timer)
}, [search])
return <div>{/* results */}</div>
}
Example 3: multicardz-style Tags
const manifest = {
tags: {
selector: '.tag',
extract: el => ({
value: el.dataset.tag,
type: el.dataset.type // 'user' or 'system'
})
}
}
function TaggedCards() {
const { tags } = useDomState(manifest)
// tags is an array of { value, type }
const userTags = tags.filter(t => t.type === 'user')
return <CardGrid tags={userTags} />
}
Common Patterns
Pattern: Active/Selected State
Use data-active or aria-selected for UI elements with selection:
<button data-tab="overview" data-active="true">Overview</button>
<button data-tab="details">Details</button>
Pattern: Multi-Select
Multiple elements can have the same attribute:
<span data-filter="urgent">Urgent</span>
<span data-filter="work">Work</span>
<span data-filter="personal">Personal</span>
// Extract all filters
{
filters: {
selector: '[data-filter]',
extract: el => el.dataset.filter
}
}
Pattern: Hidden Elements
Don't remove from DOM—hide with attributes:
<!-- Bad: removes from DOM -->
{showAdvanced && <AdvancedOptions />}
<!-- Good: keeps in DOM, toggles visibility -->
<div data-hidden={!showAdvanced}>
<AdvancedOptions />
</div>
Pattern: URL as State
URL params are DATAOS-compliant state:
// URL: /cards?filter=urgent&sort=date
const params = new URLSearchParams(window.location.search)
const filter = params.get('filter')
const sort = params.get('sort')
// Sync to DOM
document.querySelector('#filter').dataset.filter = filter
Pitfalls to Avoid
❌ Don't Duplicate State
// BAD: State in both DOM and useState
const [value, setValue] = useState('')
<input value={value} onChange={e => setValue(e.target.value)} />
// GOOD: State only in DOM
<input defaultValue="" />
// Read when needed:
const { value } = useDomState({ value: { selector: 'input', extract: el => el.value } })
❌ Don't Hide State in Closures
// BAD: State trapped in JavaScript closure
let currentFilter = 'all'
button.addEventListener('click', () => {
currentFilter = 'urgent'
})
// GOOD: State visible in DOM
button.addEventListener('click', () => {
button.dataset.filter = 'urgent'
})
❌ Don't Store Arrays in JavaScript
// BAD: Array only exists in JavaScript
const [tags, setTags] = useState(['work', 'urgent'])
// GOOD: Array represented as DOM elements
<div id="tags">
<span data-tag="work">work</span>
<span data-tag="urgent">urgent</span>
</div>
❌ Don't Use State for Derived Values
// BAD: Storing filtered results in state
const [filtered, setFiltered] = useState([])
// GOOD: Filter on render, driven by DOM state
const { filter } = useDomState(manifest)
const filtered = cards.filter(c => c.status === filter)