Creates a value that an HTTP request fills. The request is re-sent whenever a signal it reads changes. Returns [value, action]: the value getter, and an action that re-sends the request and carries its pending and error state.
import { createQuery, http } from '@barefootjs/client'
const [value, action] = createQuery<T>(
fn: () => HttpDescriptor<T>,
options?: { initial?: T; ttl?: number },
)
value() // T when `initial` is a required prop, else T | undefined
action() // re-send now; Promise<T>
action.isPending() // the last send has not settled
action.error() // the last send's error; cleared by the next success'use client'
import { createQuery, createSignal, http } from '@barefootjs/client'
type Post = { id: number; title: string }
export function PostList(props: { posts: Post[] }) {
const [page, setPage] = createSignal(1)
const [posts, fetchPosts] = createQuery(
() => http.get<Post[]>('/api/posts', { page: page() }),
{ initial: props.posts },
)
return (
<div aria-busy={fetchPosts.isPending()}>
{fetchPosts.error() ? <p role="alert">Failed to load</p> : null}
<ul>{posts().map((post) => <li key={post.id}>{post.title}</li>)}</ul>
<button onClick={() => setPage(page() + 1)}>Next page</button>
</div>
)
}The value survives a refetch and a failure: while the next page loads, the previous posts stay visible, and isPending() / error() describe the request, not the value.
#The request function
The first argument is a function, like a memo or effect body. The signals it reads are its dependencies. The compiler emits it into the client JS only, with prop reads kept live. It never runs on the server, on any adapter.
Descriptors only. The function must return an http request descriptor: http.get(url, params?), http.head, http.query, http.post, http.put, http.patch or http.delete. A descriptor is plain data, so building one sends nothing, and the method, URL and body form the cache key. A function that returns anything else, such as a Promise from your own client, throws when it runs. Responses are parsed as JSON, and a success with an empty body (such as 204 No Content) resolves to undefined; a non-2xx response rejects with HttpError (status, body).
One send per tick. Several writes to the function's dependencies in one handler can run the function more than once, but only the last descriptor of the tick is sent, at the end of the tick. A dependency shared through two memos runs it once.
#The two SSR modes
The value on the server comes from initial, never from a request.
- Mode A: the server already has the data. Pass
initialfrom a required prop. The server renders the list, the value's type isT, and the client sends nothing on mount:initialenters the cache as fresh forttl(default 15 s). - Mode B: the server renders a shell. Pass
initialfrom an optional prop, or leave it out. When it isundefined, the value's type isT | undefined, the server renders the "no value" branch, and the client sends the request after hydration.
'use client'
import { createQuery, http } from '@barefootjs/client'
import { Skeleton } from './skeleton'
type Post = { id: number; title: string }
export function Feed(props: { posts?: Post[] }) {
const [posts] = createQuery(() => http.get<Post[]>('/api/posts'), { initial: props.posts })
return !posts() ? <Skeleton /> : <ul>{posts()!.map((p) => <li key={p.id}>{p.title}</li>)}</ul>
}Both modes are the same component. Which one a request takes is whether the parent passed the prop.
#`initial` is a result, not a placeholder
initial claims "this is what the request returns". Writing initial: [] to avoid undefined tells the query the server already fetched an empty list, so it suppresses the request. For a placeholder, fold the value instead: posts() ?? [], or a memo over it.
#Pending and error in the template
On the server, isPending() is false and error() is undefined, because no request runs there. The compiler seeds these values where they render exactly as the Hono reference does:
- as a condition:
{fetchPosts.error() ? <Alert/> : null},{fetchPosts.isPending() ? <Spinner/> : null}; isPending()in an ARIA boolean-state attribute or an HTML boolean attribute:aria-busy={fetchPosts.isPending()},disabled={fetchPosts.isPending()}.
Any other template read, such as {fetchPosts.error()} as text or title={fetchPosts.isPending()}, is refused with BF117. Read it inside a condition, in an event handler or effect, or defer it with /* @client */.
#Caveats
- Destructure the call as
const [value, action] = createQuery(…).[value]and[, action]also work; anything longer, or a third argument, is BF115 / BF116. - Write the call in the component. Wrapping
createQueryin a helper sends it through the reactive-factory inliner and its constraints; share the descriptor instead:const postsAt = (page: number) => http.get('/api/posts', { page }). - A write that changes the data is a
createMutation. Itsinvalidatesoption marks matching queries stale, and a live query re-sends at once. bf debug graphshows the query with edges from the signals its request function reads.
See spec/async.md §7 for the full model.