-
Notifications
You must be signed in to change notification settings - Fork 587
/
Copy pathRealmProvider.tsx
276 lines (243 loc) · 10.5 KB
/
RealmProvider.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
////////////////////////////////////////////////////////////////////////////
//
// Copyright 2021 Realm Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////
import React, { useContext, useEffect, useRef, useState } from "react";
import Realm from "realm";
import isEqual from "lodash.isequal";
import { UserContext } from "./UserProvider";
import { RestrictivePick } from "./helpers";
type PartialRealmConfiguration = Omit<Partial<Realm.Configuration>, "sync"> & {
sync?: Partial<Realm.SyncConfiguration>;
};
export type RealmProviderFallback = React.ComponentType<{
progress: number;
}>;
/** Props used for a configuration-based Realm provider */
type RealmProviderConfigurationProps = {
/**
* If false, Realm will not be closed when the component unmounts.
* @default true
*/
closeOnUnmount?: boolean;
/**
* A ref to the Realm instance. This is useful if you need to access the Realm
* instance outside of a component that uses the Realm hooks.
*/
realmRef?: React.MutableRefObject<Realm | null>;
/**
* The fallback component to render if the Realm is not open.
*/
fallback?: RealmProviderFallback | React.ComponentType | React.ReactElement | null | undefined;
children: React.ReactNode;
} & PartialRealmConfiguration;
/** Props used for a Realm instance-based Realm provider */
type RealmProviderRealmProps = {
/**
* The Realm instance to be used by the provider.
*/
realm: Realm;
children: React.ReactNode;
};
type RealmProviderProps = RealmProviderConfigurationProps & RealmProviderRealmProps;
/**
* Represents the provider returned from `createRealmContext` with a Realm instance i.e. `createRealmContext(new Realm(...))`.
* Omits "realm" as it gets set at creation and cannot be changed.
* **Note:** the hooks returned from `createRealmContext` using an existing Realm can be used outside of the scope of the provider.
*/
export type RealmProviderFromRealm = React.FC<Omit<RealmProviderRealmProps, "realm">>;
/**
* Represents the provider returned from `createRealmContext` with a configuration, i.e. `createRealmContext({schema: [...]})`.
*/
export type RealmProviderFromConfiguration = React.FC<RealmProviderConfigurationProps>;
/**
* Represents properties of a {@link DynamicRealmProvider} where Realm instance props are set and Configuration props are disallowed.
*/
export type DynamicRealmProviderWithRealmProps = RestrictivePick<RealmProviderProps, keyof RealmProviderRealmProps>;
/**
* Represents properties of a {@link DynamicRealmProvider} where Realm configuration props are set and Realm instance props are disallowed.
*/
export type DynamicRealmProviderWithConfigurationProps = RestrictivePick<
RealmProviderProps,
keyof RealmProviderConfigurationProps
>;
/**
* Represents the provider returned from creating context with no arguments (including the default context).
* Supports either {@link RealmProviderRealmProps} or {@link RealmProviderConfigurationProps}.
*/
export type DynamicRealmProvider = React.FC<
DynamicRealmProviderWithRealmProps | DynamicRealmProviderWithConfigurationProps
>;
export function createRealmProviderFromRealm(
realm: Realm,
RealmContext: React.Context<Realm | null>,
): RealmProviderFromRealm {
return ({ children }) => {
return <RealmContext.Provider value={realm} children={children} />;
};
}
/**
* Generates a `RealmProvider` given a {@link Realm.Configuration} and {@link React.Context}.
* @param realmConfig - The configuration of the Realm to be instantiated
* @param RealmContext - The context that will contain the Realm instance
* @returns a RealmProvider component that provides context to all context hooks
*/
export function createRealmProviderFromConfig(
realmConfig: Realm.Configuration,
RealmContext: React.Context<Realm | null>,
): RealmProviderFromConfiguration {
return ({ children, fallback: Fallback, closeOnUnmount = true, realmRef, ...restProps }) => {
const [realm, setRealm] = useState<Realm | null>(() =>
realmConfig.sync === undefined && restProps.sync === undefined
? new Realm(mergeRealmConfiguration(realmConfig, restProps))
: null,
);
// Automatically set the user in the configuration if its been set.
// Grabbing directly from the context to avoid throwing an error if the user is not set.
const user = useContext(UserContext);
// We increment `configVersion` when a config override passed as a prop
// changes, which triggers a `useEffect` to re-open the Realm with the
// new config
const [configVersion, setConfigVersion] = useState(0);
// We put realm in a ref to avoid have an endless loop of updates when the realm is updated
const currentRealm = useRef(realm);
// This will merge the configuration provided by createRealmContext and any configuration properties
// set directly on the RealmProvider component. Any settings on the component will override the original configuration.
const configuration = useRef<Realm.Configuration>(mergeRealmConfiguration(realmConfig, restProps));
// Merge and set the configuration again and increment the version if any
// of the RealmProvider properties change.
useEffect(() => {
const combinedConfig = mergeRealmConfiguration(realmConfig, restProps);
// If there is a user in the current context and not one set by the props, then use the one from context
const combinedConfigWithUser =
combinedConfig?.sync && user ? mergeRealmConfiguration({ sync: { user } }, combinedConfig) : combinedConfig;
if (!areConfigurationsIdentical(configuration.current, combinedConfigWithUser)) {
configuration.current = combinedConfigWithUser;
// Only rerender if realm has already been configured
if (currentRealm.current != null) {
setConfigVersion((x) => x + 1);
}
}
}, [restProps, user]);
useEffect(() => {
currentRealm.current = realm;
if (realmRef) {
realmRef.current = realm;
}
}, [realm]);
const [progress, setProgress] = useState<number>(0);
useEffect(() => {
const realmRef = currentRealm.current;
// Check if we currently have an open Realm. If we do not (i.e. it is the first
// render, or the Realm has been closed due to a config change), then we
// need to open a new Realm.
const shouldInitRealm = realmRef === null;
const initRealm = async () => {
const openRealm = await Realm.open(configuration.current).progress((estimate: number) => {
setProgress(estimate);
});
setRealm(openRealm);
};
if (shouldInitRealm) {
initRealm().catch(console.error);
}
return () => {
if (realm) {
if (closeOnUnmount) {
realm.close();
}
setRealm(null);
}
};
}, [configVersion, realm, setRealm, closeOnUnmount]);
if (!realm) {
if (typeof Fallback === "function") {
return <Fallback progress={progress} />;
}
return <>{Fallback}</>;
}
return <RealmContext.Provider value={realm} children={children} />;
};
}
/**
* Generates a `RealmProvider` which is either based on a configuration
* or based on a realm, depending on its props.
* @param RealmContext - The context that will contain the Realm instance
* @returns a RealmProvider component that provides context to all context hooks
*/
export function createDynamicRealmProvider(RealmContext: React.Context<Realm | null>): DynamicRealmProvider {
const RealmProviderFromConfig = createRealmProviderFromConfig({}, RealmContext);
return ({ realm, children, ...config }) => {
if (realm) {
if (Object.keys(config).length > 0) {
throw new Error("Cannot use configuration props when using an existing Realm instance.");
}
return <RealmContext.Provider value={realm} children={children} />;
} else {
return <RealmProviderFromConfig {...config} children={children} />;
}
};
}
/**
* Generates the appropriate `RealmProvider` based on whether there is a config, realm, or neither given.
* @param realmOrConfig - A Realm instance, a configuration, or undefined (including default provider).
* @param RealmContext - The context that will contain the Realm instance
* @returns a RealmProvider component that provides context to all context hooks
*/
export function createRealmProvider(
realmOrConfig: Realm.Configuration | Realm | undefined,
RealmContext: React.Context<Realm | null>,
): RealmProviderFromConfiguration | RealmProviderFromRealm | DynamicRealmProvider {
if (!realmOrConfig) {
return createDynamicRealmProvider(RealmContext);
} else if (realmOrConfig instanceof Realm) {
return createRealmProviderFromRealm(realmOrConfig, RealmContext);
} else {
return createRealmProviderFromConfig(realmOrConfig, RealmContext);
}
}
/**
* Merge two configurations, creating a configuration using `configA` as the default,
* merged with `configB`, with properties in `configB` overriding `configA`.
* @param configA - The default config object
* @param configB - Config overrides object
* @returns Merged config object
*/
export function mergeRealmConfiguration(
configA: PartialRealmConfiguration,
configB: PartialRealmConfiguration,
): Realm.Configuration {
// In order to granularly update sync properties on the RealmProvider, sync must be
// seperately applied to the configuration. This allows for dynamic updates to the
// partition field.
const sync = { ...configA.sync, ...configB.sync };
return {
...configA,
...configB,
//TODO: When Realm >= 10.9.0 is a peer dependency, we can simply spread sync here
//See issue #4012
...(Object.keys(sync).length > 0 ? { sync } : undefined),
} as Realm.Configuration;
}
/**
* Utility function that does a deep comparison (key: value) of object a with object b
* @param a - Object to compare
* @param b - Object to compare
* @returns True if the objects are identical
*/
export function areConfigurationsIdentical(a: Realm.Configuration, b: Realm.Configuration): boolean {
return isEqual(a, b);
}