From RTK Query FULL
RTK Query and data-query share a model. Endpoints are declared up front in one place, with the URL, the verb, the body, and the response transform all stated rather than written into a fetch call. The differences are that there is no Redux here, and that the declaration covers more.
An API slice becomes a set of queries
RTK Query groups endpoints under one createApi with a shared baseUrl. Here each resource is its own named query, and the shared part moves to the origin-scoped header default.
// RTK Query
const api = createApi({
baseQuery: fetchBaseQuery({
baseUrl: '/api/',
prepareHeaders: (headers, { getState }) => {
headers.set('Authorization', 'Token ' + getState().auth.token);
return headers;
}
}),
tagTypes: ['Article'],
endpoints: (build) => ({
getArticles: build.query({
query: () => 'articles',
transformResponse: (r) => r.articles,
providesTags: ['Article']
}),
updateArticle: build.mutation({
query: ({ slug, ...patch }) => ({
url: `articles/${slug}`, method: 'PATCH', body: patch
}),
invalidatesTags: ['Article']
})
})
});
// WildflowerJS
wildflower.config({
headers: { self: () => ({ Authorization: 'Token ' + auth.token }) }
});
wildflower.query('articles', {
from: '/api/articles',
key: 'slug',
select: r => r.articles,
to: '/api/articles/:slug',
body: item => item
});
| RTK Query | WildflowerJS | Notes |
|---|---|---|
fetchBaseQuery({ baseUrl }) | write the path in from/to | No base to join; the URL is what you wrote. |
prepareHeaders | config({ headers }) or headers | Keyed by origin rather than by API slice. |
build.query({ query }) | from | A string, not a callback returning one. |
build.mutation({ query }) | to / create | { url, method, body } becomes the operation entry. |
transformResponse (query) | select | Same job, same place in the pipeline. |
transformResponse (mutation) | confirmation | Its absence also switches to refetch-instead-of-reconcile. |
providesTags / invalidatesTags | invalidateQueries(...names) | Coarser. See below. |
keepUnusedDataFor | nothing to set | Named queries are not collected; the result cache is a fixed size. |
| generated hooks | data-query on an element | No hook, provider, or store to configure. |
How the request URL is written
RTK's query is a callback that returns a request descriptor: query: ({ slug }) => ({ url: `articles/${slug}` }). Ours is the string itself, with :token segments filled at request time. For an author the two read almost identically, and the template literal you were writing becomes a token.
A tool reading your served source can resolve to: '/api/articles/:slug' by parsing it. It cannot resolve RTK's version without executing the callback. When the destination is data rather than code, you can read the set of URLs an application talks to, and the origins that receive its credentials, straight from the source.
Multi-verb endpoints
RTK's per-endpoint query callback lets one endpoint pick its verb at call time. Here that is a named operation, which keeps the destination fixed and the choice explicit at the call site.
to: {
favorite: { url: '/api/articles/:slug/favorite', method: 'POST',
confirmation: d => d.article },
unfavorite: { url: '/api/articles/:slug/favorite', method: 'DELETE',
confirmation: d => d.article }
}
wildflower.getQuery('articles').write('favorite', { slug, favorited: true });
Creates are separate, because a create is the one operation that creates a row rather than addressing an existing one. create takes the collection URL, the framework generates the temporary key, and the server's record replaces that key with the real one in place.
Invalidation is coarser here
Tags are RTK Query's strongest feature in this area, and there is no equivalent here. providesTags and invalidatesTags build a dependency graph, and a mutation invalidates exactly what it should with no bookkeeping at the call site.
Here the equivalent is await wildflower.invalidateQueries('articles', 'dashboard-counts'), which converges any number of named queries and resolves once every triggered refetch has settled. It takes names only, with no tags, patterns, or blanket form. That is less expressive and has fewer concepts to learn, and it works while an app has a few named queries rather than a generated endpoint per route. Much of what tags do is also unnecessary here, since a write that resolves without a confirmation refetches its own query already.
What you gain
No Redux, and no generated hooks. RTK Query requires the store, the provider, and a build step that can process the generated hook names. This is a script tag, and the binding is an attribute on the element that renders the data.
Optimistic updates without onQueryStarted. RTK's optimistic path asks you to write the patch and hold a patchResult so you can call .undo() in a catch. Here the optimistic apply and the rollback both come from the declaration, and the rollback is field-level, so overlapping writes on one row fail independently of each other.
Codegen remains possible. RTK Query can generate its API slices from an OpenAPI or GraphQL schema precisely because its endpoints are declared up front. The same is true here, more easily, since the output would be data rather than callbacks. Nothing ships for it today.