Skip to content

Commit

Permalink
Chore: Replace useSubscription with useSyncExternalStore (#25909)
Browse files Browse the repository at this point in the history
<!-- This is a pull request template, you do not need to uncomment or remove the comments, they won't show up in the PR text. -->

<!-- Your Pull Request name should start with one of the following tags
  [NEW] For new features
  [IMPROVE] For an improvement (performance or little improvements) in existing features
  [FIX] For bug fixes that affect the end-user
  [BREAK] For pull requests including breaking changes
  Chore: For small tasks
  Doc: For documentation
-->

<!-- Checklist!!! If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code. 
  - I have read the Contributing Guide - https://github.com/RocketChat/Rocket.Chat/blob/develop/.github/CONTRIBUTING.md#contributing-to-rocketchat doc
  - I have signed the CLA - https://cla-assistant.io/RocketChat/Rocket.Chat
  - Lint and unit tests pass locally with my changes
  - I have added tests that prove my fix is effective or that my feature works (if applicable)
  - I have added necessary documentation (if applicable)
  - Any dependent changes have been merged and published in downstream modules
-->

## Proposed changes (including videos or screenshots)
<!-- CHANGELOG -->
<!--
  Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request.
  If it fixes a bug or resolves a feature request, be sure to link to that issue below.
  This description will appear in the release notes if we accept the contribution.
-->

<!-- END CHANGELOG -->

## Issue(s)
<!-- Link the issues being closed by or related to this PR. For example, you can use #594 if this PR closes issue number 594 -->

## Steps to test or reproduce
<!-- Mention how you would reproduce the bug if not mentioned on the issue page already. Also mention which screens are going to have the changes if applicable -->

## Further comments
<!-- If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... -->
  • Loading branch information
tassoevan committed Jun 18, 2022
1 parent eedc18b commit 0862751
Show file tree
Hide file tree
Showing 69 changed files with 461 additions and 516 deletions.
13 changes: 5 additions & 8 deletions apps/meteor/client/components/Header/Header.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ export default {
value={{
hasPrivateAccess: true,
isLoading: false,
querySetting: (_id) => ({
getCurrentValue: () => ({
querySetting: (_id) => [
() => () => undefined,
() => ({
_id,
type: 'action',
value: '',
Expand All @@ -44,12 +45,8 @@ export default {
sorter: 1,
ts: new Date(),
}),
subscribe: () => () => undefined,
}),
querySettings: () => ({
getCurrentValue: () => [],
subscribe: () => () => undefined,
}),
],
querySettings: () => [() => () => undefined, () => []],
dispatch: async () => undefined,
}}
>
Expand Down
5 changes: 2 additions & 3 deletions apps/meteor/client/components/Omnichannel/Tags.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ import { Field, TextInput, Chip, Button } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts';
import React, { ChangeEvent, ReactElement, useState } from 'react';
import { useSubscription } from 'use-subscription';

import { AsyncStatePhase } from '../../hooks/useAsyncState';
import { useEndpointData } from '../../hooks/useEndpointData';
import { formsSubscription } from '../../views/omnichannel/additionalForms';
import { useFormsSubscription } from '../../views/omnichannel/additionalForms';
import { FormSkeleton } from './Skeleton';

const Tags = ({
Expand All @@ -21,7 +20,7 @@ const Tags = ({
tagRequired?: boolean;
}): ReactElement => {
const t = useTranslation();
const forms = useSubscription<any>(formsSubscription);
const forms = useFormsSubscription() as any;

const { value: tagsResult, phase: stateTags } = useEndpointData('/v1/livechat/tags.list');

Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,26 @@
import { createContext, useMemo, useContext } from 'react';
import { useSubscription, Unsubscribe } from 'use-subscription';
import { useSyncExternalStore } from 'use-sync-external-store/shim';

import { AsyncState } from '../../../../lib/asyncState/AsyncState';
import { AsyncStatePhase } from '../../../../lib/asyncState/AsyncStatePhase';

type IOmnichannelRoomIconContext = {
queryIcon(
app: string,
icon: string,
): {
getCurrentValue: () => AsyncState<string>;
subscribe: (callback: () => void) => Unsubscribe;
};
queryIcon(app: string, icon: string): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => AsyncState<string>];
};

export const OmnichannelRoomIconContext = createContext<IOmnichannelRoomIconContext>({
queryIcon: () => ({
getCurrentValue: (): AsyncState<string> => ({
queryIcon: () => [
(): (() => void) => (): void => undefined,
(): AsyncState<string> => ({
phase: AsyncStatePhase.LOADING,
value: undefined,
error: undefined,
}),
subscribe: (): Unsubscribe => (): void => undefined,
}),
],
});

export const useOmnichannelRoomIcon = (app: string, icon: string): AsyncState<string> => {
const { queryIcon } = useContext(OmnichannelRoomIconContext);
return useSubscription(useMemo(() => queryIcon(app, icon), [app, queryIcon, icon]));
const [subscribe, getSnapshot] = useMemo(() => queryIcon(app, icon), [app, queryIcon, icon]);
return useSyncExternalStore(subscribe, getSnapshot);
};
Original file line number Diff line number Diff line change
@@ -1,28 +1,37 @@
import React, { FC, useMemo } from 'react';
import React, { FC, useCallback, useMemo } from 'react';
import { createPortal } from 'react-dom';
import { useSubscription, Subscription } from 'use-subscription';
import { useSyncExternalStore } from 'use-sync-external-store/shim';

import { AsyncState } from '../../../../lib/asyncState/AsyncState';
import { AsyncStatePhase } from '../../../../lib/asyncState/AsyncStatePhase';
import { OmnichannelRoomIconContext } from '../context/OmnichannelRoomIconContext';
import OmnichannelRoomIcon from '../lib/OmnichannelRoomIcon';

let icons = Array.from(OmnichannelRoomIcon.icons.values());

export const OmnichannelRoomIconProvider: FC = ({ children }) => {
const svgIcons = useSubscription(
useMemo(
() => ({
getCurrentValue: (): string[] => Array.from(OmnichannelRoomIcon.icons.values()),
subscribe: (callback): (() => void) => OmnichannelRoomIcon.on('change', callback),
}),
const svgIcons = useSyncExternalStore(
useCallback(
(callback): (() => void) =>
OmnichannelRoomIcon.on('change', () => {
icons = Array.from(OmnichannelRoomIcon.icons.values());
callback();
}),
[],
),
(): string[] => icons,
);

return (
<OmnichannelRoomIconContext.Provider
value={useMemo(
() => ({
queryIcon: (app: string, iconName: string): Subscription<AsyncState<string>> => ({
getCurrentValue: (): AsyncState<string> => {
queryIcon: (
app: string,
iconName: string,
): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => AsyncState<string>] => [
(callback): (() => void) => OmnichannelRoomIcon.on(`${app}-${iconName}`, callback),
(): AsyncState<string> => {
const icon = OmnichannelRoomIcon.get(app, iconName);

if (!icon) {
Expand All @@ -39,8 +48,7 @@ export const OmnichannelRoomIconProvider: FC = ({ children }) => {
error: undefined,
};
},
subscribe: (callback): (() => void) => OmnichannelRoomIcon.on(`${app}-${iconName}`, callback),
}),
],
}),
[],
)}
Expand Down
25 changes: 11 additions & 14 deletions apps/meteor/client/contexts/CallContext.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { IVoipRoom } from '@rocket.chat/core-typings';
import { ICallerInfo, VoIpCallerInfo } from '@rocket.chat/core-typings';
import { createContext, useContext, useMemo } from 'react';
import { useSubscription } from 'use-subscription';
import { createContext, useCallback, useContext } from 'react';
import { useSyncExternalStore } from 'use-sync-external-store/shim';

import { VoIPUser } from '../lib/voip/VoIPUser';

Expand Down Expand Up @@ -89,20 +89,17 @@ export const useCallerInfo = (): VoIpCallerInfo => {
throw new Error('useCallerInfo only if Calls are enabled and ready');
}
const { voipClient } = context;
const subscription = useMemo(
() => ({
getCurrentValue: (): VoIpCallerInfo => voipClient.callerInfo,
subscribe: (callback: () => void): (() => void) => {
voipClient.on('stateChanged', callback);

return (): void => {
voipClient.off('stateChanged', callback);
};
},
}),
const subscribe = useCallback(
(callback: () => void): (() => void) => {
voipClient.on('stateChanged', callback);

return (): void => {
voipClient.off('stateChanged', callback);
};
},
[voipClient],
);
return useSubscription(subscription);
return useSyncExternalStore(subscribe, (): VoIpCallerInfo => voipClient.callerInfo);
};

export const useCallCreateRoom = (): CallContextReady['createRoom'] => {
Expand Down
25 changes: 12 additions & 13 deletions apps/meteor/client/hooks/usePresence.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useMemo } from 'react';
import { useSubscription } from 'use-subscription';
import { useCallback } from 'react';
import { useSyncExternalStore } from 'use-sync-external-store/shim';

import { Presence, UserPresence } from '../lib/presence';

Expand All @@ -13,18 +13,17 @@ type Presence = 'online' | 'offline' | 'busy' | 'away' | 'loading';
* @public
*/
export const usePresence = (uid: string | undefined): UserPresence | undefined => {
const subscription = useMemo(
() => ({
getCurrentValue: (): UserPresence | undefined => (uid ? Presence.store.get(uid) : undefined),
subscribe: (callback: any): any => {
uid && Presence.listen(uid, callback);
return (): void => {
uid && Presence.stop(uid, callback);
};
},
}),
const subscribe = useCallback(
(callback: any): any => {
uid && Presence.listen(uid, callback);
return (): void => {
uid && Presence.stop(uid, callback);
};
},
[uid],
);

return useSubscription(subscription);
const getSnapshot = (): UserPresence | undefined => (uid ? Presence.store.get(uid) : undefined);

return useSyncExternalStore(subscribe, getSnapshot);
};
16 changes: 8 additions & 8 deletions apps/meteor/client/hooks/useReactiveValue.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Tracker } from 'meteor/tracker';
import { useMemo } from 'react';
import { Subscription, Unsubscribe, useSubscription } from 'use-subscription';
import { useSyncExternalStore } from 'use-sync-external-store/shim';

export const useReactiveValue = <T>(computeCurrentValue: () => T): T => {
const subscription: Subscription<T> = useMemo(() => {
const callbacks = new Set<Unsubscribe>();
const [subscribe, getSnapshot] = useMemo(() => {
const callbacks = new Set<() => void>();

let currentValue: T;

Expand All @@ -15,9 +15,8 @@ export const useReactiveValue = <T>(computeCurrentValue: () => T): T => {
});
});

return {
getCurrentValue: (): T => currentValue,
subscribe: (callback): Unsubscribe => {
return [
(callback: () => void): (() => void) => {
callbacks.add(callback);

return (): void => {
Expand All @@ -28,8 +27,9 @@ export const useReactiveValue = <T>(computeCurrentValue: () => T): T => {
}
};
},
};
(): T => currentValue,
];
}, [computeCurrentValue]);

return useSubscription(subscription);
return useSyncExternalStore(subscribe, getSnapshot);
};
25 changes: 12 additions & 13 deletions apps/meteor/client/hooks/useUserData.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useMemo } from 'react';
import { useSubscription } from 'use-subscription';
import { useCallback } from 'react';
import { useSyncExternalStore } from 'use-sync-external-store/shim';

import { UserPresence, Presence } from '../lib/presence';

Expand All @@ -11,18 +11,17 @@ import { UserPresence, Presence } from '../lib/presence';
* @public
*/
export const useUserData = (uid: string): UserPresence | undefined => {
const subscription = useMemo(
() => ({
getCurrentValue: (): UserPresence | undefined => Presence.store.get(uid),
subscribe: (callback: any): any => {
Presence.listen(uid, callback);
return (): void => {
Presence.stop(uid, callback);
};
},
}),
const subscription = useCallback(
(callback: () => void): (() => void) => {
Presence.listen(uid, callback);
return (): void => {
Presence.stop(uid, callback);
};
},
[uid],
);

return useSubscription(subscription);
const getSnapshot = (): UserPresence | undefined => Presence.store.get(uid);

return useSyncExternalStore(subscription, getSnapshot);
};
44 changes: 17 additions & 27 deletions apps/meteor/client/lib/RoomManager.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { IRoom } from '@rocket.chat/core-typings';
import { Emitter } from '@rocket.chat/emitter';
import { useUserId, useUserRoom, useUserSubscription } from '@rocket.chat/ui-contexts';
import { useEffect, useMemo } from 'react';
import { useSubscription, Subscription, Unsubscribe } from 'use-subscription';
import { useCallback, useEffect } from 'react';
import { useSyncExternalStore } from 'use-sync-external-store/shim';

import { RoomHistoryManager } from '../../app/ui-utils/client/lib/RoomHistoryManager';
import { useAsyncState } from '../hooks/useAsyncState';
Expand Down Expand Up @@ -129,19 +129,15 @@ export const RoomManager = new (class RoomManager extends Emitter<{
}
})();

const subscribeVisitedRooms: Subscription<IRoom['_id'][]> = {
getCurrentValue: () => RoomManager.visitedRooms(),
subscribe(callback) {
return RoomManager.on('changed', callback);
},
};
const subscribeVisitedRooms = [
(callback: () => void): (() => void) => RoomManager.on('changed', callback),
(): IRoom['_id'][] => RoomManager.visitedRooms(),
] as const;

const subscribeOpenedRoom: Subscription<IRoom['_id'] | undefined> = {
getCurrentValue: () => RoomManager.opened,
subscribe(callback) {
return RoomManager.on('opened', callback);
},
};
const subscribeOpenedRoom = [
(callback: () => void): (() => void) => RoomManager.on('opened', callback),
(): IRoom['_id'] | undefined => RoomManager.opened,
] as const;

const fields = {};

Expand All @@ -165,25 +161,19 @@ export const useHandleRoom = <T extends IRoom>(rid: IRoom['_id']): AsyncState<T>
return state;
};

export const useVisitedRooms = (): IRoom['_id'][] => useSubscription(subscribeVisitedRooms);
export const useVisitedRooms = (): IRoom['_id'][] => useSyncExternalStore(...subscribeVisitedRooms);

export const useOpenedRoom = (): IRoom['_id'] | undefined => useSubscription(subscribeOpenedRoom);
export const useOpenedRoom = (): IRoom['_id'] | undefined => useSyncExternalStore(...subscribeOpenedRoom);

export const useRoomStore = (rid: IRoom['_id']): RoomStore => {
const subscribeStore: Subscription<RoomStore | undefined> = useMemo(
() => ({
getCurrentValue: (): RoomStore | undefined => RoomManager.getStore(rid),
subscribe(callback): Unsubscribe {
return RoomManager.on('changed', callback);
},
}),
[rid],
);

const store = useSubscription(subscribeStore);
const subscribe = useCallback((callback: () => void): (() => void) => RoomManager.on('changed', callback), []);
const getSnapshot = (): RoomStore | undefined => RoomManager.getStore(rid);

const store = useSyncExternalStore(subscribe, getSnapshot);

if (!store) {
throw new Error('Something wrong');
}

return store;
};
7 changes: 3 additions & 4 deletions apps/meteor/client/lib/appLayout.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { Emitter } from '@rocket.chat/emitter';
import { ReactElement } from 'react';
import { Subscription, Unsubscribe } from 'use-subscription';

type AppLayoutDescriptor = ReactElement | null;

class AppLayoutSubscription extends Emitter<{ update: void }> implements Subscription<AppLayoutDescriptor> {
class AppLayoutSubscription extends Emitter<{ update: void }> {
private descriptor: AppLayoutDescriptor = null;

getCurrentValue = (): AppLayoutDescriptor => this.descriptor;
getSnapshot = (): AppLayoutDescriptor => this.descriptor;

subscribe = (callback: () => void): Unsubscribe => this.on('update', callback);
subscribe = (onStoreChange: () => void): (() => void) => this.on('update', onStoreChange);

setCurrentValue(descriptor: AppLayoutDescriptor): void {
this.descriptor = descriptor;
Expand Down
Loading

0 comments on commit 0862751

Please sign in to comment.