Framework usage
Affordant's core is plain functions over plain data — no hooks, no stores, no framework adapter. The same can / actionFor / follow calls work everywhere; only the templating syntax changes.
Vanilla
import { can, actionFor, follow, type HateoasResource } from 'affordant'
type Order = { id: string; total: number; status: string }
const order: HateoasResource<Order> = await fetch('/orders/8f3a2c').then(r => r.json())
if (can(order, 'cancel')) {
await follow(actionFor(order, 'cancel')!, {
token: () => localStorage.getItem('token'),
body: { reason: 'changed my mind' },
})
}React
{can(order, 'cancel') && (
<button onClick={() => follow(actionFor(order, 'cancel')!, { token })}>
Cancel order
</button>
)}Vue
<button v-if="can(order, 'cancel')" @click="cancel">Cancel order</button>Svelte
{#if can(order, 'cancel')}
<button onclick={cancel}>Cancel order</button>
{/if}Express, on the server
@affordant/express's sendResource serialises the envelope to JSON unchanged, and also mirrors every link as a combined RFC 8288 Link header — _self as rel="self" first, then each _actions rel in order. Clients and proxies that read Link headers see the same affordances as the body.
Link: </orders/8f3a2c>; rel="self", </orders/8f3a2c/cancel>; rel="cancel"You never need an adapter
can and actionFor are pure, synchronous, null-safe reads over the resource you already hold. There is nothing to wire into a component lifecycle — you call them inline wherever you render. follow is a single fetch call returning a Response, so it composes with whatever data layer you already use (TanStack Query, SWR, a plain await, a Svelte store…). The vanilla examples above are a complete, supported way to use Affordant in any framework.
React, with hooks (optional)
If you want ergonomics in React, @affordant/react wraps the same calls as hooks. It is opt-in; affordant never depends on it.
npm install @affordant/reactimport { useAffordance, useFollow } from '@affordant/react'
function CancelButton({ order }) {
const cancel = useAffordance(order, 'cancel') // { can, action } — pure gating
const { run, running } = useFollow() // the Promise invoker, with state
if (!cancel.can) return null
return (
<button disabled={running} onClick={() => run(cancel.action!, { token })}>
Cancel order
</button>
)
}Vue, with composables (optional)
If you want ergonomics in Vue, @affordant/vue wraps the same calls as composables. It is opt-in; affordant never depends on it. useAffordance returns reactive computeds, so the gating stays in sync as your data loads or changes.
npm install @affordant/vue<script setup lang="ts">
import { useAffordance, useFollow } from '@affordant/vue'
const props = defineProps<{ order: HateoasResource<Order> }>()
const cancel = useAffordance(() => props.order, 'cancel')
const { run, running } = useFollow()
</script>
<template>
<button
v-if="cancel.can.value"
:disabled="running"
@click="run(cancel.action.value!, { token })"
>
Cancel order
</button>
</template>Using Effect
follow is a plain promise-returning function, so it drops into Effect (or any effect system) with a one-line wrap — Effect.tryPromise(() => follow(action, init)). That interop is yours to add if you want it; Affordant never ships an Effect dependency.
Svelte
No adapter package exists for Svelte yet — and you don't need one. The vanilla snippets above are the whole story. If hooks-style ergonomics are wanted there too, they'd ship as their own opt-in package, never as a dependency of the core.