Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: expose querykey getter and fix some querykey stuff #3302

Merged
merged 51 commits into from Dec 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
5392463
1
juliusmarminge Nov 27, 2022
8638767
Merge branch 'main' into julius/querykeys
juliusmarminge Nov 27, 2022
bc702cc
2
juliusmarminge Nov 27, 2022
5b55386
add some tests
juliusmarminge Nov 27, 2022
2711978
failing test
juliusmarminge Nov 27, 2022
30dcc40
simplify
juliusmarminge Nov 27, 2022
d77f3c8
going places
juliusmarminge Nov 27, 2022
36ffd96
docs
juliusmarminge Nov 27, 2022
0a97a6b
more docs
juliusmarminge Nov 27, 2022
217db37
revert example
juliusmarminge Nov 27, 2022
964da9c
docs 3
juliusmarminge Nov 27, 2022
90a9173
add test
juliusmarminge Nov 27, 2022
dc92dcf
fix test
juliusmarminge Nov 27, 2022
5813e19
restructure
juliusmarminge Nov 27, 2022
366107d
revert lock
juliusmarminge Nov 27, 2022
c1d4497
better brackets
juliusmarminge Nov 27, 2022
c909673
better docs
juliusmarminge Nov 27, 2022
4efa3a8
fixy
juliusmarminge Nov 27, 2022
778859d
please not now docu
juliusmarminge Nov 27, 2022
7770be8
imports
juliusmarminge Nov 27, 2022
e9f03e1
Merge branch 'main' into julius/querykeys
juliusmarminge Dec 4, 2022
cca2d30
move it to trpc object instead
juliusmarminge Dec 5, 2022
8b937df
docs
juliusmarminge Dec 5, 2022
c9b59bd
simplify test setup
juliusmarminge Dec 5, 2022
44179fd
make optional
juliusmarminge Dec 5, 2022
2d25187
add test outside react
juliusmarminge Dec 5, 2022
d06701a
add example
juliusmarminge Dec 5, 2022
4b4e12f
fix
juliusmarminge Dec 5, 2022
377e271
fix2
juliusmarminge Dec 5, 2022
c8b8ee8
docs and remove example
juliusmarminge Dec 16, 2022
cdc5808
Merge branch 'main' into julius/querykeys
juliusmarminge Dec 16, 2022
dd8062e
revert downgrade of docusaurus
juliusmarminge Dec 16, 2022
04d5333
Merge branch 'main' into julius/querykeys
juliusmarminge Dec 16, 2022
c92eb39
Merge branch 'main' into julius/querykeys
juliusmarminge Dec 16, 2022
c1b162e
qc -> queryClient
juliusmarminge Dec 16, 2022
c8e3ed7
mby???
juliusmarminge Dec 16, 2022
0e519e2
patch querykey
juliusmarminge Dec 22, 2022
60afce2
fix invalidate
juliusmarminge Dec 22, 2022
72b5ab6
fix old
juliusmarminge Dec 22, 2022
6f195da
regression tests
juliusmarminge Dec 22, 2022
d60dea3
testid
juliusmarminge Dec 22, 2022
f566339
Merge remote-tracking branch 'origin/main' into julius/querykeys
juliusmarminge Dec 22, 2022
5adc7ec
fix
juliusmarminge Dec 22, 2022
957a78c
interop
juliusmarminge Dec 22, 2022
f025343
Merge branch 'main' into julius/querykeys
juliusmarminge Dec 22, 2022
3ceba17
disable interop
juliusmarminge Dec 22, 2022
05f5c8d
comment
juliusmarminge Dec 22, 2022
88b8052
1 more test
juliusmarminge Dec 22, 2022
943fdeb
fix the interop stuff
juliusmarminge Dec 22, 2022
bb26bc1
eeh
juliusmarminge Dec 22, 2022
1396478
Merge branch 'main' into julius/querykeys
kodiakhq[bot] Dec 23, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 13 additions & 1 deletion packages/react-query/src/createTRPCReact.tsx
Expand Up @@ -15,6 +15,7 @@ import {
inferTransformedSubscriptionOutput,
} from '@trpc/server/shared';
import { useMemo } from 'react';
import { QueryKey, QueryType } from './internals/getArrayQueryKey';
import { TRPCUseQueries } from './internals/useQueries';
import {
CreateReactUtilsProxy,
Expand Down Expand Up @@ -48,6 +49,15 @@ export type DecorateProcedure<
TPath extends string,
> = TProcedure extends AnyQueryProcedure
? {
/**
* Method to extract the query key for a procedure
* @param type - defaults to `any`
* @link https://trpc.io/docs/useContext#-the-function-i-want-isnt-here
*/
getQueryKey: (
input: inferProcedureInput<TProcedure>,
type?: QueryType,
) => QueryKey;
useQuery: <
TQueryFnData = inferTransformedProcedureOutput<TProcedure>,
TData = inferTransformedProcedureOutput<TProcedure>,
Expand Down Expand Up @@ -164,7 +174,9 @@ export type DecoratedProcedureRecord<
TPath extends string = '',
> = {
[TKey in keyof TProcedures]: TProcedures[TKey] extends AnyRouter
? DecoratedProcedureRecord<
? {
getQueryKey: () => QueryKey;
} & DecoratedProcedureRecord<
TProcedures[TKey]['_def']['record'],
TFlags,
`${TPath}${TKey & string}.`
Expand Down
7 changes: 0 additions & 7 deletions packages/react-query/src/internals/context.tsx
Expand Up @@ -160,13 +160,6 @@ export interface TRPCContextState<
filters?: InvalidateQueryFilters,
options?: InvalidateOptions,
): Promise<void>;
/**
* @link https://react-query.tanstack.com/guides/query-invalidation
*/
invalidateQueries(
filters?: InvalidateQueryFilters,
options?: InvalidateOptions,
): Promise<void>;

/**
* @link https://react-query.tanstack.com/reference/QueryClient#queryclientresetqueries
Expand Down
11 changes: 11 additions & 0 deletions packages/react-query/src/internals/getArrayQueryKey.test.ts
@@ -1,6 +1,17 @@
import { getArrayQueryKey } from './getArrayQueryKey';

test('getArrayQueryKey', () => {
// empty path should not nest an extra array
expect(getArrayQueryKey('', 'any')).toMatchInlineSnapshot(`Array []`);

// should not nest an empty object
expect(getArrayQueryKey('foo', 'any')).toMatchInlineSnapshot(`
Array [
Array [
"foo",
],
]
`);
expect(getArrayQueryKey('foo', 'query')).toMatchInlineSnapshot(`
Array [
Array [
Expand Down
13 changes: 11 additions & 2 deletions packages/react-query/src/internals/getArrayQueryKey.ts
@@ -1,16 +1,21 @@
export type QueryType = 'query' | 'infinite' | 'any';

export type QueryKey = [
string[],
{ input?: unknown; type?: Exclude<QueryType, 'any'> }?,
];

/**
* To allow easy interactions with groups of related queries, such as
* invalidating all queries of a router, we use an array as the path when
* storing in tanstack query. This function converts from the `.` separated
* path passed around internally by both the legacy and proxy implementation.
* https://github.com/trpc/trpc/issues/2611
*/
**/
export function getArrayQueryKey(
queryKey: string | [string] | [string, ...unknown[]] | unknown[],
type: QueryType,
): [string[], { input?: unknown; type?: Exclude<QueryType, 'any'> }] {
): QueryKey {
const queryKeyArrayed = Array.isArray(queryKey) ? queryKey : [queryKey];
const [path, input] = queryKeyArrayed;

Expand All @@ -20,6 +25,10 @@ export function getArrayQueryKey(
// Construct a query key that is easy to destructure and flexible for
// partial selecting etc.
// https://github.com/trpc/trpc/issues/3128
if (!input && (!type || type === 'any'))
// for `utils.invalidate()` to match all queries (including vanilla react-query)
// we don't want nested array if path is empty, i.e. `[]` instead of `[[]]`
return arrayPath.length ? [arrayPath] : ([] as unknown as QueryKey);
return [
arrayPath,
{
Expand Down
3 changes: 2 additions & 1 deletion packages/react-query/src/internals/getQueryKey.ts
Expand Up @@ -6,5 +6,6 @@ export function getQueryKey(
path: string,
input: unknown,
): [string] | [string, unknown] {
return input === undefined ? [path] : [path, input];
if (path.length) return input === undefined ? [path] : [path, input];
return [] as unknown as [string];
}
7 changes: 7 additions & 0 deletions packages/react-query/src/shared/proxy/decorationProxy.ts
@@ -1,5 +1,6 @@
import { AnyRouter } from '@trpc/server';
import { createRecursiveProxy } from '@trpc/server/shared';
import { getArrayQueryKey } from '../../internals/getArrayQueryKey';
import { getQueryKey } from '../../internals/getQueryKey';
import { CreateReactQueryHooks } from '../hooks/createHooksInternal';

Expand Down Expand Up @@ -28,6 +29,12 @@ export function createReactProxyDecoration<
const [input, ...rest] = args;

const queryKey = getQueryKey(path, input);

// Expose queryKey helper
if (lastArg === 'getQueryKey') {
return getArrayQueryKey(queryKey, (rest[0] as any) ?? 'any');
}

if (lastArg.startsWith('useSuspense')) {
const opts = rest[0] || {};
const fn =
Expand Down
1 change: 1 addition & 0 deletions packages/react-query/src/shared/proxy/utilsProxy.ts
Expand Up @@ -171,6 +171,7 @@ type DecorateRouter = {
* @link https://react-query.tanstack.com/guides/query-invalidation
*/
invalidate(
input?: undefined,
filters?: InvalidateQueryFilters,
options?: InvalidateOptions,
): Promise<void>;
Expand Down
Expand Up @@ -192,12 +192,15 @@ describe('invalidateQueries()', () => {
<button
data-testid="invalidate-5-predicate"
onClick={() => {
utils.invalidateQueries({
utils.invalidateQueries(undefined, {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Breaking chaange?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above

predicate(opts) {
const { queryKey } = opts;
const [path, input] = queryKey;
const [path, rest] = queryKey;

return path === 'count' && input === 'test';
return (
JSON.stringify(path) === JSON.stringify(['count']) &&
(rest as any)?.input === 'test'
);
Comment on lines +200 to +203
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nvm I found it and fixed it here instead

},
});
}}
Expand Down
178 changes: 178 additions & 0 deletions packages/tests/server/react/getQueryKey.test.tsx
@@ -0,0 +1,178 @@
import { getServerAndReactClient } from './__reactHelpers';
import { useIsFetching } from '@tanstack/react-query';
import { render, waitFor } from '@testing-library/react';
import { initTRPC } from '@trpc/server';
import { konn } from 'konn/dist-cjs';
import React from 'react';
import { z } from 'zod';

type Post = {
id: number;
text: string;
};

const defaultPost = { id: 0, text: 'new post' };
const ctx = konn()
.beforeEach(() => {
const t = initTRPC.create();

const posts: Post[] = [defaultPost];

const appRouter = t.router({
post: t.router({
byId: t.procedure
.input(
z.object({
id: z.number(),
}),
)
.query(({ input }) => posts.find((post) => post.id === input.id)),
all: t.procedure.query(() => posts),
}),
});

return getServerAndReactClient(appRouter);
})
.afterEach(async (ctx) => {
await ctx?.close?.();
})
.done();

describe('getQueryKeys', () => {
test('no input', async () => {
const { proxy, App } = ctx;

function MyComponent() {
const happy1 = proxy.post.all.getQueryKey(undefined, 'query');
const happy2 = proxy.post.all.getQueryKey();

// @ts-expect-error - post.all has no input
const sad = proxy.post.all.getQueryKey('foo');

return (
<>
<pre data-testid="qKey1">{JSON.stringify(happy1)}</pre>
<pre data-testid="qKey2">{JSON.stringify(happy2)}</pre>
</>
);
}

const utils = render(
<App>
<MyComponent />
</App>,
);

await waitFor(() => {
expect(utils.getByTestId('qKey1')).toHaveTextContent(
JSON.stringify([['post', 'all'], { type: 'query' }]),
);
expect(utils.getByTestId('qKey2')).toHaveTextContent(
JSON.stringify([['post', 'all']]),
);
});
});

test('with input', async () => {
const { proxy, App } = ctx;

function MyComponent() {
const happy1 = proxy.post.byId.getQueryKey({ id: 1 }, 'query');

// doesn't really make sense but should still work
const happyIsh = proxy.post.byId.getQueryKey({ id: 1 });

// @ts-expect-error - post.byId has required input
const sad = proxy.post.byId.getQueryKey(undefined, 'query');

return (
<>
<pre data-testid="qKey1">{JSON.stringify(happy1)}</pre>
<pre data-testid="qKey2">{JSON.stringify(happyIsh)}</pre>
</>
);
}

const utils = render(
<App>
<MyComponent />
</App>,
);

await waitFor(() => {
expect(utils.getByTestId('qKey1')).toHaveTextContent(
JSON.stringify([['post', 'byId'], { input: { id: 1 }, type: 'query' }]),
);
expect(utils.getByTestId('qKey2')).toHaveTextContent(
JSON.stringify([['post', 'byId'], { input: { id: 1 } }]),
);
});
});

test('on router', async () => {
const { proxy, App } = ctx;

function MyComponent() {
const happy = proxy.post.getQueryKey();

// @ts-expect-error - router has no input
const sad = proxy.post.getQueryKey('foo');

return (
<div>
<pre data-testid="qKey">{JSON.stringify(happy)}</pre>
</div>
);
}

const utils = render(
<App>
<MyComponent />
</App>,
);

await waitFor(() => {
expect(utils.getByTestId('qKey')).toHaveTextContent(
JSON.stringify([['post']]),
);
});
});

test('forwarded to a real method', async () => {
const { proxy, App } = ctx;

function MyComponent() {
proxy.post.all.useQuery();

const qKey = proxy.post.all.getQueryKey(undefined, 'query');
const isFetching = useIsFetching(qKey);

return <div>{isFetching}</div>;
}

const utils = render(
<App>
<MyComponent />
</App>,
);

// should be fetching initially, and then not
expect(utils.container).toHaveTextContent('1');
await waitFor(() => {
expect(utils.container).toHaveTextContent('0');
});
});

test('outside of the react context', () => {
const { proxy } = ctx;

const all = proxy.post.all.getQueryKey(undefined, 'query');
const byId = proxy.post.byId.getQueryKey({ id: 1 }, 'query');

expect(all).toEqual([['post', 'all'], { type: 'query' }]);
expect(byId).toEqual([
['post', 'byId'],
{ input: { id: 1 }, type: 'query' },
]);
});
});
2 changes: 1 addition & 1 deletion packages/tests/server/react/invalidateQueries.test.tsx
Expand Up @@ -186,7 +186,7 @@ describe('invalidateQueries()', () => {
<button
data-testid="invalidate-4-predicate"
onClick={() => {
utils.invalidate({
utils.invalidate(undefined, {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this changing the behaviour? 🤔

Looks like a breaking change?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean yea theoretically although it wasnt working before so its more of a fix?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mentioned this here: https://discord.com/channels/867764511159091230/1055437438266392646/1055450208424706099

We always take the first arg as input no matter what the types said so any filters applied would actually end up being supressed

predicate(opts) {
const { queryKey } = opts;
const [path, input] = queryKey;
Expand Down
4 changes: 4 additions & 0 deletions packages/tests/server/react/useContext.test.tsx
Expand Up @@ -61,6 +61,10 @@ const ctx = konn()
return newPost;
}),
}),

greeting: t.router({
get: t.procedure.query(() => 'hello'),
}),
});

return getServerAndReactClient(appRouter);
Expand Down