-
Notifications
You must be signed in to change notification settings - Fork 563
/
RealmProvider.tsx
197 lines (177 loc) · 7.13 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
////////////////////////////////////////////////////////////////////////////
//
// 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";
import { UserContext } from "./UserProvider";
type PartialRealmConfiguration = Omit<Partial<Realm.Configuration>, "sync"> & {
sync?: Partial<Realm.SyncConfiguration>;
};
type ProviderProps = PartialRealmConfiguration & {
/**
* The fallback component to render if the Realm is not opened.
*/
fallback?: React.ComponentType<unknown> | React.ReactElement | null | undefined;
/**
* 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>;
children: React.ReactNode;
};
/**
* 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 createRealmProvider(
realmConfig: Realm.Configuration,
RealmContext: React.Context<Realm | null>,
): React.FC<ProviderProps> {
/**
* Returns a Context Provider component that is required to wrap any component using
* the Realm hooks.
* @example
* ```
* const AppRoot = () => {
* const syncConfig = {
* flexible: true,
* user: currentUser
* };
*
* return (
* <RealmProvider path="data.realm" sync={syncConfig}>
* <App/>
* </RealmProvider>
* )
* }
* ```
* @param props - The {@link Realm.Configuration} for this Realm defaults to
* the config passed to `createRealmProvider`, but individual config keys can
* be overridden when creating a `<RealmProvider>` by passing them as props.
* For example, to override the `path` config value, use a prop named `path`,
* e.g. `path="newPath.realm"`
*/
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]);
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);
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 />;
}
return <>{Fallback}</>;
}
return <RealmContext.Provider value={realm} children={children} />;
};
}
/**
* 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);
}