Closed sannajammeh closed 1 year ago
Hey thanks for writing all of this!
New client creator API Being able to access the raw tRPC proxy client is incredibly useful during calls outside of React. Previously, the API would've required you to create a new client instance separate from trpc-swr. AKA. Two tRPC instances running. Therefore, I propose a new API for creating a client instance. This will allow for the following:
import { createTRPCClient } from '@trpc/client'; import { createSWRProxyHooks } from 'trpc-swr';
export const client = createTRPCClient
export const trpc = createSWRProxyHooks(client); // New API
// Use the client instance directly // Here createSWRProxyHooks internally creates a new TRPCProxy client // This is for usage outside of React const cat = await trpc.client.cats.byId.query({id: 1});
// Use the hooks const {data: cat} = trpc.cats.byId.useSWR({id: 1});
This is nice, but we can't do this because it breaks SSR. With this code example, `client` will be shared across all requests. Alternative API 2 seems like a good way to go here.
I like `.getKey`, but I think we should try to reduce usage of it as much as possible. For this specific example:
> The creation of this .getKey API also gives us a direct way to mutate SWR keys in the global cache. This is useful for optimistic updates, manual cache overwrites, etc.
```ts
// We are inside a React component here
const {client} = trpc.useContext(); // Native tRPC client for mutation
const {mutate} = useSWRConfig();
function doSomethingThenMutate(){
// Do something
const newCat = client.cats.byId.mutate({id: 1}, {
id: 1,
name: 'Mr. Garfield',
});
// Mutate the cache
// All hooks using this key will now have the new data
await mutate(trpc.cats.byId.getKey({id: 1}), newCat, false);
}
I would still prefer if we had some way of using the proxy API here instead.
Prefetch data before React mounts directly into SWR cache
Again, instead of .getKey
here I think trpc.cats.byId.preload
would be better. So I prefer Potential API 2.
This also matches the @trpc/react
API much better.
SWR's useSWRMutation API
Yes, would love to have support this. The API will probably work the same as useSWR
.
Again, thanks for outlining this! I don't have much info on what is idiomatic SWR as I don't use it that much myself, so your help is much appreciated.
Got it! Getting started as I have a bit more time on my hands :)
While building this I've met a slight problem. Essentially what we want is to only use a single TRPCClient instance, however, we cannot follow the API used by @trpc/react
where you pass the client into the Context provider due to the fact SWRHook.preload()
is supposed to kick off a request outside of React before the app mounts. Therefore the client must be available before React hydrates.
These are not alternatives, both can be used. I overloaded the hooks creator function to accept an optional client like so:
// Option 1
export const trpc = createSWRProxyHooks<AppRouter>(config); // Similar to current API, generates the client
// Option 2 - If you need the native client for whatever reason
const client = createTRPCClient<AppRouter>(config); // From @trpc/client
export const trpc = createSWRProxyHooks(
null,
client
); // Will use the provided client.
// Use the native client proxy for whatever reason (custom requests, custom mutations etc.), example: Init the @trpc/client with this
export const trpcNative = createTRPCClientProxy(client); // imported from @trpc/swr
Both of the options currently only use a single instance of the native TRPCClient. We do have the option of leveraging trpc.createClient() and injecting it through context, however it would require some modifications so .preload()
has access to a global instance of this as long as its called top level before any .preload()
calls.
We can also only leave option 1 on the table and provide a trpc.getClient()
as opposed to trpc.createClient()
. This will be the same instance as you would get through useContext()
What's the difference between that and prefetching in the top level App
component?
No effects will run before the App is mounted & hydrated, in the case of SSR. For large apps this can be quite a bit. Secondly preload()
can be used to for instance kick off a request to trpc.users.byId
when a user hovers a link. This is of course inside of react, but it cannot be one as a hook due to the fact that SWR's preload api is also not inside of react.
We can build a custom hook, but this ends up not reflecting how SWR's preload API works.
If we are making it compatible with SWR v2 either some global reference of the client (when trpc.createClient()
is called) or my proposed API should be used.
I did come up with another method however. Let me know what you think :)
// Option 1
export const trpc = createSWRProxyHooks<AppRouter>(config); // Similar to current API, generates the client
// Option 2 - If you need the native client for whatever reason
const client = createTRPCClient<AppRouter>(config); // From @trpc/client
export const trpc = createSWRProxyHooks.withClient(client);
I do see what you mean by the SSR sharing of the client (in standard nodejs ssr not lambdas). This could be a problem. In that case I propose a different solution:
We keep the API as similar to @trpc/react meaning the client will be injected through context.
createSWRProxyHooks
will only accept config which will be passed down.
// Client creator API
// lib/trpc.ts
export const trpc = createSWRProxyHooks<AppRouter>({
links: [
httpBatchLink({
url: getApiUrl(),
}),
]
})
// In _app.tsx
function MyApp({ Component, pageProps }) {
const [client] = useState(() => trpc.createClient()); // Notice no config here as its provided when creating the hooks
return (
<trpc.Provider client={client} >
<Component {...pageProps} />
</trpc.Provider>
)
}
Minimal implementation:
// As preload is a browser only function, request sharing will not matter on SSR.
let client: TRPCClient<AnyRouter>;
const preload = (pathAndInput, ...args) => {
if(typeof window === "undefined") return Promise.resolve();
return _SWRPreload(pathAndInput, () => {
if(!client) {
client = createClient();
}
return client.query(...pathAndInput, ...args);
});
}
With this approach we can keep the API as similar as possible to the react one. The client is not shared accross SSR requests and preload is a noop on SSR.
Once the trpc.Provider
component mounts it will directly set the client variable (can be global cache too). This means preload()
will not have to create another client if its used to preload data on hover or something similar.
If however, preload is used before React, it will create its own client and dispatch the request. trpc.Provider
will take over once app mounts and the old client will be garbage collected. We can probably use refs to manually clear it from memory as well.
You are right. This API which would be alternative API 2 is the best path imo.
I've updated my fork here: https://github.com/sannajammeh/trpc-swr useSWR, useSWRMutation, preload and getKey all function with 100% test coverage.
I will get started on SWRInfinite, however as this is a codesplit package, how do you propose we use it? Similar to the current API?
export const infinite = createSWRInfiniteProxy(trpc)
infinite.cats.useSWRInfinite(...args)
// or since infinite is already presumed
infinite.cats.use(...args)
Sorry for the late response. I'm a bit confused, which API did you decide to go with for preload
?
I will get started on SWRInfinite, however as this is a codesplit package, how do you propose we use it? Similar to the current API?
I like infinite.cats.use(...args)
.
I went with tRPC/react's api with a few modifications.
The only difference between this and tRPC/react'a api is that the config has to go inside the createHooks method and not create client. It's a necessary compromise to make preload work.
This also lets us build a SSR utility which I'll propose a bit later.
useSWRInfinite has been successfully implemented. All that remains now is typing the non-proxy hooks as we also export their methods.
97.7% Test coverage. Some quirks on infinite I have to iron out.
.use()
& .useCursor()
react-query
contains custom logic to use infinite queries. SWR lets this the user decide if they want to use cursors, pagination or any other kind of use case. To be more compliant with @trpc/react
we now have two methods.
infinite.<endpoint>.use()
& infinite.<endpoint>.useCursor()
. The .use api is a tRPC ified version of SWR. The paging logic must be implemented by hand which can cover almost all use cases. useCursor()
's API is made to mimmic @trpc/react
. Its a helper function providing the cursor paging logic out of the box, makes it really easy to integrate with prisma on the back-end like useInfiniteQuery
does.
infinite.<endpoint>.use()
Manual integration of the paging. Any kind of logic can be used here. ID fetching, cursor etc.
const infinite = createSWRInfiniteProxy(trpc)
const Component = () => {
const { data, setSize, size } = infinite.people.get.use((index, previousPageData) => {
if (index !== 0 && !previousPageData?.length) return null // Last page
return { limit: 1, page: index }
})
if (!data) {
return <div>Loading...</div>
}
const people = data.flatMap((page) => page)
const hasMore = (data.at(-1) ?? []).length > 0
return (
<>
<div>
{people.map((user, index) => {
return <p key={user.name}>{user.name}</p>
})}
</div>
{hasMore && <button onClick={() => setSize(size + 1)}>Load More</button>}
</>
)
}
infinite.<endpoint>.useCursor()
Prebuilt cursor logic
const infinite = createSWRInfiniteProxy(trpc)
const Component = () => {
const { data, setSize, size } = infinite.people.get.useCursor({limit: 5},
(data) => data?.nextCursor
)
if (!data) {
return <div>Loading...</div>
}
const people = data.flatMap((page) => page.items)
const hasMore = !!data.at(-1)?.nextCursor
return (
<>
<div>
{people.map((user, index) => {
return <p key={user.name}>{user.name}</p>
})}
</div>
{hasMore && <button onClick={() => setSize(size + 1)}>Load More</button>}
</>
)
}
Both these are now active in my fork and on @chiefkoshi/trpc-swr@1.0.0-beta-5
on npmjs. I'll upgrade the latest trpc core packages to the newest beta later.
Thanks for the update! That looks awesome
Was waiting for tRPC v10 to release and got caught with work. I have some time on my hands to finalize this RFC.
We now export Next.js helpers, this includes a withTRPCSWR
HOC, and a proxy helper function using server side calls.
The flow is as follows:
router.createCaller()
.dehydrate()
will await either way. As we are using SWR, there is no point in making a fetch call, thus we use the caller
to make this a server to server call.
The key is then serialized using SWR's serialization function.
@trpc/next
In any server util file
// server/ssg
export const createSSG = () => {
return createProxySSGHelpers({
router: appRouter,
ctx: {},
transformer: SuperJSON,
})
}
In next _app
// _app.tsx
const App = ({ Component, pageProps }: AppProps) => {
const [client] = useState(() => trpc.createClient())
return (
<trpc.Provider client={client}>
<Component {...pageProps} />
</trpc.Provider>
)
}
export default withTRPCSWR({
transformer: SuperJSON,
})(App)
In any SSG/SSR route
export const getStaticProps: GetStaticProps = async () => {
const trpc = createSSG()
trpc.user.byId.fetch({ id: 1 })
trpc.user.byId.fetch({ id: 2 })
// Can decide to await or not. This is useful if the user wants parallell execution for faster builds/ssr
return {
props: {
swr: await trpc.dehydrate(), // Must be awaited
},
}
}
And thats it, the hooks will have hydrated state.
I'm currently working on documenting everything in this RFC as an online documentation using Nextra
as the static generator.
@sachinraja how does this look? We can most likely make something similar to @trpc/next
when you init the trpc client as well.
@sannajammeh Amazing! This RFC looks awesome.
That looks great @sannajammeh!
Would you be interested in taking over development of trpc-swr? No pressure, but I don't have the bandwidth to manage this project on top of my other ones and I myself don't use SWR. I'd be happy to transfer the GitHub repo and/or the npm package if you're interested. Thanks!
That looks great @sannajammeh!
Would you be interested in taking over development of trpc-swr? No pressure, but I don't have the bandwidth to manage this project on top of my other ones and I myself don't use SWR. I'd be happy to transfer the GitHub repo and/or the npm package if you're interested. Thanks!
I definitely don't mind taking over maintenance of trpc-swr! Would you need the npmjs username or email?
Thank you so much! I just need your username to transfer ownership.
Thank you so much! I just need your username to transfer ownership.
Username is: chiefkoshi
Invited you!
Invited you!
I've accepted the npmjs code. Would you mind transferring the github repo as well?
Can't transfer the repo since your fork has the same name: https://github.com/sannajammeh/trpc-swr
Makes sense. Renamed it!
I think you might actually have to delete your fork according to these docs? In any case I've invited you as a collaborator so feel free to push your branches here.
Hello @sachinraja
I've publish the first release candidate of this package on npm. My fork is now deleted and changes have been pushed here.
Would you mind transferring the repo over to me so I can deploy the documentation on Vercel?
@sachinraja Any possibility for this?
@sannajammeh I think I sent you an invite to transfer the repo a day after you asked
@sannajammeh
![]()
@sannajammeh
We may have to abort and retry the transfer. It appears GitHub wont fulfill this one
Done, sent another transfer request.
@sannajammeh Any updates about this RFC?
@sannajammeh Any updates about this RFC?
It's currently in RC mode! I'm testing it in my dev environments and trying to collect any bugs before I release the V1.
There's also a bug in the Next.js rust compiler preventing SWR use in the new App directory. https://github.com/vercel/swr/issues/2632
As a hot fix we had to duplicate SWR's hashing function, which isn't ideal. I'm confident we can release the v1 once this is fixed.
Hi everyone!
I've successfully tested this library in most environments now. All thats missing is a an E2E test with Next 13.
v1 will release by the end of this week :)
Hit some weird dependency resolution issues with both TypeScript and Next 13. For some reason the very same resolution method works in SWR, but not here. If I'm unable to resolve this by tomorrow, I'll be switching to a more safe packaging method instead, similar to tRPC.
Example:
@trpc-swr/react
- previously trpc-swr
@trpc-swr/next
- previously trpc-swr/next
@trpc-swr/infinite
- previously trpc-swr/infinite
Essentially the problem is as follows:
When testing inside of the monorepo, internal packages of trpc-swr like /infinite
cannot resolve the trpc-swr
core package. I am using the exact same technique as SWR itself, yet the resolution is somehow different.
This only appears to happen in the monorepo and not external Next.js projects.
Looks like pnpm@8.1 has resolved this issue and will respect the files
when installing packages. Will be confirming if this fixes the issue by tomorrow and pushing a subsequent release if resolved.
After a week of testing it appears that the issue remains and is due to Webpack's handling of symlinks, causing Next to import ESM swr
and trpc-swr always importing CJS swr
despite being in ESM itself. We are unable to solve this for now. In order to ensure safety with E2E tests, I will switching over to the new package strategy detailed above. This gives us a number of benefits:
Once merged the trpc-swr
will be archived at its current version and the above packages will be preferred.
All updates will now arrive in vercel/next.js#40
Thank you for your work!
New Proxy API & SWR 2.0
This RFC outlines the implementation of tRPC's v10 proxy api in conjunction with SWR hooks, along with some following features:
fallbackData
&SWRConfig
Motivation
As tRPC v10 is nearing completion (main proxy API is stable), SWR hooks is a "must have" for many developers like myself.
The changes
Below are the changes I propose for
trpc-swr
(v1.0?).New proxy API
trpc-swr
will take advantage of the newly constructed Proxy technique used in tRPC V10. Examples:SWR 2.0
trpc-swr
will take advantage of the newest SWR release. This allows for optimisticUpdate support, and other features. With this RFCswr: ^2
will become a peer dependency rather thanswr: ^1
Example
New client creator API
Being able to access the raw tRPC proxy client is incredibly useful during calls outside of React. Previously, the API would've required you to create a new client instance separate from
trpc-swr
. AKA. Two tRPC instances running. Therefore, I propose a new API for creating a client instance. This will allow for the following:Alternative API 1:
In this API no
trpc.client
will be available. Instead, you can reference the client directly. This also allows for usage outside of React. Here we are using named properties.trpc
will always be thehooks
instance, andclient
will always be the standard tRPC proxyclient
instance.Alternative API 2:
Keep the raw client inside of a
trpc.useContext()
call as it is today. Allow passing in aclient
instance tocreateSWRProxyHooks
to use the same client instance. This would allow for usage outside of React.Better SSR support
trpc-swr
will take advantage of SWR'sfallbackData
&SWRConfig
to provide better SSR support. This requires a new API forcreateSWRProxyHooks
:.getKey
:The reason this is wildy important is for SSR to work from a global state standpoint. Providing the fallback data to a single
useSWR
hook will not populate the cache for other hooks (Wierdly enough since SWR uses a global cache anyway).The creation of this
.getKey
API also gives us a direct way to mutate SWR keys in the global cache. This is useful for optimistic updates, manual cache overwrites, etc.This is something thats been missing even from the current V2
SWR
core. Since we use proxies in tRPC this is a very easy API to implement and will provide tremendous DX for SWR users.Future & possible API's
Prefetch data before React mounts directly into SWR cache
This is a feature SWR is in the process of releasing. It will allow for prefetching data before React mounts. This is useful for SSR, and also for prefetching data before a user navigates to a page. A new API for
trpc-swr
might look like this:Potential API 1
Using the
.getKey
method from above.Potential API 2
Using a special
.preload()
on queries.SWR's
useSWRMutation
APISWR is also working on a brand new mutation API similar to
react-query
for triggerable mutations. This is a very exciting API and I'm looking forward to it. I'm not sure how this will work with tRPC yet, but I'm sure we can figure something out.Footnotes
This is a very rough draft of what I'm thinking. I'm sure there are many things I'm missing. I'm also not sure if this is the best way to do things. I'm open to suggestions and feedback. Additionally, some API's in the current release are not accounted for here. I'll be sure to update this RFC once I have some more time.
Once I have some feedback on the above suggestions I am happy to start a PR for this new API. And huge thanks to @sachinraja for making this wonderful library :)