Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/hooks/useDeepCompareEffect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useEffect, useRef } from 'react';
import isEqual from 'lodash/isEqual';

function useDeepCompareMemoize<T>(value: T): T {
const ref = useRef<T>(value);

if (!isEqual(value, ref.current)) {
ref.current = value;
}

return ref.current;
}

/**
* Custom hook that works like useEffect but performs a deep comparison of dependencies
* instead of reference equality. This is useful when dealing with complex objects or arrays
* in dependencies that could trigger unnecessary re-renders.
*
* Inspired by https://github.com/kentcdodds/use-deep-compare-effect
*
* @param callback Effect callback that can either return nothing (void) or return a cleanup function (() => void).
* @param dependencies Array of dependencies to be deeply compared
*/
function useDeepCompareEffect(
callback: () => void | (() => void),
dependencies: any[],
) {
useEffect(callback, dependencies.map(useDeepCompareMemoize));
}

export default useDeepCompareEffect;
25 changes: 18 additions & 7 deletions src/hooks/useStore.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { useContext, useRef, useCallback } from 'react';
import { useContext, useRef, useCallback, useMemo } from 'react';
import { useSyncExternalStore } from 'use-sync-external-store/shim';
import { type Store } from '../utils/storeManager';

type StoreSelector<T, U> = (state: T) => U;

function hasStateChanged<T>(prevState: T, updates: Partial<T>): boolean {
return Object.entries(updates).some(([key, value]) => {
return prevState[key as keyof T] !== value;
});
}

/**
* A generic hook for accessing and updating store state
* @param StoreContext
Expand All @@ -19,6 +25,7 @@ export function useStore<T, U>(
if (!store) {
throw new Error('useStore must be used within a StoreProvider');
}

// Ensure the stability of the selector function using useRef
const selectorRef = useRef(selector);
selectorRef.current = selector;
Expand All @@ -36,14 +43,18 @@ export function useStore<T, U>(
);

const updateState = useCallback((updates: Partial<T>) => {
store.setState((prevState) => ({
...prevState,
...updates,
}));
const currentState = store.getState();

if (hasStateChanged(currentState, updates)) {
store.setState((prevState) => ({
...prevState,
...updates,
}));
}
}, [store]);

return {
return useMemo(() => ({
state,
updateState,
};
}), [state, updateState]);
}
3 changes: 2 additions & 1 deletion src/modules/GroupChannel/context/GroupChannelProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import type {
GroupChannelState,
} from './types';
import useSendbird from '../../../lib/Sendbird/context/hooks/useSendbird';
import useDeepCompareEffect from '../../../hooks/useDeepCompareEffect';

const initialState = {
currentChannel: null,
Expand Down Expand Up @@ -280,7 +281,7 @@ const GroupChannelManager :React.FC<React.PropsWithChildren<GroupChannelProvider
isScrollBottomReached,
]);

useEffect(() => {
useDeepCompareEffect(() => {
updateState({
// Channel state
channelUrl,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { DependencyList, useLayoutEffect, useRef, useState } from 'react';
import pubSubFactory from '../../../../lib/pubSub';
import { useGroupChannel } from './useGroupChannel';

/**
* You can pass the resolve function to scrollPubSub, if you want to catch when the scroll is finished.
Expand Down Expand Up @@ -30,7 +31,10 @@ export function useMessageListScroll(behavior: 'smooth' | 'auto', deps: Dependen
const scrollDistanceFromBottomRef = useRef(0);

const [scrollPubSub] = useState(() => pubSubFactory<ScrollTopics, ScrollTopicUnion>({ publishSynchronous: true }));
const [isScrollBottomReached, setIsScrollBottomReached] = useState(true);
const {
state: { isScrollBottomReached },
actions: { setIsScrollBottomReached },
} = useGroupChannel();
Comment on lines -33 to +37
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


// SideEffect: Reset scroll state
useLayoutEffect(() => {
Expand Down
21 changes: 18 additions & 3 deletions src/utils/storeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,26 @@ export type Store<T> = {
export function createStore<T extends object>(initialState: T): Store<T> {
let state = { ...initialState };
const listeners = new Set<() => void>();
let isUpdating = false;

const setState = (partial: Partial<T> | ((state: T) => Partial<T>)) => {
const nextState = typeof partial === 'function' ? partial(state) : partial;
state = { ...state, ...nextState };
listeners.forEach((listener) => listener());
// Prevent nested updates
if (isUpdating) return;

try {
isUpdating = true;
const nextState = typeof partial === 'function' ? partial(state) : partial;
const hasChanged = Object.entries(nextState).some(
([key, value]) => state[key] !== value,
);

if (hasChanged) {
state = { ...state, ...nextState };
listeners.forEach((listener) => listener());
}
} finally {
isUpdating = false;
}
};

return {
Expand Down