design guide

How to Build with stateless

Learn DATAOS principles and how to design your frontend and backend for zero-state architecture

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

  1. Is it visible?

    If it's rendered in the DOM, it should be in the DOM, not duplicated in JavaScript.

  2. 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.

  3. 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)