Skip to content

Commit 21c96a5

Browse files
authored
Merge pull request #17 from softnetics/miello/feat/react-query
[DRIZZ-41] React query client for react
2 parents dd26f26 + 0ee7b4d commit 21c96a5

10 files changed

Lines changed: 380 additions & 43 deletions

File tree

.changeset/kind-rivers-obey.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@kivotos/react-query': minor
3+
---
4+
5+
[[DRIZZ-41] React Query](https://app.plane.so/softnetics/browse/DRIZZ-41/)

examples/erp/drizzlify/client.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { createKivotosQueryClient } from '@kivotos/react-query'
2+
import { createRestClient } from '@kivotos/rest'
3+
4+
import type { serverConfig } from './config'
5+
6+
export const restClient = createRestClient<typeof serverConfig>({
7+
baseUrl: '',
8+
})
9+
export const queryClient = createKivotosQueryClient(restClient)

examples/erp/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
"dependencies": {
1717
"@kivotos/core": "workspace:^",
1818
"@kivotos/next": "workspace:^",
19+
"@kivotos/react-query": "workspace:^",
1920
"@kivotos/rest": "workspace:^",
2021
"@tailwindcss/postcss": "^4.0.14",
22+
"@tanstack/react-query": "^5.71.5",
2123
"drizzle-orm": "^0.41.0",
2224
"next": "15.2.2",
2325
"next-themes": "^0.4.6",
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1+
'use client'
2+
13
import { UIPlayground } from '@kivotos/next'
24

5+
import { queryClient } from '../../../../drizzlify/client'
6+
37
interface PlaygroundPageProps {
48
params: Promise<{ segments: string[] }>
59
searchParams: Promise<{ [key: string]: string | string[] }>
610
}
711

8-
export default async function Playground(props: PlaygroundPageProps) {
12+
export default function Playground(props: PlaygroundPageProps) {
13+
const result = queryClient.useQuery('GET', '/api/hello2', {})
14+
console.log(result.data)
915
return <UIPlayground />
1016
}

examples/erp/src/components/providers.tsx

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,50 @@
22

33
import type { PropsWithChildren } from 'react'
44

5+
import { isServer, QueryClient, QueryClientProvider } from '@tanstack/react-query'
56
import { ThemeProvider as NextThemesProvider } from 'next-themes'
67

8+
function makeQueryClient() {
9+
return new QueryClient({
10+
defaultOptions: {
11+
queries: {
12+
// With SSR, we usually want to set some default staleTime
13+
// above 0 to avoid refetching immediately on the client
14+
staleTime: 60 * 1000,
15+
},
16+
},
17+
})
18+
}
19+
20+
let browserQueryClient: QueryClient | undefined = undefined
21+
22+
function getQueryClient() {
23+
if (isServer) {
24+
// Server: always make a new query client
25+
return makeQueryClient()
26+
} else {
27+
// Browser: make a new query client if we don't already have one
28+
// This is very important, so we don't re-make a new client if React
29+
// suspends during the initial render. This may not be needed if we
30+
// have a suspense boundary BELOW the creation of the query client
31+
if (!browserQueryClient) browserQueryClient = makeQueryClient()
32+
return browserQueryClient
33+
}
34+
}
35+
736
export function Providers(props: PropsWithChildren) {
37+
const queryClient = getQueryClient()
838
return (
9-
<NextThemesProvider
10-
attribute="class"
11-
defaultTheme="system"
12-
enableSystem
13-
disableTransitionOnChange
14-
enableColorScheme
15-
>
16-
{props.children}
17-
</NextThemesProvider>
39+
<QueryClientProvider client={queryClient}>
40+
<NextThemesProvider
41+
attribute="class"
42+
defaultTheme="system"
43+
enableSystem
44+
disableTransitionOnChange
45+
enableColorScheme
46+
>
47+
{props.children}
48+
</NextThemesProvider>
49+
</QueryClientProvider>
1850
)
1951
}

packages/react-query/package.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"name": "@kivotos/react-query",
3+
"private": true,
4+
"sideEffects": false,
5+
"description": "",
6+
"type": "module",
7+
"main": "./src/index.tsx",
8+
"module": "./src/index.tsx",
9+
"exports": {
10+
".": "./src/index.tsx"
11+
},
12+
"scripts": {
13+
"lint": "eslint .",
14+
"format": "prettier --write .",
15+
"format:check": "prettier --check .",
16+
"typecheck": "tsc --noEmit"
17+
},
18+
"keywords": [],
19+
"author": "",
20+
"license": "ISC",
21+
"devDependencies": {
22+
"@internals/project-config": "workspace:^",
23+
"@kivotos/core": "workspace:^",
24+
"@kivotos/rest": "workspace:^",
25+
"@types/node": "^22.15.23",
26+
"@types/react": "^19.1.6",
27+
"type-fest": "^4.41.0",
28+
"vitest": "^3.0.9"
29+
},
30+
"dependencies": {
31+
"@tanstack/react-query": "^5.71.5"
32+
}
33+
}

packages/react-query/src/index.tsx

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import type {
2+
DefaultError,
3+
UseMutationOptions,
4+
UseMutationResult,
5+
UseQueryOptions,
6+
UseQueryResult,
7+
} from '@tanstack/react-query'
8+
import { useMutation, useQuery } from '@tanstack/react-query'
9+
import type { ValueOf } from 'type-fest'
10+
11+
import type { ApiRouter } from '@kivotos/core'
12+
import {
13+
type FilterByMethod,
14+
type RestClient,
15+
type RestPayload,
16+
type RestResponse,
17+
} from '@kivotos/rest'
18+
19+
export interface KivotosQueryClient<TApiRouter extends ApiRouter> {
20+
useQuery: <
21+
const TMethod extends keyof RestClient<TApiRouter>,
22+
const TPath extends ValueOf<FilterByMethod<TApiRouter, TMethod>>['schema']['path'],
23+
const TPayload = RestPayload<TApiRouter, TPath>,
24+
const TResponse = RestResponse<TApiRouter, TPath>,
25+
const TError = DefaultError,
26+
>(
27+
method: TMethod,
28+
path: TPath,
29+
payload: TPayload,
30+
options?: Omit<UseQueryOptions<TResponse, TError, TPayload>, 'queryKey'>
31+
) => UseQueryResult<TResponse, TError>
32+
useMutation: <
33+
const TMethod extends keyof RestClient<TApiRouter>,
34+
const TPath extends ValueOf<FilterByMethod<TApiRouter, TMethod>>['schema']['path'],
35+
const TPayload = RestPayload<TApiRouter, TPath>,
36+
const TResponse = RestResponse<TApiRouter, TPath>,
37+
const TError = DefaultError,
38+
const TContext = unknown,
39+
>(
40+
method: TMethod,
41+
path: TPath,
42+
payload: TPayload,
43+
options?: UseMutationOptions<TResponse, TError, TPayload, TContext>
44+
) => UseMutationResult<TResponse, TError, TPayload, TContext>
45+
queryOptions: <
46+
const TMethod extends keyof RestClient<TApiRouter>,
47+
const TPath extends ValueOf<FilterByMethod<TApiRouter, TMethod>>['schema']['path'],
48+
const TPayload = RestPayload<TApiRouter, TPath>,
49+
const TResponse = RestResponse<TApiRouter, TPath>,
50+
const TError = DefaultError,
51+
>(
52+
method: TMethod,
53+
path: TPath,
54+
payload: TPayload,
55+
options?: Omit<UseQueryOptions<TResponse, TError, TPayload>, 'queryKey'>
56+
) => UseQueryOptions<TResponse, TError, TPayload>
57+
mutationOptions: <
58+
const TMethod extends keyof RestClient<TApiRouter>,
59+
const TPath extends ValueOf<FilterByMethod<TApiRouter, TMethod>>['schema']['path'],
60+
const TPayload = RestPayload<TApiRouter, TPath>,
61+
const TResponse = RestResponse<TApiRouter, TPath>,
62+
const TError = DefaultError,
63+
const TContext = unknown,
64+
>(
65+
method: TMethod,
66+
path: TPath,
67+
payload: TPayload,
68+
options?: UseMutationOptions<TResponse, TError, TPayload, TContext>
69+
) => UseMutationOptions<TResponse, TError, TPayload, TContext>
70+
}
71+
72+
export function createKivotosQueryClient<TApiRouter extends ApiRouter<any>>(
73+
restClient: RestClient<TApiRouter>
74+
): KivotosQueryClient<TApiRouter> {
75+
return {
76+
useQuery: function (method: string, path: string, payload: any, options?: any) {
77+
return useQuery({
78+
queryKey: queryKey(method, path, payload),
79+
queryFn: () => {
80+
return (restClient as any)[method](path, payload)
81+
},
82+
...options,
83+
}) as UseQueryResult<any, any>
84+
},
85+
useMutation: function (method: string, path: string, payload: any, options?: any) {
86+
return useMutation({
87+
mutationKey: queryKey(method, path, payload),
88+
mutationFn: (data) => {
89+
return (restClient as any)[method](path, data)
90+
},
91+
...options,
92+
}) as UseMutationResult<any, any, any, any>
93+
},
94+
queryOptions: function (method: string, path: string, payload: any, options?: any) {
95+
return {
96+
queryKey: queryKey(method, path, payload),
97+
queryFn: () => {
98+
return (restClient as any)[method](path, payload)
99+
},
100+
...options,
101+
} as UseQueryOptions<any, any, any>
102+
},
103+
mutationOptions: function (method: string, path: string, payload: any, options?: any) {
104+
return {
105+
mutationKey: queryKey(method, path, payload),
106+
mutationFn: (data) => {
107+
return (restClient as any)[method](path, data)
108+
},
109+
...options,
110+
} as UseMutationOptions<any, any, any, any>
111+
},
112+
}
113+
}
114+
115+
export function queryKey(method: string, path: string | number | symbol, payload: any) {
116+
const payloadKey = {
117+
pathParams: payload?.pathParams ?? {},
118+
query: payload?.query ?? {},
119+
headers: payload?.headers ?? {},
120+
}
121+
return [method, path, payloadKey] as const
122+
}

packages/react-query/tsconfig.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"extends": "@internals/project-config/tsconfig/base.json",
3+
"compilerOptions": {
4+
"baseUrl": "."
5+
}
6+
}

packages/rest/src/index.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { ApiRouteHandlerPayload, ApiRouteSchema } from '@kivotos/core'
55

66
import { withPathParams, withQueryParams } from './utils'
77

8-
interface CreateRestClientConfig {
8+
export interface CreateRestClientConfig {
99
baseUrl: string
1010
}
1111

@@ -54,38 +54,38 @@ export function createRestClient<TServerConfig extends ServerConfig<any, any, an
5454
}
5555
}
5656

57-
type ExtractClientApiRouterPath<TApiRouter extends ApiRouter<any>> = ValueOf<{
57+
export type ExtractClientApiRouterPath<TApiRouter extends ApiRouter<any>> = ValueOf<{
5858
[TKey in keyof TApiRouter]: TApiRouter[TKey]['schema'] extends {
5959
path: infer TPath extends string
6060
}
6161
? TPath
6262
: never
6363
}>
6464

65-
type RestResponse<TApiRouter extends ApiRouter<any>, TPath extends string> = ValueOf<{
65+
export type RestResponse<TApiRouter extends ApiRouter<any>, TPath extends string> = ValueOf<{
6666
[TKey in keyof TApiRouter]: TApiRouter[TKey]['schema'] extends { path: TPath }
6767
? TApiRouter[TKey]['schema'] extends infer TApiRouteSchema extends ApiRouteSchema
6868
? ApiRouteResponse<TApiRouteSchema['responses']>
6969
: never
7070
: never
7171
}>
7272

73-
type RestPayload<TApiRouter extends ApiRouter<any>, TPath extends string> = ValueOf<{
73+
export type RestPayload<TApiRouter extends ApiRouter<any>, TPath extends string> = ValueOf<{
7474
[TKey in keyof TApiRouter]: TApiRouter[TKey]['schema'] extends { path: TPath }
7575
? TApiRouter[TKey]['schema'] extends infer TApiRouteSchema extends ApiRouteSchema
7676
? ApiRouteHandlerPayload<TApiRouteSchema>
7777
: never
7878
: never
7979
}>
8080

81-
type RestMethod<TApiRouter extends ApiRouter<any>> = <
81+
export type RestMethod<TApiRouter extends ApiRouter<any>> = <
8282
TPath extends ExtractClientApiRouterPath<TApiRouter>,
8383
>(
8484
path: TPath,
8585
payload: RestPayload<TApiRouter, TPath>
8686
) => Promise<RestResponse<TApiRouter, TPath>>
8787

88-
type FilterByMethod<TApiRouter extends ApiRouter<any>, TMethod extends string> = {
88+
export type FilterByMethod<TApiRouter extends ApiRouter<any>, TMethod extends string> = {
8989
[TKey in keyof TApiRouter as TApiRouter[TKey]['schema'] extends { method: TMethod }
9090
? TKey
9191
: never]: TApiRouter[TKey]

0 commit comments

Comments
 (0)