Skip to content

Commit

Permalink
RJS-2649: Add more additional overload to useQuery to allow exhaustiv…
Browse files Browse the repository at this point in the history
…e-deps to work (#6819)

* Export createUseQuery & createUseObject

* Allow first argument in useQuery as function

* Rever package.json change and fix test name

* Update package lock

* Revert package-lock.json

* Add typeguards to improve code quality

* Fix wrong type
  • Loading branch information
bimusiek authored Oct 2, 2024
1 parent 3154413 commit 5bec6e4
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 23 deletions.
9 changes: 9 additions & 0 deletions packages/realm-react/src/__tests__/useQueryHook.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,15 @@ describe("useQuery", () => {
expect(result.current.length).toBe(3);
});

describe("passing function as 1st argument, deps as 2nd and options as 3rd", () => {
it("can filter objects via a 'query' property", () => {
const { result, renders } = profileHook(() =>
useQuery<IDog>((dogs) => dogs.filtered("age > 10"), [], { type: "dog" }),
);
expect(result.current.length).toBe(3);
expect(renders).toHaveLength(1);
});
});
describe("passing an object of options as argument", () => {
it("can filter objects via a 'query' property", () => {
const { result, renders } = profileHook(() =>
Expand Down
2 changes: 2 additions & 0 deletions packages/realm-react/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,5 @@ export { useUser, UserProvider } from "./UserProvider";
export * from "./useAuth";
export * from "./useEmailPasswordAuth";
export * from "./types";
export { createUseObject } from "./useObject";
export { createUseQuery } from "./useQuery";
97 changes: 75 additions & 22 deletions packages/realm-react/src/useQuery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,28 @@ import { AnyRealmObject, RealmClassType, getObjects, isClassModelConstructor } f
type QueryCallback<T> = (collection: Realm.Results<T>) => Realm.Results<T>;
type DependencyList = ReadonlyArray<unknown>;

export type QueryHookOptions<T> = {
type: string;
query?: QueryCallback<T>;
export type QueryHookPartialOptions<T> = {
type: string | RealmClassType<T>;
keyPaths?: string | string[];
};

export type QueryHookClassBasedOptions<T> = {
type: RealmClassType<T>;
export type QueryHookOptions<T> = QueryHookPartialOptions<T> & {
query?: QueryCallback<T>;
keyPaths?: string | string[];
};

export type UseQueryHook = {
<T>(options: QueryHookOptions<T>, deps?: DependencyList): Realm.Results<T & Realm.Object<T>>;
<T extends AnyRealmObject>(options: QueryHookClassBasedOptions<T>, deps?: DependencyList): Realm.Results<T>;
<T extends AnyRealmObject>(options: QueryHookOptions<T>, deps?: DependencyList): Realm.Results<T>;
<T>(type: string): Realm.Results<T & Realm.Object<T>>;
<T extends AnyRealmObject>(type: RealmClassType<T>): Realm.Results<T>;
<T extends AnyRealmObject>(
query: QueryCallback<T>,
deps: DependencyList,
options: QueryHookPartialOptions<T>,
): Realm.Results<T>;
<T>(query: QueryCallback<T>, deps: DependencyList, options: QueryHookPartialOptions<T>): Realm.Results<
T & Realm.Object<T>
>;

/** @deprecated To help the `react-hooks/exhaustive-deps` eslint rule detect missing dependencies, we've suggest passing a option object as the first argument */
<T>(type: string, query?: QueryCallback<T>, deps?: DependencyList): Realm.Results<T & Realm.Object<T>>;
Expand All @@ -53,6 +58,12 @@ export type UseQueryHook = {
): Realm.Results<T>;
};

type PossibleQueryArgs<T> = {
typeOrOptionsOrQuery: QueryHookOptions<T> | string | RealmClassType<T> | QueryCallback<T>;
queryOrDeps?: DependencyList | QueryCallback<T>;
depsOrPartialOptions?: DependencyList | QueryHookPartialOptions<T>;
};

/**
* Maps a value to itself
*/
Expand All @@ -67,7 +78,7 @@ function identity<T>(value: T): T {
*/
export function createUseQuery(useRealm: () => Realm): UseQueryHook {
function useQuery<T extends AnyRealmObject>(
{ type, query = identity, keyPaths }: QueryHookOptions<T> | QueryHookClassBasedOptions<T>,
{ type, query = identity, keyPaths }: QueryHookOptions<T>,
deps: DependencyList = [],
): Realm.Results<T> {
const realm = useRealm();
Expand Down Expand Up @@ -132,21 +143,63 @@ export function createUseQuery(useRealm: () => Realm): UseQueryHook {
}

return function useQueryOverload<T extends AnyRealmObject>(
typeOrOptions: QueryHookOptions<T> | QueryHookClassBasedOptions<T> | string | RealmClassType<T>,
queryOrDeps: DependencyList | QueryCallback<T> = identity,
deps: DependencyList = [],
typeOrOptionsOrQuery: PossibleQueryArgs<T>["typeOrOptionsOrQuery"],
queryOrDeps: PossibleQueryArgs<T>["queryOrDeps"] = identity,
depsOrPartialOptions: PossibleQueryArgs<T>["depsOrPartialOptions"] = [],
): Realm.Results<T> {
if (typeof typeOrOptions === "string" && typeof queryOrDeps === "function") {
/* eslint-disable-next-line react-hooks/rules-of-hooks -- We're calling `useQuery` once in any of the brances */
return useQuery({ type: typeOrOptions, query: queryOrDeps }, deps);
} else if (isClassModelConstructor(typeOrOptions) && typeof queryOrDeps === "function") {
/* eslint-disable-next-line react-hooks/rules-of-hooks -- We're calling `useQuery` once in any of the brances */
return useQuery({ type: typeOrOptions as RealmClassType<T>, query: queryOrDeps }, deps);
} else if (typeof typeOrOptions === "object" && typeOrOptions !== null) {
/* eslint-disable-next-line react-hooks/rules-of-hooks -- We're calling `useQuery` once in any of the brances */
return useQuery(typeOrOptions, Array.isArray(queryOrDeps) ? queryOrDeps : deps);
} else {
throw new Error("Unexpected arguments passed to useQuery");
const args = { typeOrOptionsOrQuery, queryOrDeps, depsOrPartialOptions };
/* eslint-disable react-hooks/rules-of-hooks -- We're calling `useQuery` once in any of the brances */
if (isTypeFunctionDeps(args)) {
return useQuery({ type: args.typeOrOptionsOrQuery, query: args.queryOrDeps }, args.depsOrPartialOptions);
}
if (isOptionsDepsNone(args)) {
return useQuery(args.typeOrOptionsOrQuery, Array.isArray(args.queryOrDeps) ? args.queryOrDeps : []);
}
if (isFunctionDepsOptions(args)) {
return useQuery({ ...args.depsOrPartialOptions, query: args.typeOrOptionsOrQuery }, args.queryOrDeps);
}
/* eslint-enable react-hooks/rules-of-hooks */

throw new Error("Unexpected arguments passed to useQuery");
};
}

function isTypeFunctionDeps<T>(args: PossibleQueryArgs<T>): args is {
typeOrOptionsOrQuery: string | RealmClassType<T>;
queryOrDeps: QueryCallback<T>;
depsOrPartialOptions: DependencyList;
} {
const { typeOrOptionsOrQuery, queryOrDeps, depsOrPartialOptions } = args;
return (
(typeof typeOrOptionsOrQuery === "string" || isClassModelConstructor(typeOrOptionsOrQuery)) &&
typeof queryOrDeps === "function" &&
Array.isArray(depsOrPartialOptions)
);
}

function isOptionsDepsNone<T>(args: PossibleQueryArgs<T>): args is {
typeOrOptionsOrQuery: QueryHookOptions<T>;
queryOrDeps: DependencyList | typeof identity;
depsOrPartialOptions: never;
} {
const { typeOrOptionsOrQuery, queryOrDeps } = args;
return (
typeof typeOrOptionsOrQuery === "object" &&
typeOrOptionsOrQuery !== null &&
(Array.isArray(queryOrDeps) || queryOrDeps === identity)
);
}

function isFunctionDepsOptions<T>(args: PossibleQueryArgs<T>): args is {
typeOrOptionsOrQuery: QueryCallback<T>;
queryOrDeps: DependencyList;
depsOrPartialOptions: QueryHookPartialOptions<T>;
} {
const { typeOrOptionsOrQuery, queryOrDeps, depsOrPartialOptions } = args;
return (
typeof typeOrOptionsOrQuery === "function" &&
Array.isArray(queryOrDeps) &&
typeof depsOrPartialOptions === "object" &&
depsOrPartialOptions !== null
);
}
2 changes: 1 addition & 1 deletion packages/realm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -284,4 +284,4 @@
6
]
}
}
}

0 comments on commit 5bec6e4

Please sign in to comment.