Skip to content

Commit 22f5c1b

Browse files
authored
[CLNP-5043] Migrate MessageSearchProvider (#1216)
## Overview This PR refactors the MessageSearch state management system, introducing a custom store solution and several hooks for improved performance, maintainability, and type safety. ## New Files - `src/utils/storeManager.ts`: Implements a custom store creation utility - `src/contexts/_MessageSearchContext.tsx`: Provides the MessageSearch context and store \w new state mgmt logic. It's just temporal name. - `src/hooks/useStore.ts`: A generic hook for accessing and updating store state - `src/hooks/useMessageSearchStore.ts`: A specialized hook for MessageSearch state management - `src/components/MessageSearchManager.tsx`: Manages MessageSearch state and side effects - `src/hooks/useMessageSearchActions.ts`: Manages action handlers ## Updated Hooks - `useSetChannel`: Now uses `useMessageSearchStore` directly - `useSearchStringEffect`: Refactored to work with the new store - `useGetSearchMessages`: Updated to utilize the new state management system - `useScrollCallback`: Adapted to work with the custom store ## Key Changes 1. Introduced a custom store solution to replace the previous reducer-based state management. 2. Implemented `useStore` hook for type-safe and efficient state access and updates. 3. Created `MessageSearchManager` to centralize state management logic. 4. Refactored existing hooks to work with the new store system. 5. Improved type safety throughout the MessageSearch module.
1 parent e087c5c commit 22f5c1b

File tree

11 files changed

+424
-241
lines changed

11 files changed

+424
-241
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@
142142
"ts-pattern": "^4.2.2",
143143
"typedoc": "^0.25.13",
144144
"typescript": "^5.4.5",
145+
"use-sync-external-store": "^1.2.2",
145146
"vite": "^5.1.5",
146147
"vite-plugin-svgr": "^4.2.0"
147148
},

src/hooks/useStore.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { useContext, useRef, useCallback } from 'react';
2+
import { useSyncExternalStore } from 'use-sync-external-store/shim';
3+
import { type Store } from '../utils/storeManager';
4+
5+
type StoreSelector<T, U> = (state: T) => U;
6+
7+
/**
8+
* A generic hook for accessing and updating store state
9+
* @param StoreContext
10+
* @param selector
11+
* @param initialState
12+
*/
13+
export function useStore<T, U>(
14+
StoreContext: React.Context<Store<T> | null>,
15+
selector: StoreSelector<T, U>,
16+
initialState: T,
17+
) {
18+
const store = useContext(StoreContext);
19+
if (!store) {
20+
throw new Error('useStore must be used within a StoreProvider');
21+
}
22+
// Ensure the stability of the selector function using useRef
23+
const selectorRef = useRef(selector);
24+
selectorRef.current = selector;
25+
/**
26+
* useSyncExternalStore - a new API introduced in React18
27+
* but we're using a shim for now since it's only available in 18 >= version.
28+
* useSyncExternalStore simply tracks changes in an external store that is not dependent on React
29+
* through useState and useEffect
30+
* and helps with re-rendering and state sync through the setter of useState
31+
*/
32+
const state = useSyncExternalStore(
33+
store.subscribe,
34+
() => selectorRef.current(store.getState()),
35+
() => selectorRef.current(initialState),
36+
);
37+
38+
const updateState = useCallback((updates: Partial<T>) => {
39+
store.setState((prevState) => ({
40+
...prevState,
41+
...updates,
42+
}));
43+
}, [store]);
44+
45+
return {
46+
state,
47+
updateState,
48+
};
49+
}

src/modules/MessageSearch/components/MessageSearchUI/index.tsx

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { FileMessage, UserMessage } from '@sendbird/chat/message';
33
import './index.scss';
44

55
import { LocalizationContext } from '../../../../lib/LocalizationContext';
6-
import { useMessageSearchContext } from '../../context/MessageSearchProvider';
6+
import useMessageSearch from '../../context/hooks/useMessageSearch';
77

88
import MessageSearchItem from '../../../../ui/MessageSearchItem';
99
import PlaceHolder, { PlaceHolderTypes } from '../../../../ui/PlaceHolder';
@@ -34,28 +34,27 @@ export const MessageSearchUI: React.FC<MessageSearchUIProps> = ({
3434
renderSearchItem,
3535
}: MessageSearchUIProps) => {
3636
const {
37-
isInvalid,
38-
searchString,
39-
requestString,
40-
currentChannel,
41-
retryCount,
42-
setRetryCount,
43-
loading,
44-
scrollRef,
45-
hasMoreResult,
46-
onScroll,
47-
allMessages,
48-
onResultClick,
49-
selectedMessageId,
50-
setSelectedMessageId,
51-
} = useMessageSearchContext();
37+
state: {
38+
isInvalid,
39+
searchString,
40+
requestString,
41+
currentChannel,
42+
loading,
43+
scrollRef,
44+
hasMoreResult,
45+
onScroll,
46+
allMessages,
47+
onResultClick,
48+
selectedMessageId,
49+
},
50+
actions: {
51+
setSelectedMessageId,
52+
handleRetryToConnect,
53+
},
54+
} = useMessageSearch();
5255

5356
const { stringSet } = useContext(LocalizationContext);
5457

55-
const handleRetryToConnect = () => {
56-
setRetryCount(retryCount + 1);
57-
};
58-
5958
const handleOnScroll = (e) => {
6059
const scrollElement = e.target;
6160
const {
Lines changed: 127 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,19 @@
1-
import React, {
2-
useRef,
3-
useState,
4-
useReducer,
5-
} from 'react';
6-
import { SendbirdError } from '@sendbird/chat';
7-
import type { MessageSearchQuery } from '@sendbird/chat/message';
1+
import React, { createContext, useRef, useContext, useCallback, useEffect } from 'react';
82
import type { GroupChannel } from '@sendbird/chat/groupChannel';
3+
import { MessageSearchQuery } from '@sendbird/chat/message';
4+
import { ClientSentMessages } from '../../../types';
5+
import { SendbirdError } from '@sendbird/chat';
96
import type { MessageSearchQueryParams } from '@sendbird/chat/lib/__definition';
107

118
import useSendbirdStateContext from '../../../hooks/useSendbirdStateContext';
12-
import { ClientSentMessages } from '../../../types';
13-
14-
import messageSearchReducer from './dux/reducers';
15-
import messageSearchInitialState, { State as MessageSearchReducerState } from './dux/initialState';
169

1710
import useSetChannel from './hooks/useSetChannel';
1811
import useGetSearchMessages from './hooks/useGetSearchedMessages';
19-
import useScrollCallback, {
20-
CallbackReturn as UseScrollCallbackType,
21-
} from './hooks/useScrollCallback';
12+
import useScrollCallback from './hooks/useScrollCallback';
2213
import useSearchStringEffect from './hooks/useSearchStringEffect';
2314
import { CoreMessageType } from '../../../utils';
15+
import { createStore } from '../../../utils/storeManager';
16+
import { useStore } from '../../../hooks/useStore';
2417

2518
export interface MessageSearchProviderProps {
2619
channelUrl: string;
@@ -31,130 +24,160 @@ export interface MessageSearchProviderProps {
3124
onResultClick?(message: ClientSentMessages): void;
3225
}
3326

34-
interface MessageSearchProviderInterface extends MessageSearchProviderProps {
35-
requestString?: string;
36-
retryCount: number;
37-
setRetryCount: React.Dispatch<React.SetStateAction<number>>;
38-
selectedMessageId: number;
39-
setSelectedMessageId: React.Dispatch<React.SetStateAction<number>>;
40-
messageSearchDispatcher: (props: { type: string, payload: any }) => void;
41-
scrollRef: React.RefObject<HTMLDivElement>;
42-
allMessages: MessageSearchReducerState['allMessages'];
27+
export interface MessageSearchState extends MessageSearchProviderProps {
28+
channelUrl: string;
29+
allMessages: ClientSentMessages[];
4330
loading: boolean;
4431
isInvalid: boolean;
45-
currentChannel: GroupChannel;
46-
currentMessageSearchQuery: MessageSearchQuery;
32+
initialized: boolean;
33+
currentChannel: GroupChannel | null;
34+
currentMessageSearchQuery: MessageSearchQuery | null;
4735
hasMoreResult: boolean;
48-
onScroll: UseScrollCallbackType;
49-
handleRetryToConnect: () => void;
50-
handleOnScroll: (e: React.BaseSyntheticEvent) => void;
36+
retryCount: number;
37+
selectedMessageId: number | null;
38+
requestString: string;
39+
onScroll?: ReturnType<typeof useScrollCallback>;
40+
handleOnScroll?: (e: React.BaseSyntheticEvent) => void;
41+
scrollRef?: React.RefObject<HTMLDivElement>;
5142
}
5243

53-
const MessageSearchContext = React.createContext<MessageSearchProviderInterface | null>(null);
54-
55-
const MessageSearchProvider: React.FC<MessageSearchProviderProps> = (props: MessageSearchProviderProps) => {
56-
const {
57-
// message search props
58-
channelUrl,
59-
searchString,
60-
messageSearchQuery,
61-
onResultLoaded,
62-
onResultClick,
63-
} = props;
64-
65-
const globalState = useSendbirdStateContext();
66-
67-
// hook variables
68-
const [retryCount, setRetryCount] = useState(0); // this is a trigger flag for activating useGetSearchMessages
69-
const [selectedMessageId, setSelectedMessageId] = useState(0);
70-
const [messageSearchStore, messageSearchDispatcher] = useReducer(messageSearchReducer, messageSearchInitialState);
71-
const {
72-
allMessages,
73-
loading,
74-
isInvalid,
75-
currentChannel,
76-
currentMessageSearchQuery,
77-
hasMoreResult,
78-
} = messageSearchStore;
79-
80-
const logger = globalState?.config?.logger;
81-
const sdk = globalState?.stores?.sdkStore?.sdk;
82-
const sdkInit = globalState?.stores?.sdkStore?.initialized;
83-
const scrollRef = useRef<HTMLDivElement>(null);
84-
const handleOnScroll = (e: React.BaseSyntheticEvent) => {
85-
const scrollElement = e.target as HTMLDivElement;
86-
const {
87-
scrollTop,
88-
scrollHeight,
89-
clientHeight,
90-
} = scrollElement;
44+
const initialState: MessageSearchState = {
45+
channelUrl: '',
46+
allMessages: [],
47+
loading: false,
48+
isInvalid: false,
49+
initialized: false,
50+
currentChannel: null,
51+
currentMessageSearchQuery: null,
52+
messageSearchQuery: null,
53+
hasMoreResult: false,
54+
retryCount: 0,
55+
selectedMessageId: null,
56+
searchString: '',
57+
requestString: '',
58+
};
9159

92-
if (!hasMoreResult) {
93-
return;
94-
}
95-
if (scrollTop + clientHeight >= scrollHeight) {
96-
onScroll(() => {
97-
// after load more searched messages
98-
});
99-
}
100-
};
60+
export const MessageSearchContext = createContext<ReturnType<typeof createStore<MessageSearchState>> | null>(null);
61+
62+
const MessageSearchManager: React.FC<MessageSearchProviderProps> = ({
63+
channelUrl,
64+
searchString,
65+
messageSearchQuery,
66+
onResultLoaded,
67+
onResultClick,
68+
}) => {
69+
const { state, updateState } = useMessageSearchStore();
70+
const { config, stores } = useSendbirdStateContext();
71+
const sdk = stores?.sdkStore?.sdk;
72+
const sdkInit = stores?.sdkStore?.initialized;
73+
const { logger } = config;
74+
const scrollRef = useRef<HTMLDivElement>(null);
10175

10276
useSetChannel(
10377
{ channelUrl, sdkInit },
104-
{ sdk, logger, messageSearchDispatcher },
78+
{ sdk, logger },
10579
);
10680

107-
const requestString = useSearchStringEffect({ searchString: searchString ?? '' }, { messageSearchDispatcher });
81+
const requestString = useSearchStringEffect(
82+
{ searchString: searchString ?? '' },
83+
);
10884

10985
useGetSearchMessages(
110-
{ currentChannel, channelUrl, requestString, messageSearchQuery, onResultLoaded, retryCount },
111-
{ sdk, logger, messageSearchDispatcher },
86+
{
87+
currentChannel: state.currentChannel,
88+
channelUrl,
89+
requestString,
90+
messageSearchQuery,
91+
onResultLoaded,
92+
},
93+
{ sdk, logger },
11294
);
11395

11496
const onScroll = useScrollCallback(
115-
{ currentMessageSearchQuery, hasMoreResult, onResultLoaded },
116-
{ logger, messageSearchDispatcher },
97+
{ onResultLoaded },
98+
{ logger },
11799
);
118100

119-
const handleRetryToConnect = () => {
120-
setRetryCount(retryCount + 1);
121-
};
122-
return (
123-
<MessageSearchContext.Provider value={{
101+
const handleOnScroll = useCallback((e: React.BaseSyntheticEvent) => {
102+
const scrollElement = e.target as HTMLDivElement;
103+
const { scrollTop, scrollHeight, clientHeight } = scrollElement;
104+
105+
if (!state.hasMoreResult) {
106+
return;
107+
}
108+
if (scrollTop + clientHeight >= scrollHeight) {
109+
onScroll(() => {
110+
// after load more searched messages
111+
});
112+
}
113+
}, [state.hasMoreResult, onScroll]);
114+
115+
useEffect(() => {
116+
updateState({
124117
channelUrl,
125118
searchString,
126-
requestString,
127119
messageSearchQuery,
128-
onResultLoaded,
129120
onResultClick,
130-
retryCount,
131-
setRetryCount,
132-
selectedMessageId,
133-
setSelectedMessageId,
134-
messageSearchDispatcher,
135-
allMessages,
136-
loading,
137-
isInvalid,
138-
currentChannel,
139-
currentMessageSearchQuery,
140-
hasMoreResult,
141121
onScroll,
142-
scrollRef,
143-
handleRetryToConnect,
144122
handleOnScroll,
145-
}}>
146-
{props?.children}
123+
scrollRef,
124+
requestString,
125+
});
126+
}, [channelUrl, searchString, messageSearchQuery, onResultClick, updateState, requestString]);
127+
128+
return null;
129+
};
130+
131+
const createMessageSearchStore = () => createStore(initialState);
132+
const InternalMessageSearchProvider: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
133+
const storeRef = useRef(createMessageSearchStore());
134+
135+
return (
136+
<MessageSearchContext.Provider value={storeRef.current}>
137+
{children}
147138
</MessageSearchContext.Provider>
148139
);
149140
};
150141

142+
const MessageSearchProvider: React.FC<MessageSearchProviderProps> = ({
143+
children,
144+
channelUrl,
145+
searchString,
146+
messageSearchQuery,
147+
onResultLoaded,
148+
onResultClick,
149+
}) => {
150+
151+
return (
152+
<InternalMessageSearchProvider>
153+
<MessageSearchManager
154+
channelUrl={channelUrl}
155+
searchString={searchString}
156+
messageSearchQuery={messageSearchQuery}
157+
onResultLoaded={onResultLoaded}
158+
onResultClick={onResultClick}
159+
/>
160+
{children}
161+
</InternalMessageSearchProvider>
162+
);
163+
};
164+
151165
const useMessageSearchContext = () => {
152-
const context = React.useContext(MessageSearchContext);
166+
const context = useContext(MessageSearchContext);
153167
if (!context) throw new Error('MessageSearchContext not found. Use within the MessageSearch module.');
154168
return context;
155169
};
156170

157171
export {
158172
MessageSearchProvider,
159173
useMessageSearchContext,
174+
MessageSearchManager,
175+
};
176+
177+
/**
178+
* A specialized hook for MessageSearch state management
179+
* @returns {ReturnType<typeof createStore<MessageSearchState>>}
180+
*/
181+
const useMessageSearchStore = () => {
182+
return useStore(MessageSearchContext, state => state, initialState);
160183
};

0 commit comments

Comments
 (0)