Docs
React hooks
@shipnoted/react holds the data and the logic. You write the markup, so it looks like your product and nothing else.
Install#
npm i @shipnoted/reactReact 18 or later. Wrap the part of your app that uses it once (or pass project to each hook and skip the wrapper):
import { ShipnotedProvider } from "@shipnoted/react"
<ShipnotedProvider project="your-slug" baseUrl="https://shipnoted.com">
<App />
</ShipnotedProvider>No API key is involved: the hooks use the same open API as the widget, which only reads public data and lets visitors vote, post and subscribe. baseUrl is your custom domain’s origin if you have one.
useChangelog#
const { releases, loading, error, hasMore, loadMore, loadingMore, unread, markSeen, refresh } =
useChangelog({ limit: 10, lang: "es" })releasesare newest first, withtitle,version,tags,summary, full Markdownbody,urlandpublishedAt. Withlang, translated titles and notes are returned where you made them.unreadcounts releases newer than the last time this browser calledmarkSeen(). Call it when the panel opens, and the badge clears.loadMore()fetches the next page and never repeats a release.
function WhatsNew() {
const { releases, unread, markSeen } = useChangelog({ limit: 5 })
return (
<details onToggle={(e) => e.currentTarget.open && markSeen()}>
<summary>What's new {unread > 0 && <span className="badge">{unread}</span>}</summary>
{releases.map((r) => <a key={r.id} href={r.url}>{r.title}</a>)}
</details>
)
}useFeedback#
const { items, voted, hasVoted, vote, submit, similar, loading, submitting, error } =
useFeedback({ status: "open", sort: "top", q: search })itemsis the public board, filtered bystatusandq(words in the title or body) and ordered bysort(topornew).vote(id)toggles your vote. The count changes at once and is corrected by the server’s answer; if the vote is refused it is put back anderrorsays why.submit({ title, body, email })posts an idea. It appears first and counts as your vote. Resolves to the new request, ornull(seeerror).similar(text)returns existing requests that say the same, so people vote instead of repeating. Call it as someone types.
function Board() {
const { items, vote, hasVoted } = useFeedback({ status: "open" })
return items.map((f) => (
<button key={f.id} aria-pressed={hasVoted(f.id)} onClick={() => vote(f.id)}>
▲ {f.votes} {f.title}
</button>
))
}Subscribe, reactions, theme#
useSubscribe
const { subscribe, status, error, busy } = useSubscribe()
await subscribe("ada@acme.com", ["fixed", "breaking"]) // only those types; [] means everything
// status: "idle" | "busy" | "sent" | "already" | "error"It starts a double opt-in: nobody is emailed until they confirm from their inbox.
useReactions
const { counts, mine, toggle } = useReactions(release)
<button onClick={() => toggle("🎉")}>🎉 {counts["🎉"] ?? 0}</button> // 👍 🎉 ❤️ 🔥 👀useProjectConfig
The project’s accent colour, theme variables and switches, if you want your UI to follow what you set in Design.
Server rendering#
Fetch the first page on the server and pass it as initialData: the first paint has content and no loading state. Both hooks accept it.
A server component (Next.js)
import { createPublicClient } from "@shipnoted/react/core"
const client = createPublicClient({ project: "your-slug", baseUrl: "https://shipnoted.com" })
const { releases } = await client.releases({ limit: 10 })
return <Changelog initialData={releases} />Signed-in customers#
Give the provider a token your server signed and votes and ideas count as that verified customer, with their plan and revenue (see Verified users). Pass a function if the token refreshes. A token the server rejects is dropped and the call retried anonymously, so a stale token never breaks the page.
<ShipnotedProvider project="your-slug" baseUrl="https://shipnoted.com" userToken={() => sessionStorage.getItem("sn_token") ?? undefined}>The core, without React#
The hooks are thin: all the behaviour lives in framework-free stores exported from @shipnoted/react/core. Each has getSnapshot() and subscribe(fn), which is what Vue, Svelte, Solid and plain scripts need.
import { createPublicClient, createChangelog, createFeedback } from "@shipnoted/react/core"
const client = createPublicClient({ project: "your-slug", baseUrl: "https://shipnoted.com" })
const log = createChangelog(client, { limit: 10 })
log.subscribe(() => render(log.getSnapshot()))
await log.refresh()Prefer our UI? Use the shadcn components, which are built on the same API.