swr integration

This is an official integration for @tanstack/query. Here’s some quick info about it

Plus it enforces best practices and helps with plethora of complications you’d likely not even think about (read the motivation section 🤓 )

Motivation

Why should you use this integration? Imagine you got an api interface with multiple requests

const router = a.router({
  name: 'example',
  routes: [
    a.procedure('getAllUsers', async () => db.from('users').select('*')),
    a.procedure('getUserById', getUserById)
    // and 15 other procedures like this...
  ]
})

This works nicely on server and some basic client calls, but you decide to integrate swr to provide some caching on client and smoother optimistic updates.

Easiest way is to do this, is to append a route version of our api calls with useSWR:

// ❌ DON'T DO THIS!!!
routes: [
  a.procedure('getUserById', getUserById),
  a.procedure(
    'getUserByIdSWR',
    (id: string) => useSWR(['getUserById', id], () => getUserById(id))
  )
]

Luckily, this integration remedies all 5 of these issues! 🎉

Installation

# Depending on yur package manager:
pnpm add @hulla/api-swr
bun add @hulla/api-swr
npm install @hulla/api-swr
yarn add @hulla/api-swr

All the chapters presume you’ve done the setup as described in the Setup section below.

import { api } from '@hulla/api'
import { swr, mutation, queryKey } from '@hulla/api-query'
// note the package is tree-shakeable, so you can import only what you need
// to see which methods you need for which calls, check the table of contents to the right ->
// i.e. if you only need to do useSWR calls, just import { swr } from '@hulla/api'

const a = api()
const usersAPI = a.router({
  name: 'users',
  routes: [
    a.procedure('getUserById', getUserById),
    a.procedure('getUsersPage', getUsersPage),
    // ...
  ],
  adapters: { swr, mutation, queryKey }, // add the integration adapters here 🚀
})

Now we’ll go over individual integration methods and how they can be converted to swr calls.

swr

All the swr method (import { swr } from '@hulla/api-swr') does, is create a SWROptions array for you to re-use in your query related calls.

Consider the standard call we’d use in the base version of @hulla/api

usersAPI.call('getUserById', '123') // how you'd standardly call it
// => Promise<User>

To transform this into a query options config, we’ll just add a .swr before the .call method.

usersAPI.swr.call('getUserById', '123')
// => [
//      readonly [`${method}/${router}/getUserById', arg: '123'],
//      () => getUserById('123') }
//    ]

useSWR

We’ll start off with the most basic example - a standard useSWR call.

Accessing a swr query is as simple as accessing a standard call.

function App () {
  const [queryKey, queryFn] = usersAPI.swr.call('getUserById', '123')
  const { data, error } = useSWR(queryKey, queryFn)
}

useSWRInfinite

While you can cover most use-cases with just useSWR (including pagination), sometimes you do need infinite queries.

It works the same as useSWR, since the .swr just returns a key and function.

const [queryKey, queryFn] = usersAPI.swr.call('getUsersPage', { page: 1 })
const { data, size } = useSWRInfinite(queryKey, queryFn)

useSWRSubscription

For subscribing to real-time data sources, swr exports a handy useSWRSubscription hook.

It operates the same way, the only distinction being, that the route fn you pass to your procedure must match the swr subscribe type.

// this is how swr defines it
subscribe: (key: Key, options: {
  next: (error?: Error | null, data?: Data) => void
}) => () => void): { data?: Data, error?: Error }

As long as the given fn matches this type, you can use the same syntax as above

preload (Prefetching)

Same as a swr call

const [queryKey, queryFn] = usersAPI.swr.call('getUserById', '123')
preload(queryKey, queryFn)

mutation

Unlike in the @tanstack/query integration, the mutate method of swr has the same arguments as swr.

Technically you could just use the .swr call and it will have the same effect as calling .mutation (as we re-export the same function under the hood anyways)

But if you want to be explicit in your code (like me), you can use the mutation method instead.

useSWRMutation

import useSWRMutation from 'swr/mutation'

const [mutationKey, mutationFn] = usersAPI.mutation.call('addUser', { id: '123', name: 'John' })
const { trigger } = useSWRMutation(mutationKey, mutationFn)

Note, the mutate function is going to be described in the next chapter, because it behaves slightly differently.

queryKey

And last but not least, we have the queryKey method. This is a utility function that helps you decode your queryKeys.

One thing you need to understand, the package encodes queryKey for you. While it may be a small learning curve, from typing out your queryKeys manually, it’s a huge benefit in the long run.

Especially if you have a large codebase, and when you do in usersAPI.call('create', ...) you don’t want it to invalidate queries with other create initial keys (i.e. postsAPI.call('create', ...)). Or even worse, if you have a GET and POST method with same url.

That’s why the package encodes the queryKey for you, to save you a lot of hassle.

mutate (Revalidation)

swr apart from useSWRMutation also exports (least to me, somewhat confusingly named) mutate function, which invalidates query cache for a given key. Unlike useSWRMutation which expects entire length of data, mutate only expects the key, and optionally, some other data.

const { mutate } = useSWRConfig()
const [mutationKey, ...data] = usersAPI.queryKey('getUserById', '123')
mutate(mutationKey, data)

If youre using a bound mutation, you don’t have to pass anything, hence you don’t need to use queryKey at all.

Caveat: queryKey vs swr.call()[0]

If you’re asking yourself:

Since swr integration method already returns query key as first array item, why can’t i just use that instead?

Well, first off, congratulate yourself as very observant and pat yourself on the back for a very good question.

Second of all, there’s a good reason for it — When doing stuff like Revalidation you may not need to always pass the entire length of query key. Cosnider this example

export const tAPI = a.router({
  name: 'tricky',
  routes: [
    a.procedure('getById', async (id: string) => getUserById(id)),
  ],
  adapters: { swr, queryKey },
})

Now depending on our usecase

// in a useQuery
useSWR({
  ...tAPI.swr.call('getById', '123') // ✅ Here the `id` must be passed!
//...tAPI.swr.call('getById') // ❌ (TypeError) This doesn't make sense, what `id` do I fetch?
})

// however, in mutate (revalidation)
const [mutationKey, ...data] = tAPI.queryKey('getById', '123') // ✅ Here the `id` was be passed!
mutate(mutationKey, data)

const [mutationKey] = tAPI.queryKey('getById') // ✅ Here the `id` wasn't passed, also valid!
mutate(mutationKey) 

That’s an important distinction to make in terms of type-safety. Essentially swr.call necessitates, that you pass all the defined arguments in the query function, meanwhile queryKey just requires the name of the route (in this case procedure.)

methodtype signatureexample
swrAll parameters must be definedreadonly ['getById', string]
queryKeyFirst parameter must be defined, rest doesn’t have to be be definedreadonly ['getById', string | undefined]

All other methods

If you can’t find a specific method that swr implements, it’s likely that you can just make it work with combination of swr, mutation and queryKey methods, as long as you understand what they do.

Least to my knowledge, right now @hulla/api-swr is 100% compatible with all swr features.

However if you’re still struggling to make something work, or a new version of swr was added which no longer works with the integration, please check the links to the right, and either submit an issue or even update, contribute to the package / docs if you found the solution out and help me and others out. ❤️

Package metadata

MetaDescription
Package name@hulla/api-swr
Dependencies0
Dev Dependencies1: @hulla/api
Integration typeAdapters
Package size430 B (0.43 KB)