From 91de9446242a3dc7aaaf640925af360b5d91a7d2 Mon Sep 17 00:00:00 2001 From: LJ <81748770+elle-j@users.noreply.github.com> Date: Fri, 5 Apr 2024 13:11:27 +0200 Subject: [PATCH] RJS-2673: Prevent flickering behavior in `RealmProvider` (#6550) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Make internal 'User' fields non-enumerable. * [realm/react] Set user only when specific fields change. * [realm/react] Use the same function reference for the listener when triggering rerender. * [realm/react] Add CHANGELOG entry. * Add comment explaining why to avoid the 'useEffect()'. --- packages/realm-react/CHANGELOG.md | 1 + packages/realm-react/src/UserProvider.tsx | 52 +++++++++++++++++------ packages/realm/src/Listeners.ts | 2 +- packages/realm/src/app-services/User.ts | 30 +++++++++++-- 4 files changed, 66 insertions(+), 19 deletions(-) diff --git a/packages/realm-react/CHANGELOG.md b/packages/realm-react/CHANGELOG.md index 759a80ebb4..6ba8012ab4 100644 --- a/packages/realm-react/CHANGELOG.md +++ b/packages/realm-react/CHANGELOG.md @@ -17,6 +17,7 @@ ### Fixed * Removed race condition in `useObject` ([#6291](https://github.com/realm/realm-js/issues/6291)) Thanks [@bimusik](https://github.com/bimusiek)! +* Fixed flickering of the `RealmProvider`'s `fallback` component and its `children` when offline. ([#6333](https://github.com/realm/realm-js/issues/6333)) ### Compatibility * React Native >= v0.71.4 diff --git a/packages/realm-react/src/UserProvider.tsx b/packages/realm-react/src/UserProvider.tsx index 90da85ee20..908fe71f0e 100644 --- a/packages/realm-react/src/UserProvider.tsx +++ b/packages/realm-react/src/UserProvider.tsx @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import React, { createContext, useContext, useEffect, useState } from "react"; +import React, { createContext, useContext, useEffect, useReducer, useState } from "react"; import type Realm from "realm"; import { useApp } from "./AppProvider"; @@ -35,6 +35,21 @@ type UserProviderProps = { children: React.ReactNode; }; +function userWasUpdated(userA: Realm.User | null, userB: Realm.User | null) { + if (!userA && !userB) { + return false; + } else if (userA && userB) { + return ( + userA.id !== userB.id || + userA.state !== userB.state || + userA.accessToken !== userB.accessToken || + userA.refreshToken !== userB.refreshToken + ); + } else { + return true; + } +} + /** * React component providing a Realm user on the context for the sync hooks * to use. A `UserProvider` is required for an app to use the hooks. @@ -42,23 +57,32 @@ type UserProviderProps = { export const UserProvider: React.FC = ({ fallback: Fallback, children }) => { const app = useApp(); const [user, setUser] = useState(() => app.currentUser); + const [, forceUpdate] = useReducer((x) => x + 1, 0); - // Support for a possible change in configuration - if (app.currentUser?.id != user?.id) { - setUser(app.currentUser); + // Support for a possible change in configuration. + // Do the check here rather than in a `useEffect()` so as to not render stale state. This allows + // for the rerender to restart without also having to rerender the children using the stale state. + const currentUser = app.currentUser; + if (userWasUpdated(user, currentUser)) { + setUser(currentUser); } useEffect(() => { - const event = () => { - setUser(app.currentUser); - }; - user?.addListener(event); - app?.addListener(event); - return () => { - user?.removeListener(event); - app?.removeListener(event); - }; - }, [user, app]); + app.addListener(forceUpdate); + + return () => app.removeListener(forceUpdate); + }, [app]); + + useEffect(() => { + user?.addListener(forceUpdate); + + return () => user?.removeListener(forceUpdate); + + /* + eslint-disable-next-line react-hooks/exhaustive-deps + -- We should depend on `user.id` rather than `user` as the ID will indicate a new user in this case. + */ + }, [user?.id]); if (!user) { if (typeof Fallback === "function") { diff --git a/packages/realm/src/Listeners.ts b/packages/realm/src/Listeners.ts index 976e83a6c2..07e57dcda7 100644 --- a/packages/realm/src/Listeners.ts +++ b/packages/realm/src/Listeners.ts @@ -32,7 +32,7 @@ export type ListenersOptions = /** @internal */ export class Listeners { - constructor(private options: ListenersOptions) {} + constructor(private readonly options: ListenersOptions) {} /** * Mapping of registered listener callbacks onto the their token in the bindings ObjectNotifier. */ diff --git a/packages/realm/src/app-services/User.ts b/packages/realm/src/app-services/User.ts index 379ae77210..e3106e7b51 100644 --- a/packages/realm/src/app-services/User.ts +++ b/packages/realm/src/app-services/User.ts @@ -87,15 +87,16 @@ export class User< UserProfileDataType extends DefaultUserProfileData = DefaultUserProfileData, > { /** @internal */ - public app: App; + public readonly app: App; /** @internal */ - public internal: binding.SyncUser; + public readonly internal: binding.SyncUser; - // cached version of profile + /** @internal */ private cachedProfile: UserProfileDataType | undefined; - private listeners = new Listeners({ + /** @internal */ + private readonly listeners = new Listeners({ add: (callback: () => void): UserListenerToken => { return this.internal.subscribe(callback); }, @@ -123,6 +124,27 @@ export class User< this.internal = internal; this.app = app; this.cachedProfile = undefined; + + Object.defineProperty(this, "listeners", { + enumerable: false, + configurable: false, + writable: false, + }); + Object.defineProperty(this, "internal", { + enumerable: false, + configurable: false, + writable: false, + }); + Object.defineProperty(this, "app", { + enumerable: false, + configurable: false, + writable: false, + }); + Object.defineProperty(this, "cachedProfile", { + enumerable: false, + configurable: false, + writable: true, + }); } /**