1.0.0-beta.1 • Published 2 years ago

@chiefkoshi/trpc-swr v1.0.0-beta.1

Weekly downloads
-
License
MIT
Repository
github
Last release
2 years ago

trpc-swr

trpc-swr

tRPC-ified SWR hooks

Installation

npm install trpc-swr @trpc/client

Usage

First, create your fully typed hooks using your router type:

// trpc.ts
import { createSWRHooks } from 'trpc-swr'
// `import type` ensures this import is fully erased at runtime
import type { AppRouter } from './router'

export const trpc = createSWRHooks<AppRouter>()

Then, add the trpc.TRPCProvider to your root App component:

// _app.tsx
import { createTRPCClient } from '@trpc/client'
import { trpc } from '../utils/trpc'

const App = () => {
	// create a tRPC vanilla client
	// see https://trpc.io/docs/vanilla
	// note that you should pass data transformers (https://trpc.io/docs/data-transformers) here
	const [client] = useState(() =>
		createTRPCClient({ url: 'http://localhost:3000/api/trpc' })
	)

	return (
		<trpc.TRPCProvider client={client}>
			<Component {...pageProps} />
		</trpc.TRPCProvider>
	)
}

Tip: For SWR's global configuration, wrap this provider with SWRConfig.

useSWR

Now use trpc to query in a component:

// profile.tsx
import { trpc } from './trpc'

const Profile = (props: { userId: string }) => {
	const { data, isValidating } = trpc.useSWR(['user.get', { id: props.userId }])

	return (
		<div>
			Name: {!data && isValidating
				? 'loading...'
				: data
				? data.name
				: 'User does not exist'}
		</div>
	)
}

trpc.useSWR functions the same and accepts all the options that SWR's useSWR hook does. It is only a very small wrapper that adds tRPC types and creates a fetcher using tRPC's vanilla client.

Mutations

You can use trpc.useContext to get a tRPC client for mutations:

// profile.tsx
import { trpc } from './trpc'

const Profile = (props: { userId: string }) => {
	// get `mutate` from trpc.useSWR
	// this is a bound mutate (https://swr.vercel.app/docs/mutation#bound-mutate)
	const { data, mutate, isValidating } = trpc.useSWR(['user.get', {
		id: props.userId,
	}])
	const { client } = trpc.useContext()

	return (
		<div>
			<div>
				Name: {!data && isValidating
					? 'loading...'
					: data
					? data.name
					: 'User does not exist'}
			</div>

			<button
				onClick={() => {
					// you would typically get this from user input
					// but it is hardcoded here to simplify the example
					const newName = 'Jack'

					// `mutate` revalidates the `user.get` key above
					// so it is refetched after the mutation is complete
					mutate(
						() => {
							return client.mutation('user.changeName', {
								id: props.userId,
								newName,
							})
						}, // use optimisticData to show new name before mutation completes
						{ optimisticData: { name: newName } },
					)
				}}
			>
			</button>
		</div>
	)
}

You can also use trpc.useContext to get a mutate function which is the same as SWR's global mutate. However, you will have the pass the same key, meaning the query path and input that you passed to useSWR. Here it is with the same example as above:

// profile.tsx
import { trpc } from './trpc'

const Profile = (props: { userId: string }) => {
	const { data, isValidating } = trpc.useSWR(['user.get', {
		id: props.userId,
	}])

	// get `mutate` from `trpc.useContext`
	const { client, mutate } = trpc.useContext()

	return (
		<div>
			<div>
				Name: {!data && isValidating
					? 'loading...'
					: data
					? data.name
					: 'User does not exist'}
			</div>

			<button
				onClick={() => {
					const newName = 'Jack'

					mutate(
						// must pass in exact same query path and input
						// to revalidate the query key
						// note that you can use `matchMutate` to
						// revalidate query keys with the same path
						['user.get', { id: props.userId }],
						() => {
							return client.mutation('user.changeName', {
								id: props.userId,
								newName,
							})
						},
						{ optimisticData: { name: newName } },
					)
				}}
			>
			</button>
		</div>
	)
}

useSWRInfinite

trpc-swr also provides a useSWRInfinite wrapper. Create your typed useSWRInfinite:

// trpc.ts
import { createSWRHooks } from 'trpc-swr'
import { getUseSWRInfinite } from 'trpc-swr/infinite'

// `import type` ensures this import is fully erased at runtime
import type { AppRouter } from './router'

export const trpc = createSWRHooks<AppRouter>()
export const useSWRInfinite = getUseSWRInfinite<AppRouter>()

This requires using getUseSWRInfinite and passing in the AppRouter type again, so we can take full advantage of tree shaking and remove the functions that your app does not use.

Now use it in a component:

// users.tsx
import { useSWRInfinite } from './trpc'

const Users = () => {
	const { data, size, setSize } = useSWRInfinite(
		// pass in path
		'user.get',
		(index, previousPageData) => {
			if (index !== 0 && !previousPageData) return null

			// return a value for the input of the path you passed
			// `user.get` in this case
			return [{ id: index }]
		},
	)

	if (!data) {
		return <div>Loading...</div>
	}

	return (
		<>
			<div>
				{data.map((user) => {
					return <p key={user.name}>{user.name}</p>
				})}
			</div>

			<button onClick={() => setSize(size + 1)}>Load More Users</button>
		</>
	)
}

Utility

matchMutate

The matchMutate utility allows you to invalidate query keys that match a tRPC route. Create your typed useMutateMutate function:

// trpc.ts
import { createSWRHooks, getUseMatchMutate } from 'trpc-swr'
// `import type` ensures this import is fully erased at runtime
import type { AppRouter } from './router'

export const trpc = createSWRHooks<AppRouter>()
export const useMatchMutate = getUseMatchMutate<AppRouter>()

Now use it in a component:

import { trpc, useMatchMutate } from './trpc'

// profiles.tsx
const Profiles = () => {
	const userBobData = trpc.useSWR([
		'user.get',
		{
			name: 'Bob',
		},
	])

	const userAvaData = trpc.useSWR([
		'user.get',
		{
			name: 'Ava',
		},
	])

	const matchMutate = useMatchMutate()

	return (
		<div>
			{[userBobData, userAvaData].map(({ data: user, isValidating }) => (
				<div>
					Name: {!data && isValidating
						? 'loading...'
						: data
						? data.name
						: 'User does not exist'}
				</div>
			))}
			<button onClick={() => matchMutate('user.get')}>
				Revalidate all tRPC `user.get` queries
			</button>
		</div>
	)
}