Skip to content

Commit

Permalink
RJS-2673: Prevent flickering behavior in RealmProvider (#6550)
Browse files Browse the repository at this point in the history
* 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()'.
  • Loading branch information
elle-j authored Apr 5, 2024
1 parent 0bd8ee4 commit 91de944
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 19 deletions.
1 change: 1 addition & 0 deletions packages/realm-react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
52 changes: 38 additions & 14 deletions packages/realm-react/src/UserProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -35,30 +35,54 @@ 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.
*/
export const UserProvider: React.FC<UserProviderProps> = ({ fallback: Fallback, children }) => {
const app = useApp();
const [user, setUser] = useState<Realm.User | null>(() => 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") {
Expand Down
2 changes: 1 addition & 1 deletion packages/realm/src/Listeners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export type ListenersOptions<CallbackType, TokenType, Args extends unknown[]> =

/** @internal */
export class Listeners<CallbackType, TokenType, Args extends unknown[] = []> {
constructor(private options: ListenersOptions<CallbackType, TokenType, Args>) {}
constructor(private readonly options: ListenersOptions<CallbackType, TokenType, Args>) {}
/**
* Mapping of registered listener callbacks onto the their token in the bindings ObjectNotifier.
*/
Expand Down
30 changes: 26 additions & 4 deletions packages/realm/src/app-services/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<UserChangeCallback, UserListenerToken>({
/** @internal */
private readonly listeners = new Listeners<UserChangeCallback, UserListenerToken>({
add: (callback: () => void): UserListenerToken => {
return this.internal.subscribe(callback);
},
Expand Down Expand Up @@ -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,
});
}

/**
Expand Down

0 comments on commit 91de944

Please sign in to comment.