Examples

Build with Better-State

Real-world patterns showing what you can build. Copy, paste, ship.

Synced Counter

ReactBeginner

The simplest possible example — a counter that stays in sync across every connected tab.

App.tsx
1import { BetterStateProvider, useBetterState } from '@better-state/react'
2 
3function Counter() {
4 const [count, , updateCount] = useBetterState('counter', 0)
5 
6 return (
7 <div>
8 <h1>{count}</h1>
9 <button onClick={() => updateCount(n => n + 1)}>+1</button>
10 <button onClick={() => updateCount(n => n - 1)}>-1</button>
11 </div>
12 )
13}
14 
15export default function App() {
16 return (
17 <BetterStateProvider url="http://localhost:3001" apiKey="your-key">
18 <Counter />
19 </BetterStateProvider>
20 )
21}

PollSync — Live Voting

ReactProductionAdvancedSource

A real-time voting app. Multiple users vote simultaneously with optimistic UI. Uses update() for atomic increments to prevent race conditions.

Poll.tsx
1import { useBetterState, useConnectionStatus } from '@better-state/react'
2 
3interface PollData {
4 question: string
5 options: { id: string; text: string; votes: number }[]
6 totalVotes: number
7}
8 
9function Poll() {
10 const [poll, , updatePoll] = useBetterState<PollData>('poll', DEFAULT_POLL)
11 const status = useConnectionStatus()
12 
13 function vote(optionId: string) {
14 updatePoll((prev) => ({
15 ...prev,
16 options: prev.options.map((o) =>
17 o.id === optionId ? { ...o, votes: o.votes + 1 } : o
18 ),
19 totalVotes: prev.totalVotes + 1,
20 }))
21 }
22 
23 return (
24 <div>
25 {status !== 'connected' && <Banner>{status}</Banner>}
26 <h2>{poll.question}</h2>
27 {poll.options.map((o) => (
28 <button key={o.id} onClick={() => vote(o.id)}>
29 {o.text}: {o.votes} votes
30 </button>
31 ))}
32 </div>
33 )
34}

Key patterns

  • updatePoll(prev => ...) for atomic updates — prevents race conditions with concurrent voters
  • useConnectionStatus() to show real-time connectivity state
  • Complex nested state (object with arrays) synced seamlessly

Collaborative Todo List

ReactIntermediate

A shared todo list where multiple users can add, toggle, and remove items in real-time.

Todos.tsx
1import { useBetterState } from '@better-state/react'
2 
3interface Todo {
4 id: string
5 text: string
6 done: boolean
7}
8 
9function Todos() {
10 const [todos, setTodos, updateTodos] = useBetterState<Todo[]>('todos', [])
11 
12 function addTodo(text: string) {
13 const id = crypto.randomUUID()
14 setTodos([...todos, { id, text, done: false }])
15 }
16 
17 function toggleTodo(id: string) {
18 updateTodos((list) =>
19 list.map((t) => (t.id === id ? { ...t, done: !t.done } : t))
20 )
21 }
22 
23 function removeTodo(id: string) {
24 updateTodos((list) => list.filter((t) => t.id !== id))
25 }
26 
27 return (
28 <ul>
29 {todos.map((t) => (
30 <li key={t.id}>
31 <input type="checkbox" checked={t.done} onChange={() => toggleTodo(t.id)} />
32 <span>{t.text}</span>
33 <button onClick={() => removeTodo(t.id)}>x</button>
34 </li>
35 ))}
36 </ul>
37 )
38}

set() vs update()

  • setTodos() for adding — the new ID is computed outside the function, avoiding serialization issues
  • updateTodos() for toggle/remove — pure transformations safe to serialize

Vanilla JS — No Framework

Vanilla JSBeginner

Better-State works without React. Here's a plain JavaScript counter using the core client SDK.

main.js
1import { createClient } from '@better-state/client'
2 
3const client = createClient('http://localhost:3001', {
4 apiKey: 'your-key',
5})
6 
7const counter = client.state('counter', 0)
8 
9const display = document.getElementById('count')
10const btn = document.getElementById('increment')
11 
12counter.subscribe((value) => {
13 display.textContent = value
14})
15 
16btn.addEventListener('click', () => {
17 counter.update((n) => n + 1)
18})

Built-in Studio Dashboard

Every Better-State server ships with a built-in Studio. See all state keys, watch live mutations, and debug issues in real-time.

StudioBuilt-in
  1. 1.Start the server: npx @better-state/server
  2. 2.Open localhost:3001/studio in your browser
  3. 3.See all state keys, current values, and a live feed of every mutation as it happens

The Studio connects via the same WebSocket protocol your app uses — no extra setup needed.