User Dashboard
A dashboard showing a list of users with individual status indicators and aggregate stats. Demonstrates atomFamily for dynamic collections, selectorFamily for per-user derived state, and selector for aggregates.
Shared state
import { atom, atomFamily, selector, selectorFamily } from "valdres"
export type User = {
id: string
name: string
status: "online" | "away" | "offline"
lastSeen: number
}
export const userAtom = atomFamily<string, User>()
export const userIdsAtom = atom<string[]>([])
// Per-user derived state
export const userDisplaySelector = selectorFamily<string, string>(id => get => {
const user = get(userAtom(id))
const statusEmoji = { online: "●", away: "◐", offline: "○" }
return `${statusEmoji[user.status]} ${user.name}`
})
// Aggregate stats
export const onlineCountSelector = selector(get => {
const ids = get(userIdsAtom)
return ids.filter(id => get(userAtom(id)).status === "online").length
})
export const userStatsSelector = selector(get => {
const ids = get(userIdsAtom)
const users = ids.map(id => get(userAtom(id)))
return {
total: users.length,
online: users.filter(u => u.status === "online").length,
away: users.filter(u => u.status === "away").length,
offline: users.filter(u => u.status === "offline").length,
}
})
// Actions
export function addUser(store, name) {
const id = crypto.randomUUID()
store.txn(set => {
set(userAtom(id), { id, name, status: "online", lastSeen: Date.now() })
set(userIdsAtom, ids => [...ids, id])
})
}
export function setStatus(store, id, status) {
store.set(userAtom(id), prev => ({
...prev,
status,
lastSeen: Date.now(),
}))
}
export function removeUser(store, id) {
store.txn(set => {
set(userIdsAtom, ids => ids.filter(i => i !== id))
})
}
Fine-grained updates
When one user's status changes, only the component rendering that specific user re-renders. The stats bar also updates since it depends on all users — but the other user cards don't.React
import { useValue, useStore } from "valdres-react"
import { userAtom, userIdsAtom, userStatsSelector, addUser, setStatus, removeUser } from "./state"
function Dashboard() {
const store = useStore()
const stats = useValue(userStatsSelector)
const ids = useValue(userIdsAtom)
return (
<div>
<div className="stats-bar">
<span>{stats.total} users</span>
<span>{stats.online} online</span>
<span>{stats.away} away</span>
<span>{stats.offline} offline</span>
</div>
<button onClick={() => addUser(store, prompt("Name?") || "User")}>
Add User
</button>
<div className="user-grid">
{ids.map(id => <UserCard key={id} id={id} />)}
</div>
</div>
)
}
function UserCard({ id }) {
const store = useStore()
const user = useValue(userAtom(id))
const statusColors = {
online: "#22c55e",
away: "#eab308",
offline: "#94a3b8",
}
return (
<div className="user-card">
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<span style={{
width: 8, height: 8, borderRadius: "50%",
backgroundColor: statusColors[user.status],
}} />
<strong>{user.name}</strong>
</div>
<div>
<select value={user.status}
onChange={e => setStatus(store, id, e.target.value)}>
<option value="online">Online</option>
<option value="away">Away</option>
<option value="offline">Offline</option>
</select>
<button onClick={() => removeUser(store, id)}>Remove</button>
</div>
</div>
)
}
Vue
<script setup>
import { useValue, useStore } from "valdres-vue"
import { userAtom, userIdsAtom, userStatsSelector, addUser, setStatus, removeUser } from "./state"
const store = useStore()
const stats = useValue(userStatsSelector)
const ids = useValue(userIdsAtom)
const statusColors = { online: "#22c55e", away: "#eab308", offline: "#94a3b8" }
</script>
<template>
<div>
<div class="stats-bar">
<span>{{ stats.total }} users</span>
<span>{{ stats.online }} online</span>
<span>{{ stats.away }} away</span>
</div>
<button @click="addUser(store, prompt('Name?') || 'User')">Add User</button>
<div class="user-grid">
<UserCard v-for="id in ids" :key="id" :id="id" />
</div>
</div>
</template>
<!-- UserCard.vue -->
<script setup>
import { useValue, useStore } from "valdres-vue"
import { userAtom, setStatus, removeUser } from "./state"
const props = defineProps(["id"])
const store = useStore()
const user = useValue(userAtom(props.id))
const statusColors = { online: "#22c55e", away: "#eab308", offline: "#94a3b8" }
</script>
<template>
<div class="user-card">
<div style="display: flex; align-items: center; gap: 8px">
<span :style="{ width: '8px', height: '8px', borderRadius: '50%', backgroundColor: statusColors[user.status] }" />
<strong>{{ user.name }}</strong>
</div>
<select :value="user.status" @change="setStatus(store, id, $event.target.value)">
<option value="online">Online</option>
<option value="away">Away</option>
<option value="offline">Offline</option>
</select>
<button @click="removeUser(store, id)">Remove</button>
</div>
</template>
Svelte
<script>
import { watch, readable } from "valdres-svelte"
import { getValdresContext } from "valdres-svelte"
import { userAtom, userIdsAtom, userStatsSelector, addUser, setStatus, removeUser } from "./state"
const store = getValdresContext()
const ids = readable(userIdsAtom)
const stats = readable(userStatsSelector)
const statusColors = { online: "#22c55e", away: "#eab308", offline: "#94a3b8" }
</script>
<div>
<div class="stats-bar">
<span>{stats.value.total} users</span>
<span>{stats.value.online} online</span>
<span>{stats.value.away} away</span>
</div>
<button onclick={() => addUser(store, prompt("Name?") || "User")}>Add User</button>
<div class="user-grid">
{#each ids.value as id (id)}
{@const user = watch(userAtom(id))}
<div class="user-card">
<div style="display:flex;align-items:center;gap:8px">
<span style:background-color={statusColors[user.value.status]}
style="width:8px;height:8px;border-radius:50%" />
<strong>{user.value.name}</strong>
</div>
<select value={user.value.status}
onchange={e => setStatus(store, id, e.target.value)}>
<option value="online">Online</option>
<option value="away">Away</option>
<option value="offline">Offline</option>
</select>
<button onclick={() => removeUser(store, id)}>Remove</button>
</div>
{/each}
</div>
</div>
Solid
import { For } from "solid-js"
import { createValue, useStore } from "valdres-solid"
import { userAtom, userIdsAtom, userStatsSelector, addUser, setStatus, removeUser } from "./state"
function Dashboard() {
const store = useStore()
const stats = createValue(userStatsSelector)
const ids = createValue(userIdsAtom)
return (
<div>
<div class="stats-bar">
<span>{stats().total} users</span>
<span>{stats().online} online</span>
<span>{stats().away} away</span>
</div>
<button onClick={() => addUser(store, prompt("Name?") || "User")}>
Add User
</button>
<div class="user-grid">
<For each={ids()}>
{id => <UserCard id={id} />}
</For>
</div>
</div>
)
}
function UserCard(props) {
const store = useStore()
const user = createValue(userAtom(props.id))
const statusColors = { online: "#22c55e", away: "#eab308", offline: "#94a3b8" }
return (
<div class="user-card">
<div style={{ display: "flex", "align-items": "center", gap: "8px" }}>
<span style={{
width: "8px", height: "8px", "border-radius": "50%",
"background-color": statusColors[user().status],
}} />
<strong>{user().name}</strong>
</div>
<select value={user().status}
onChange={e => setStatus(store, props.id, e.target.value)}>
<option value="online">Online</option>
<option value="away">Away</option>
<option value="offline">Offline</option>
</select>
<button onClick={() => removeUser(store, props.id)}>Remove</button>
</div>
)
}
Angular
import { Component } from "@angular/core"
import { injectValue, injectStore } from "valdres-angular"
import { userAtom, userIdsAtom, userStatsSelector, addUser, setStatus, removeUser } from "./state"
@Component({
selector: "user-dashboard",
template: `
<div class="stats-bar">
<span>{{ stats().total }} users</span>
<span>{{ stats().online }} online</span>
<span>{{ stats().away }} away</span>
</div>
<button (click)="handleAdd()">Add User</button>
<div class="user-grid">
@for (id of ids(); track id) {
<user-card [userId]="id" />
}
</div>
`,
})
export class UserDashboard {
store = injectStore()
stats = injectValue(userStatsSelector)
ids = injectValue(userIdsAtom)
handleAdd() {
addUser(this.store, prompt("Name?") || "User")
}
}
@Component({
selector: "user-card",
inputs: ["userId"],
template: `
<div class="user-card">
<div style="display:flex;align-items:center;gap:8px">
<span [style.background-color]="colors[user().status]"
style="width:8px;height:8px;border-radius:50%"></span>
<strong>{{ user().name }}</strong>
</div>
<select [value]="user().status" (change)="onStatus($event)">
<option value="online">Online</option>
<option value="away">Away</option>
<option value="offline">Offline</option>
</select>
<button (click)="onRemove()">Remove</button>
</div>
`,
})
export class UserCard {
store = injectStore()
userId!: string
user = injectValue(userAtom(this.userId))
colors = { online: "#22c55e", away: "#eab308", offline: "#94a3b8" }
onStatus(e: Event) { setStatus(this.store, this.userId, (e.target as HTMLSelectElement).value) }
onRemove() { removeUser(this.store, this.userId) }
}
Key takeaways
- Dynamic collections —
atomFamily+atom<string[]>for IDs is the idiomatic Valdres pattern for lists. - Per-item derived state —
selectorFamilycomputes derived values per user without recomputing for unrelated users. - Aggregate selectors — the stats bar depends on all user atoms but only re-renders when an actual status changes.
- Transactions — adding and removing users atomically updates both the family and the ID list.