@pomwik/afterdb-react 0.0.0-stage → 0.20.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGES-FROM-UPSTREAM.md +59 -0
- package/LICENSE +177 -0
- package/NOTICE +9 -0
- package/README.md +10 -2
- package/dist/RealmContext.d.ts +153 -0
- package/dist/RealmContext.d.ts.map +1 -0
- package/dist/RealmProvider.d.ts +95 -0
- package/dist/RealmProvider.d.ts.map +1 -0
- package/dist/cachedCollection.d.ts +58 -0
- package/dist/cachedCollection.d.ts.map +1 -0
- package/dist/cachedObject.d.ts +48 -0
- package/dist/cachedObject.d.ts.map +1 -0
- package/dist/helpers.d.ts +22 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/index.cjs +2818 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +100 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2809 -0
- package/dist/index.js.map +1 -0
- package/dist/tsconfig.types.tsbuildinfo +1 -0
- package/dist/types.d.ts +64 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/useObject.d.ts +30 -0
- package/dist/useObject.d.ts.map +1 -0
- package/dist/useQuery.d.ts +31 -0
- package/dist/useQuery.d.ts.map +1 -0
- package/dist/useRealm.d.ts +12 -0
- package/dist/useRealm.d.ts.map +1 -0
- package/package.json +117 -4
- package/src/RealmContext.ts +196 -0
- package/src/RealmProvider.tsx +251 -0
- package/src/__tests__/RealmProvider.test.tsx +583 -0
- package/src/__tests__/createRealmTestContext.ts +68 -0
- package/src/__tests__/helpers.ts +35 -0
- package/src/__tests__/mocks.ts +154 -0
- package/src/__tests__/profileHook.tsx +76 -0
- package/src/__tests__/setup.ts +22 -0
- package/src/__tests__/tsconfig.json +14 -0
- package/src/__tests__/useObjectHook.test.tsx +158 -0
- package/src/__tests__/useObjectRender.test.tsx +630 -0
- package/src/__tests__/useQueryHook.test.tsx +209 -0
- package/src/__tests__/useQueryRender.test.tsx +463 -0
- package/src/cachedCollection.ts +210 -0
- package/src/cachedObject.ts +164 -0
- package/src/helpers.ts +60 -0
- package/src/index.tsx +130 -0
- package/src/runtime.d.ts +31 -0
- package/src/types.ts +92 -0
- package/src/useObject.tsx +227 -0
- package/src/useQuery.tsx +205 -0
- package/src/useRealm.tsx +41 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
//
|
|
3
|
+
// Copyright 2023 Realm Inc.
|
|
4
|
+
//
|
|
5
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
// you may not use this file except in compliance with the License.
|
|
7
|
+
// You may obtain a copy of the License at
|
|
8
|
+
//
|
|
9
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
//
|
|
11
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
// See the License for the specific language governing permissions and
|
|
15
|
+
// limitations under the License.
|
|
16
|
+
//
|
|
17
|
+
////////////////////////////////////////////////////////////////////////////
|
|
18
|
+
|
|
19
|
+
export enum AuthOperationName {
|
|
20
|
+
LogIn = "logIn",
|
|
21
|
+
LogInWithAnonymous = "logInWithAnonymous",
|
|
22
|
+
LogInWithApiKey = "logInWithApiKey",
|
|
23
|
+
LogInWithEmailPassword = "logInWithEmailPassword",
|
|
24
|
+
LogInWithJWT = "logInWithJWT",
|
|
25
|
+
LogInWithGoogle = "logInWithGoogle",
|
|
26
|
+
LogInWithApple = "logInWithApple",
|
|
27
|
+
LogInWithFacebook = "logInWithFacebook",
|
|
28
|
+
LogInWithFunction = "logInWithFunction",
|
|
29
|
+
LogOut = "logOut",
|
|
30
|
+
Register = "register",
|
|
31
|
+
Confirm = "confirm",
|
|
32
|
+
ResendConfirmationEmail = "resendConfirmationEmail",
|
|
33
|
+
RetryCustomConfirmation = "retryCustomConfirmation",
|
|
34
|
+
SendResetPasswordEmail = "sendResetPasswordEmail",
|
|
35
|
+
ResetPassword = "resetPassword",
|
|
36
|
+
CallResetPasswordFunction = "callResetPasswordFunction",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The `AuthError` is set based on the operation name and the error message.
|
|
41
|
+
*/
|
|
42
|
+
export class AuthError extends Error {
|
|
43
|
+
public operation: AuthOperationName;
|
|
44
|
+
constructor(operation: AuthOperationName, message: string) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.name = "AuthError";
|
|
47
|
+
this.operation = operation;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The possible states an {@link OperationResult} can be in.
|
|
53
|
+
*/
|
|
54
|
+
export enum OperationState {
|
|
55
|
+
NotStarted = "not-started",
|
|
56
|
+
Pending = "pending",
|
|
57
|
+
Success = "success",
|
|
58
|
+
Error = "error",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Represents the state and result of a call to a hook operation. This is
|
|
63
|
+
* designed to allow the user to update their UI accordingly without needing to
|
|
64
|
+
* store separate React state to keep track of pending/error states etc.
|
|
65
|
+
*/
|
|
66
|
+
export type OperationResult = {
|
|
67
|
+
/**
|
|
68
|
+
* The current state of the operation.
|
|
69
|
+
*/
|
|
70
|
+
state: OperationState;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The string name of the current operation running.
|
|
74
|
+
*/
|
|
75
|
+
operation: AuthOperationName | null;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Convenience accessors, so users can write e.g. `loginResult.pending`
|
|
79
|
+
* instead of `loginResult.state === OperationState.Pending`
|
|
80
|
+
*/
|
|
81
|
+
pending: boolean;
|
|
82
|
+
success: boolean;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The error returned from the operation, if any. This will only be populated
|
|
86
|
+
* if `state === OperationState.Error`, and will be cleared each time the
|
|
87
|
+
* operation is called.
|
|
88
|
+
*/
|
|
89
|
+
error: AuthError | undefined;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export type AuthResult = OperationResult;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
//
|
|
3
|
+
// Copyright 2021 Realm Inc.
|
|
4
|
+
//
|
|
5
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
// you may not use this file except in compliance with the License.
|
|
7
|
+
// You may obtain a copy of the License at
|
|
8
|
+
//
|
|
9
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
//
|
|
11
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
// See the License for the specific language governing permissions and
|
|
15
|
+
// limitations under the License.
|
|
16
|
+
//
|
|
17
|
+
////////////////////////////////////////////////////////////////////////////
|
|
18
|
+
|
|
19
|
+
import { useEffect, useMemo, useReducer, useRef } from "react";
|
|
20
|
+
import Realm from "realm";
|
|
21
|
+
|
|
22
|
+
import { CachedObject, createCachedObject } from "./cachedObject";
|
|
23
|
+
import {
|
|
24
|
+
AnyRealmObject,
|
|
25
|
+
CollectionCallback,
|
|
26
|
+
getObjectForPrimaryKey,
|
|
27
|
+
getObjects,
|
|
28
|
+
isClassModelConstructor,
|
|
29
|
+
} from "./helpers";
|
|
30
|
+
import { UseRealmHook } from "./useRealm";
|
|
31
|
+
|
|
32
|
+
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
33
|
+
type RealmClassType<T = any> = { new (...args: any): T };
|
|
34
|
+
|
|
35
|
+
export type ObjectHookOptions<T> = {
|
|
36
|
+
type: string;
|
|
37
|
+
primaryKey: T[keyof T];
|
|
38
|
+
keyPaths?: string | string[];
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type ObjectHookClassBasedOptions<T> = {
|
|
42
|
+
type: RealmClassType<T>;
|
|
43
|
+
primaryKey: T[keyof T];
|
|
44
|
+
keyPaths?: string | string[];
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type UseObjectHook = {
|
|
48
|
+
<T>(options: ObjectHookOptions<T>): (T & Realm.Object<T>) | null;
|
|
49
|
+
<T extends AnyRealmObject>(options: ObjectHookClassBasedOptions<T>): T | null;
|
|
50
|
+
|
|
51
|
+
<T>(type: string, primaryKey: T[keyof T], keyPaths?: string | string[]): (T & Realm.Object<T>) | null;
|
|
52
|
+
<T extends AnyRealmObject>(type: RealmClassType<T>, primaryKey: T[keyof T], keyPaths?: string | string[]): T | null;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Generates the `useObject` hook from a given `useRealm` hook.
|
|
57
|
+
* @param useRealm - Hook that returns an open Realm instance
|
|
58
|
+
* @returns useObject - Hook that is used to gain access to a single Realm object from a primary key
|
|
59
|
+
*/
|
|
60
|
+
export function createUseObject(useRealm: UseRealmHook): UseObjectHook {
|
|
61
|
+
function useObject<T extends AnyRealmObject>(
|
|
62
|
+
type: string | RealmClassType<T>,
|
|
63
|
+
primaryKey: T[keyof T],
|
|
64
|
+
keyPaths?: string | string[],
|
|
65
|
+
): T | null {
|
|
66
|
+
const realm = useRealm();
|
|
67
|
+
|
|
68
|
+
// Create a forceRerender function for the cachedObject to use as its updateCallback, so that
|
|
69
|
+
// the cachedObject can force the component using this hook to re-render when a change occurs.
|
|
70
|
+
const [, forceRerender] = useReducer((x) => x + 1, 0);
|
|
71
|
+
|
|
72
|
+
// Get the original object from the realm, so we can check if it exists
|
|
73
|
+
const originalObject = getObjectForPrimaryKey(realm, type, primaryKey);
|
|
74
|
+
|
|
75
|
+
// Store the primaryKey as a ref, since when it is an objectId or UUID, it will be a new instance on every render
|
|
76
|
+
const primaryKeyRef = useRef(primaryKey);
|
|
77
|
+
|
|
78
|
+
const collectionRef = useRef(getObjects(realm, type));
|
|
79
|
+
|
|
80
|
+
const objectRef = useRef<T & Realm.Object<T>>(null);
|
|
81
|
+
const updatedRef = useRef(true);
|
|
82
|
+
|
|
83
|
+
// Initializing references with a function call or class constructor will
|
|
84
|
+
// cause the function or constructor to be called on ever render.
|
|
85
|
+
// Even though this value is thrown away on subsequent renders, `createCachedObject` will end up registering a listener.
|
|
86
|
+
// Therefore, we initialize the references with null, and only create the object if it is null
|
|
87
|
+
// Ref: https://github.com/facebook/react/issues/14490
|
|
88
|
+
const cachedObjectRef = useRef<null | CachedObject>(null);
|
|
89
|
+
|
|
90
|
+
const memoizedKeyPaths = useMemo(
|
|
91
|
+
() => (typeof keyPaths === "string" ? [keyPaths] : keyPaths),
|
|
92
|
+
/* eslint-disable-next-line react-hooks/exhaustive-deps -- Memoizing the keyPaths to avoid renders */
|
|
93
|
+
[JSON.stringify(keyPaths)],
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
if (!cachedObjectRef.current) {
|
|
97
|
+
cachedObjectRef.current = createCachedObject({
|
|
98
|
+
object: originalObject ?? null,
|
|
99
|
+
realm,
|
|
100
|
+
updateCallback: forceRerender,
|
|
101
|
+
updatedRef,
|
|
102
|
+
keyPaths: memoizedKeyPaths,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Create a ref, since the object returned from `objectForPrimaryKey` is always going to have a different reference
|
|
107
|
+
const originalObjectRef = useRef(originalObject);
|
|
108
|
+
|
|
109
|
+
// Wrap the cachedObject in useMemo, so we only replace it with a new instance if `primaryKey` or `type` change
|
|
110
|
+
const { object, tearDown } = useMemo(
|
|
111
|
+
// TODO: There will be an upcoming breaking change that makes objectForPrimaryKey return null
|
|
112
|
+
// When this is implemented, remove `?? null`
|
|
113
|
+
() => {
|
|
114
|
+
// This should never happen, but if it does, we want to return a null result
|
|
115
|
+
if (!cachedObjectRef.current) {
|
|
116
|
+
return { object: null, tearDown: () => undefined };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Re-instantiate the cachedObject if the primaryKey has changed or the originalObject has gone from null to not null
|
|
120
|
+
if (
|
|
121
|
+
!arePrimaryKeysIdentical(primaryKey, primaryKeyRef.current) ||
|
|
122
|
+
(!originalObjectRef.current && originalObject)
|
|
123
|
+
) {
|
|
124
|
+
cachedObjectRef.current = createCachedObject({
|
|
125
|
+
object: originalObject ?? null,
|
|
126
|
+
realm,
|
|
127
|
+
updateCallback: forceRerender,
|
|
128
|
+
updatedRef,
|
|
129
|
+
keyPaths: memoizedKeyPaths,
|
|
130
|
+
});
|
|
131
|
+
originalObjectRef.current = originalObject;
|
|
132
|
+
|
|
133
|
+
// Primary key has updated, so update the reference
|
|
134
|
+
primaryKeyRef.current = primaryKey;
|
|
135
|
+
// Signal that the object reference needs to be updated
|
|
136
|
+
updatedRef.current = true;
|
|
137
|
+
}
|
|
138
|
+
return cachedObjectRef.current;
|
|
139
|
+
},
|
|
140
|
+
[realm, originalObject, primaryKey, memoizedKeyPaths],
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Invoke the tearDown of the cachedObject when useObject is unmounted
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
return tearDown;
|
|
146
|
+
}, [tearDown]);
|
|
147
|
+
|
|
148
|
+
// If the object doesn't exist, listen for insertions to the collection and force a rerender if the inserted object has the correct primary key
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
const collection = collectionRef.current;
|
|
151
|
+
const collectionListener: CollectionCallback = (_, changes) => {
|
|
152
|
+
const primaryKeyProperty = collection?.[0]?.objectSchema()?.primaryKey;
|
|
153
|
+
for (const index of changes.insertions) {
|
|
154
|
+
const object = collection[index];
|
|
155
|
+
if (primaryKeyProperty) {
|
|
156
|
+
//@ts-expect-error - if the primaryKeyProperty exists, then it is indexable. However, we don't allow it when we don't actually know the type of the object
|
|
157
|
+
const insertedPrimaryKey = object[primaryKeyProperty];
|
|
158
|
+
if (arePrimaryKeysIdentical(insertedPrimaryKey, primaryKeyRef.current)) {
|
|
159
|
+
forceRerender();
|
|
160
|
+
collection.removeListener(collectionListener);
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
if (!originalObjectRef.current) {
|
|
168
|
+
collection.addListener(collectionListener);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return () => {
|
|
172
|
+
// If the app is closing, the realm will be closed and the listener does not need to be removed if
|
|
173
|
+
if (!realm.isClosed && collection) {
|
|
174
|
+
collection.removeListener(collectionListener);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}, [realm, type, forceRerender]);
|
|
178
|
+
|
|
179
|
+
// If the object has been deleted or doesn't exist for the given primary key, just return null
|
|
180
|
+
if (!object?.isValid()) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (updatedRef.current) {
|
|
185
|
+
// Wrap object in a proxy to update the reference on rerender ( should only rerender when something has changed )
|
|
186
|
+
objectRef.current = new Proxy(object, {}) as T & Realm.Object<T>;
|
|
187
|
+
updatedRef.current = false;
|
|
188
|
+
}
|
|
189
|
+
// This will never be undefined, but the type system doesn't know that
|
|
190
|
+
return objectRef.current as T;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return function useObjectOverload<T extends AnyRealmObject>(
|
|
194
|
+
typeOrOptions: string | RealmClassType<T> | ObjectHookOptions<T> | ObjectHookClassBasedOptions<T>,
|
|
195
|
+
primaryKey?: T[keyof T],
|
|
196
|
+
keyPaths?: string | string[],
|
|
197
|
+
): T | null {
|
|
198
|
+
if (typeof typeOrOptions === "string" || isClassModelConstructor(typeOrOptions)) {
|
|
199
|
+
if (typeof primaryKey === "undefined") {
|
|
200
|
+
throw new Error("Expected a primary key");
|
|
201
|
+
}
|
|
202
|
+
/* eslint-disable-next-line react-hooks/rules-of-hooks -- We're calling `useQuery` once in any of the brances */
|
|
203
|
+
return useObject<T>(typeOrOptions, primaryKey, keyPaths);
|
|
204
|
+
} else {
|
|
205
|
+
const { type, primaryKey, keyPaths } = typeOrOptions;
|
|
206
|
+
/* eslint-disable-next-line react-hooks/rules-of-hooks -- We're calling `useQuery` once in any of the brances */
|
|
207
|
+
return useObject<T>(type, primaryKey, keyPaths);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// This is a helper function that determines if two primary keys are equal. It will also handle the case where the primary key is an ObjectId or UUID
|
|
213
|
+
function arePrimaryKeysIdentical(a: unknown, b: unknown): boolean {
|
|
214
|
+
if (typeof a !== typeof b) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
if (typeof a === "string" || typeof a === "number") {
|
|
218
|
+
return a === b;
|
|
219
|
+
}
|
|
220
|
+
if (a instanceof Realm.BSON.ObjectId && b instanceof Realm.BSON.ObjectId) {
|
|
221
|
+
return a.toHexString() === b.toHexString();
|
|
222
|
+
}
|
|
223
|
+
if (a instanceof Realm.BSON.UUID && b instanceof Realm.BSON.UUID) {
|
|
224
|
+
return a.toHexString() === b.toHexString();
|
|
225
|
+
}
|
|
226
|
+
return false;
|
|
227
|
+
}
|
package/src/useQuery.tsx
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
//
|
|
3
|
+
// Copyright 2021 Realm Inc.
|
|
4
|
+
//
|
|
5
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
// you may not use this file except in compliance with the License.
|
|
7
|
+
// You may obtain a copy of the License at
|
|
8
|
+
//
|
|
9
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
//
|
|
11
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
// See the License for the specific language governing permissions and
|
|
15
|
+
// limitations under the License.
|
|
16
|
+
//
|
|
17
|
+
////////////////////////////////////////////////////////////////////////////
|
|
18
|
+
|
|
19
|
+
import { useCallback, useEffect, useMemo, useReducer, useRef } from "react";
|
|
20
|
+
import Realm from "realm";
|
|
21
|
+
|
|
22
|
+
import { createCachedCollection } from "./cachedCollection";
|
|
23
|
+
import { AnyRealmObject, RealmClassType, getObjects, isClassModelConstructor } from "./helpers";
|
|
24
|
+
|
|
25
|
+
type QueryCallback<T> = (collection: Realm.Results<T>) => Realm.Results<T>;
|
|
26
|
+
type DependencyList = ReadonlyArray<unknown>;
|
|
27
|
+
|
|
28
|
+
export type QueryHookPartialOptions<T> = {
|
|
29
|
+
type: string | RealmClassType<T>;
|
|
30
|
+
keyPaths?: string | string[];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type QueryHookOptions<T> = QueryHookPartialOptions<T> & {
|
|
34
|
+
query?: QueryCallback<T>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type UseQueryHook = {
|
|
38
|
+
<T>(options: QueryHookOptions<T>, deps?: DependencyList): Realm.Results<T & Realm.Object<T>>;
|
|
39
|
+
<T extends AnyRealmObject>(options: QueryHookOptions<T>, deps?: DependencyList): Realm.Results<T>;
|
|
40
|
+
<T>(type: string): Realm.Results<T & Realm.Object<T>>;
|
|
41
|
+
<T extends AnyRealmObject>(type: RealmClassType<T>): Realm.Results<T>;
|
|
42
|
+
<T extends AnyRealmObject>(
|
|
43
|
+
query: QueryCallback<T>,
|
|
44
|
+
deps: DependencyList,
|
|
45
|
+
options: QueryHookPartialOptions<T>,
|
|
46
|
+
): Realm.Results<T>;
|
|
47
|
+
<T>(query: QueryCallback<T>, deps: DependencyList, options: QueryHookPartialOptions<T>): Realm.Results<
|
|
48
|
+
T & Realm.Object<T>
|
|
49
|
+
>;
|
|
50
|
+
|
|
51
|
+
/** @deprecated To help the `react-hooks/exhaustive-deps` eslint rule detect missing dependencies, we've suggest passing a option object as the first argument */
|
|
52
|
+
<T>(type: string, query?: QueryCallback<T>, deps?: DependencyList): Realm.Results<T & Realm.Object<T>>;
|
|
53
|
+
/** @deprecated To help the `react-hooks/exhaustive-deps` eslint rule detect missing dependencies, we've suggest passing a option object as the first argument */
|
|
54
|
+
<T extends AnyRealmObject>(
|
|
55
|
+
type: RealmClassType<T>,
|
|
56
|
+
query?: QueryCallback<T>,
|
|
57
|
+
deps?: DependencyList,
|
|
58
|
+
): Realm.Results<T>;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
type PossibleQueryArgs<T> = {
|
|
62
|
+
typeOrOptionsOrQuery: QueryHookOptions<T> | string | RealmClassType<T> | QueryCallback<T>;
|
|
63
|
+
queryOrDeps?: DependencyList | QueryCallback<T>;
|
|
64
|
+
depsOrPartialOptions?: DependencyList | QueryHookPartialOptions<T>;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Maps a value to itself
|
|
69
|
+
*/
|
|
70
|
+
function identity<T>(value: T): T {
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Generates the `useQuery` hook from a given `useRealm` hook.
|
|
76
|
+
* @param useRealm - Hook that returns an open Realm instance
|
|
77
|
+
* @returns useObject - Hook that is used to gain access to a {@link Realm.Collection}
|
|
78
|
+
*/
|
|
79
|
+
export function createUseQuery(useRealm: () => Realm): UseQueryHook {
|
|
80
|
+
function useQuery<T extends AnyRealmObject>(
|
|
81
|
+
{ type, query = identity, keyPaths }: QueryHookOptions<T>,
|
|
82
|
+
deps: DependencyList = [],
|
|
83
|
+
): Realm.Results<T> {
|
|
84
|
+
const realm = useRealm();
|
|
85
|
+
|
|
86
|
+
// We need to add the type to the deps, so that if the type changes, the query will be re-run.
|
|
87
|
+
// This will be saved in an array which will be spread into the provided deps.
|
|
88
|
+
const requiredDeps = [type];
|
|
89
|
+
|
|
90
|
+
// Create a forceRerender function for the cachedCollection to use as its updateCallback, so that
|
|
91
|
+
// the cachedCollection can force the component using this hook to re-render when a change occurs.
|
|
92
|
+
const [, forceRerender] = useReducer((x) => x + 1, 0);
|
|
93
|
+
const collectionRef = useRef<Realm.Results<T>>(null);
|
|
94
|
+
const updatedRef = useRef(true);
|
|
95
|
+
const queryCallbackRef = useRef<QueryCallback<T> | null>(null);
|
|
96
|
+
|
|
97
|
+
/* eslint-disable-next-line react-hooks/exhaustive-deps -- We want the user of this hook to be able pass in the `query` function inline (without the need to `useCallback` on it)
|
|
98
|
+
This means that the query function is unstable and will be a redefined on each render of the component where `useQuery` is used
|
|
99
|
+
Therefore we use the `deps` array to memoize the query function internally, and only use the returned `queryCallback` */
|
|
100
|
+
const queryCallback = useCallback(query, [...deps, ...requiredDeps]);
|
|
101
|
+
|
|
102
|
+
// If the query function changes, we need to update the cachedCollection
|
|
103
|
+
if (queryCallbackRef.current !== queryCallback) {
|
|
104
|
+
queryCallbackRef.current = queryCallback;
|
|
105
|
+
updatedRef.current = true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const queryResult = useMemo(() => {
|
|
109
|
+
return queryCallback(getObjects(realm, type));
|
|
110
|
+
}, [type, realm, queryCallback]);
|
|
111
|
+
|
|
112
|
+
const memoizedKeyPaths = useMemo(
|
|
113
|
+
() => (typeof keyPaths === "string" ? [keyPaths] : keyPaths),
|
|
114
|
+
/* eslint-disable-next-line react-hooks/exhaustive-deps -- Memoizing the keyPaths to avoid renders */
|
|
115
|
+
[JSON.stringify(keyPaths)],
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// Wrap the cachedObject in useMemo, so we only replace it with a new instance if `realm` or `queryResult` change
|
|
119
|
+
const { collection, tearDown } = useMemo(() => {
|
|
120
|
+
return createCachedCollection<T>({
|
|
121
|
+
collection: queryResult,
|
|
122
|
+
realm,
|
|
123
|
+
updateCallback: forceRerender,
|
|
124
|
+
updatedRef,
|
|
125
|
+
keyPaths: memoizedKeyPaths,
|
|
126
|
+
});
|
|
127
|
+
}, [realm, queryResult, memoizedKeyPaths]);
|
|
128
|
+
|
|
129
|
+
// Invoke the tearDown of the cachedCollection when useQuery is unmounted
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
return tearDown;
|
|
132
|
+
}, [tearDown]);
|
|
133
|
+
|
|
134
|
+
// This makes sure the collection has a different reference on a rerender
|
|
135
|
+
// Also we are ensuring the type returned is Realm.Results, as this is known in this context
|
|
136
|
+
if (updatedRef.current) {
|
|
137
|
+
updatedRef.current = false;
|
|
138
|
+
collectionRef.current = new Proxy(collection as Realm.Results<T & Realm.Object>, {});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// This will never not be defined, but the type system doesn't know that
|
|
142
|
+
return collectionRef.current as Realm.Results<T>;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return function useQueryOverload<T extends AnyRealmObject>(
|
|
146
|
+
typeOrOptionsOrQuery: PossibleQueryArgs<T>["typeOrOptionsOrQuery"],
|
|
147
|
+
queryOrDeps: PossibleQueryArgs<T>["queryOrDeps"] = identity,
|
|
148
|
+
depsOrPartialOptions: PossibleQueryArgs<T>["depsOrPartialOptions"] = [],
|
|
149
|
+
): Realm.Results<T> {
|
|
150
|
+
const args = { typeOrOptionsOrQuery, queryOrDeps, depsOrPartialOptions };
|
|
151
|
+
/* eslint-disable react-hooks/rules-of-hooks -- We're calling `useQuery` once in any of the brances */
|
|
152
|
+
if (isTypeFunctionDeps(args)) {
|
|
153
|
+
return useQuery({ type: args.typeOrOptionsOrQuery, query: args.queryOrDeps }, args.depsOrPartialOptions);
|
|
154
|
+
}
|
|
155
|
+
if (isOptionsDepsNone(args)) {
|
|
156
|
+
return useQuery(args.typeOrOptionsOrQuery, Array.isArray(args.queryOrDeps) ? args.queryOrDeps : []);
|
|
157
|
+
}
|
|
158
|
+
if (isFunctionDepsOptions(args)) {
|
|
159
|
+
return useQuery({ ...args.depsOrPartialOptions, query: args.typeOrOptionsOrQuery }, args.queryOrDeps);
|
|
160
|
+
}
|
|
161
|
+
/* eslint-enable react-hooks/rules-of-hooks */
|
|
162
|
+
|
|
163
|
+
throw new Error("Unexpected arguments passed to useQuery");
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function isTypeFunctionDeps<T>(args: PossibleQueryArgs<T>): args is {
|
|
168
|
+
typeOrOptionsOrQuery: string | RealmClassType<T>;
|
|
169
|
+
queryOrDeps: QueryCallback<T>;
|
|
170
|
+
depsOrPartialOptions: DependencyList;
|
|
171
|
+
} {
|
|
172
|
+
const { typeOrOptionsOrQuery, queryOrDeps, depsOrPartialOptions } = args;
|
|
173
|
+
return (
|
|
174
|
+
(typeof typeOrOptionsOrQuery === "string" || isClassModelConstructor(typeOrOptionsOrQuery)) &&
|
|
175
|
+
typeof queryOrDeps === "function" &&
|
|
176
|
+
Array.isArray(depsOrPartialOptions)
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isOptionsDepsNone<T>(args: PossibleQueryArgs<T>): args is {
|
|
181
|
+
typeOrOptionsOrQuery: QueryHookOptions<T>;
|
|
182
|
+
queryOrDeps: DependencyList | typeof identity;
|
|
183
|
+
depsOrPartialOptions: never;
|
|
184
|
+
} {
|
|
185
|
+
const { typeOrOptionsOrQuery, queryOrDeps } = args;
|
|
186
|
+
return (
|
|
187
|
+
typeof typeOrOptionsOrQuery === "object" &&
|
|
188
|
+
typeOrOptionsOrQuery !== null &&
|
|
189
|
+
(Array.isArray(queryOrDeps) || queryOrDeps === identity)
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function isFunctionDepsOptions<T>(args: PossibleQueryArgs<T>): args is {
|
|
194
|
+
typeOrOptionsOrQuery: QueryCallback<T>;
|
|
195
|
+
queryOrDeps: DependencyList;
|
|
196
|
+
depsOrPartialOptions: QueryHookPartialOptions<T>;
|
|
197
|
+
} {
|
|
198
|
+
const { typeOrOptionsOrQuery, queryOrDeps, depsOrPartialOptions } = args;
|
|
199
|
+
return (
|
|
200
|
+
typeof typeOrOptionsOrQuery === "function" &&
|
|
201
|
+
Array.isArray(queryOrDeps) &&
|
|
202
|
+
typeof depsOrPartialOptions === "object" &&
|
|
203
|
+
depsOrPartialOptions !== null
|
|
204
|
+
);
|
|
205
|
+
}
|
package/src/useRealm.tsx
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
//
|
|
3
|
+
// Copyright 2021 Realm Inc.
|
|
4
|
+
//
|
|
5
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
// you may not use this file except in compliance with the License.
|
|
7
|
+
// You may obtain a copy of the License at
|
|
8
|
+
//
|
|
9
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
//
|
|
11
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
// See the License for the specific language governing permissions and
|
|
15
|
+
// limitations under the License.
|
|
16
|
+
//
|
|
17
|
+
////////////////////////////////////////////////////////////////////////////
|
|
18
|
+
|
|
19
|
+
import Realm from "realm";
|
|
20
|
+
import { useContext } from "react";
|
|
21
|
+
|
|
22
|
+
export type UseRealmHook = {
|
|
23
|
+
(): Realm;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Generates a `useRealm` hook given a RealmContext. This allows access to the {@link Realm}
|
|
28
|
+
* instance anywhere within the RealmProvider.
|
|
29
|
+
* @param RealmContext - The context containing the {@link Realm} instance
|
|
30
|
+
* @returns useRealm - Hook that is used to gain access to the {@link Realm} instance
|
|
31
|
+
*/
|
|
32
|
+
export const createUseRealm = (RealmContext: React.Context<Realm | null>): UseRealmHook => {
|
|
33
|
+
return function useRealm() {
|
|
34
|
+
// This is the context setup by `createRealmContext`
|
|
35
|
+
const context = useContext(RealmContext);
|
|
36
|
+
if (context === null) {
|
|
37
|
+
throw new Error("Realm context not found. Did you call useRealm() within a <RealmProvider/>?");
|
|
38
|
+
}
|
|
39
|
+
return context;
|
|
40
|
+
};
|
|
41
|
+
};
|