diff --git a/package/src/components/AutoCompleteInput/AutoCompleteInput.tsx b/package/src/components/AutoCompleteInput/AutoCompleteInput.tsx index df602a0034..f0652f0d75 100644 --- a/package/src/components/AutoCompleteInput/AutoCompleteInput.tsx +++ b/package/src/components/AutoCompleteInput/AutoCompleteInput.tsx @@ -3,10 +3,6 @@ import { I18nManager, StyleSheet, TextInput, TextInputProps } from 'react-native import throttle from 'lodash/throttle'; -import { - ChannelContextValue, - useChannelContext, -} from '../../contexts/channelContext/ChannelContext'; import { MessageInputContextValue, useMessageInputContext, @@ -53,22 +49,22 @@ const isCommand = (text: string) => text[0] === '/' && text.split(' ').length <= type AutoCompleteInputPropsWithContext< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Pick, 'giphyEnabled'> & - Pick< - MessageInputContextValue, - | 'additionalTextInputProps' - | 'autoCompleteSuggestionsLimit' - | 'giphyActive' - | 'maxMessageLength' - | 'mentionAllAppUsersEnabled' - | 'mentionAllAppUsersQuery' - | 'numberOfLines' - | 'onChange' - | 'setGiphyActive' - | 'setInputBoxRef' - | 'text' - | 'triggerSettings' - > & +> = Pick< + MessageInputContextValue, + | 'additionalTextInputProps' + | 'autoCompleteSuggestionsLimit' + | 'giphyActive' + | 'giphyEnabled' + | 'maxMessageLength' + | 'mentionAllAppUsersEnabled' + | 'mentionAllAppUsersQuery' + | 'numberOfLines' + | 'onChange' + | 'setGiphyActive' + | 'setInputBoxRef' + | 'text' + | 'triggerSettings' +> & Pick< SuggestionsContextValue, 'closeSuggestions' | 'openSuggestions' | 'updateSuggestions' @@ -484,8 +480,8 @@ export const AutoCompleteInput = < >( props: AutoCompleteInputProps, ) => { - const { giphyEnabled } = useChannelContext(); const { + giphyEnabled, additionalTextInputProps, autoCompleteSuggestionsLimit, giphyActive, diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index 1bc8ad5a7f..ae5ca45407 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -781,9 +781,11 @@ const ChannelWithContext = < } // If the event is typing.start or typing.stop, set the typing state - const isTypingEvent = event.type === 'typing.start' || event.type === 'typing.stop'; - if (isTypingEvent) { - setTyping(channel); + if (event.type === 'typing.start' || event.type === 'typing.stop') { + if (event.user?.id !== client.userID) { + setTyping(channel); + } + return; } else { if (thread?.id) { const updatedThreadMessages = @@ -1758,6 +1760,11 @@ const ChannelWithContext = < const sendMessageRef = useRef['sendMessage']>(sendMessage); sendMessageRef.current = sendMessage; + const sendMessageStable = useCallback< + InputMessageInputContextValue['sendMessage'] + >((...args) => { + return sendMessageRef.current(...args); + }, []); const inputMessageInputContext = useCreateInputMessageInputContext({ additionalTextInputProps, @@ -1811,7 +1818,7 @@ const ChannelWithContext = < quotedMessage, SendButton, sendImageAsync, - sendMessage: (...args) => sendMessageRef.current(...args), + sendMessage: sendMessageStable, SendMessageDisallowedIndicator, setInputRef, setQuotedMessageState, @@ -1944,11 +1951,13 @@ const ChannelWithContext = < VideoThumbnail, }); - const suggestionsContext = { - AutoCompleteSuggestionHeader, - AutoCompleteSuggestionItem, - AutoCompleteSuggestionList, - }; + const suggestionsContext = useMemo(() => { + return { + AutoCompleteSuggestionHeader, + AutoCompleteSuggestionItem, + AutoCompleteSuggestionList, + }; + }, [AutoCompleteSuggestionHeader, AutoCompleteSuggestionItem, AutoCompleteSuggestionList]); const threadContext = useCreateThreadContext({ allowThreadMessagesInChannel, diff --git a/package/src/components/Message/MessageSimple/MessageSimple.tsx b/package/src/components/Message/MessageSimple/MessageSimple.tsx index fe476b53dd..693e18a762 100644 --- a/package/src/components/Message/MessageSimple/MessageSimple.tsx +++ b/package/src/components/Message/MessageSimple/MessageSimple.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { Dimensions, LayoutChangeEvent, StyleSheet, View } from 'react-native'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; @@ -12,6 +12,8 @@ import Animated, { withSpring, } from 'react-native-reanimated'; +const AnimatedWrapper = Animated.createAnimatedComponent(View); + import { MessageContextValue, useMessageContext, @@ -205,77 +207,104 @@ const MessageSimpleWithContext = < const translateX = useSharedValue(0); const touchStart = useSharedValue<{ x: number; y: number } | null>(null); + const isSwiping = useSharedValue(false); + const [isBeingSwiped, setIsBeingSwiped] = useState(false); - const onSwipeToReply = () => { + const onSwipeToReply = useCallback(() => { clearQuotedMessageState(); setQuotedMessageState(message); - }; + }, [clearQuotedMessageState, message, setQuotedMessageState]); const THRESHOLD = 25; const triggerHaptic = NativeHandlers.triggerHaptic; - const swipeGesture = Gesture.Pan() - .hitSlop(messageSwipeToReplyHitSlop) - .onBegin((event) => { - touchStart.value = { x: event.x, y: event.y }; - }) - .onTouchesMove((event, state) => { - if (!touchStart.value || !event.changedTouches.length) { - state.fail(); - return; - } - - const xDiff = Math.abs(event.changedTouches[0].x - touchStart.value.x); - const yDiff = Math.abs(event.changedTouches[0].y - touchStart.value.y); - const isHorizontalPanning = xDiff > yDiff; - - if (isHorizontalPanning) { - state.activate(); - } else { - state.fail(); - } - }) - .onStart(() => { - translateX.value = 0; - }) - .onChange(({ translationX }) => { - if (translationX > 0) { - translateX.value = translationX; - } - }) - .onEnd(() => { - if (translateX.value >= THRESHOLD) { - runOnJS(onSwipeToReply)(); - if (triggerHaptic) { - runOnJS(triggerHaptic)('impactMedium'); - } - } - translateX.value = withSpring(0, { - dampingRatio: 1, - duration: 500, - overshootClamping: true, - stiffness: 1, - }); - }); - - const messageBubbleAnimatedStyle = useAnimatedStyle(() => ({ - transform: [{ translateX: translateX.value }], - })); - - const swipeContentAnimatedStyle = useAnimatedStyle(() => ({ - opacity: interpolate(translateX.value, [0, THRESHOLD], [0, 1]), - transform: [ - { - translateX: interpolate( - translateX.value, - [0, THRESHOLD], - [-THRESHOLD, 0], - Extrapolation.CLAMP, - ), - }, - ], - })); + const swipeGesture = useMemo( + () => + Gesture.Pan() + .hitSlop(messageSwipeToReplyHitSlop) + .onBegin((event) => { + touchStart.value = { x: event.x, y: event.y }; + }) + .onTouchesMove((event, state) => { + if (!touchStart.value || !event.changedTouches.length) { + state.fail(); + return; + } + + const xDiff = Math.abs(event.changedTouches[0].x - touchStart.value.x); + const yDiff = Math.abs(event.changedTouches[0].y - touchStart.value.y); + const isHorizontalPanning = xDiff > yDiff; + + if (isHorizontalPanning) { + state.activate(); + isSwiping.value = true; + runOnJS(setIsBeingSwiped)(true); + } else { + state.fail(); + } + }) + .onStart(() => { + translateX.value = 0; + }) + .onChange(({ translationX }) => { + if (translationX > 0) { + translateX.value = translationX; + } + }) + .onEnd(() => { + if (translateX.value >= THRESHOLD) { + runOnJS(onSwipeToReply)(); + if (triggerHaptic) { + runOnJS(triggerHaptic)('impactMedium'); + } + } + translateX.value = withSpring( + 0, + { + dampingRatio: 1, + duration: 500, + overshootClamping: true, + stiffness: 1, + }, + () => { + isSwiping.value = false; + runOnJS(setIsBeingSwiped)(false); + }, + ); + }), + [isSwiping, messageSwipeToReplyHitSlop, onSwipeToReply, touchStart, translateX, triggerHaptic], + ); + + const messageBubbleAnimatedStyle = useAnimatedStyle( + () => + isSwiping.value + ? { + transform: [{ translateX: translateX.value }], + } + : {}, + [], + ); + + const swipeContentAnimatedStyle = useAnimatedStyle( + () => + isSwiping.value + ? { + opacity: interpolate(translateX.value, [0, THRESHOLD], [0, 1]), + transform: [ + { + translateX: interpolate( + translateX.value, + [0, THRESHOLD], + [-THRESHOLD, 0], + Extrapolation.CLAMP, + ), + }, + ], + } + : {}, + [], + ); const renderMessageBubble = useMemo( () => ( @@ -309,18 +338,31 @@ const MessageSimpleWithContext = < () => ( - - {MessageSwipeContent ? : null} - - {renderMessageBubble} + {isBeingSwiped ? ( + <> + + {MessageSwipeContent ? : null} + + + {renderMessageBubble} + + + ) : ( + renderMessageBubble + )} ), [ MessageSwipeContent, contentWrapper, + isBeingSwiped, messageBubbleAnimatedStyle, messageSwipeToReplyHitSlop, renderMessageBubble, diff --git a/package/src/components/MessageInput/InputButtons.tsx b/package/src/components/MessageInput/InputButtons.tsx index 4ffa702bee..b842a1f5a8 100644 --- a/package/src/components/MessageInput/InputButtons.tsx +++ b/package/src/components/MessageInput/InputButtons.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import { StyleSheet, View } from 'react-native'; import { @@ -29,12 +29,12 @@ export type InputButtonsWithContextProps< | 'hasCommands' | 'hasFilePicker' | 'hasImagePicker' + | 'hasText' | 'MoreOptionsButton' | 'openCommandsPicker' | 'selectedPicker' | 'setShowMoreOptions' | 'showMoreOptions' - | 'text' | 'toggleAttachmentPicker' >; @@ -51,11 +51,11 @@ export const InputButtonsWithContext = < hasCommands, hasFilePicker, hasImagePicker, + hasText, MoreOptionsButton, openCommandsPicker, setShowMoreOptions, showMoreOptions, - text, } = props; const { @@ -64,6 +64,10 @@ export const InputButtonsWithContext = < }, } = useTheme(); + const handleShowMoreOptions = useCallback(() => { + setShowMoreOptions(true); + }, [setShowMoreOptions]); + const ownCapabilities = useOwnCapabilitiesContext(); if (giphyActive) { @@ -71,7 +75,7 @@ export const InputButtonsWithContext = < } return !showMoreOptions && (hasCameraPicker || hasImagePicker || hasFilePicker) && hasCommands ? ( - setShowMoreOptions(true)} /> + ) : ( <> {(hasCameraPicker || hasImagePicker || hasFilePicker) && ownCapabilities.uploadFile && ( @@ -81,7 +85,7 @@ export const InputButtonsWithContext = < )} - {hasCommands && !text && ( + {hasCommands && !hasText && ( @@ -100,9 +104,9 @@ const areEqual = (); @@ -188,12 +192,12 @@ export const InputButtons = < hasCommands, hasFilePicker, hasImagePicker, + hasText, MoreOptionsButton, openCommandsPicker, selectedPicker, setShowMoreOptions, showMoreOptions, - text, toggleAttachmentPicker, }} {...props} diff --git a/package/src/components/MessageInput/MessageInput.tsx b/package/src/components/MessageInput/MessageInput.tsx index c0202a1d06..34c1cd1004 100644 --- a/package/src/components/MessageInput/MessageInput.tsx +++ b/package/src/components/MessageInput/MessageInput.tsx @@ -108,7 +108,7 @@ type MessageInputPropsWithContext< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = Pick & Pick, 'isOnline'> & - Pick, 'members' | 'threadList' | 'watchers'> & + Pick, 'channel' | 'members' | 'threadList' | 'watchers'> & Pick< MessageInputContextValue, | 'additionalTextInputProps' @@ -198,6 +198,7 @@ const MessageInputWithContext = < AudioRecordingLockIndicator, AudioRecordingPreview, AutoCompleteSuggestionList, + channel, closeAttachmentPicker, closePollCreationDialog, cooldownEndsAt, @@ -746,7 +747,6 @@ const MessageInputWithContext = < })), }; - const { channel } = useChannelContext(); const { aiState } = useAIState(channel); const stopGenerating = useCallback(() => channel?.stopAIResponse(), [channel]); @@ -860,9 +860,8 @@ const MessageInputWithContext = < {shouldDisplayStopAIGeneration ? ( - ) : ( - isSendingButtonVisible() && - (cooldownRemainingSeconds ? ( + ) : isSendingButtonVisible() ? ( + cooldownRemainingSeconds ? ( ) : ( @@ -870,8 +869,8 @@ const MessageInputWithContext = < disabled={sending.current || !isValidMessage() || (giphyActive && !isOnline)} /> - )) - )} + ) + ) : null} {audioRecordingEnabled && isAudioRecorderAvailable() && !micLocked && ( (); + const { channel, members, threadList, watchers } = useChannelContext(); const { additionalTextInputProps, @@ -1263,6 +1269,7 @@ export const MessageInput = < AutoCompleteSuggestionHeader, AutoCompleteSuggestionItem, AutoCompleteSuggestionList, + channel, clearEditingState, clearQuotedMessageState, closeAttachmentPicker, diff --git a/package/src/components/MessageInput/MoreOptionsButton.tsx b/package/src/components/MessageInput/MoreOptionsButton.tsx index 300453cd39..813992adeb 100644 --- a/package/src/components/MessageInput/MoreOptionsButton.tsx +++ b/package/src/components/MessageInput/MoreOptionsButton.tsx @@ -1,6 +1,6 @@ import React from 'react'; import type { GestureResponderEvent } from 'react-native'; -import { TouchableOpacity } from 'react-native-gesture-handler'; +import { TouchableOpacity } from 'react-native'; import { useTheme } from '../../contexts/themeContext/ThemeContext'; import { CircleRight } from '../../icons/CircleRight'; diff --git a/package/src/components/MessageList/MessageList.tsx b/package/src/components/MessageList/MessageList.tsx index 5434a65770..dc06381745 100644 --- a/package/src/components/MessageList/MessageList.tsx +++ b/package/src/components/MessageList/MessageList.tsx @@ -617,7 +617,6 @@ const MessageListWithContext = < setTimeout(() => { channelResyncScrollSet.current = true; if (channel.countUnread() > 0) { - console.log('marking read'); markRead(); } }, 500); diff --git a/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap b/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap index b05aa25bed..8b4be28659 100644 --- a/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap +++ b/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap @@ -405,254 +405,123 @@ exports[`Thread should match thread snapshot 1`] = ` style={ [ { - "position": "absolute", - }, - { - "opacity": undefined, - "transform": [ - { - "translateX": undefined, - }, - ], + "alignItems": "center", + "flexDirection": "row", }, {}, ] } > - - - - - - - - - - - - - - Message6 - + Message6 - + @@ -916,254 +785,123 @@ exports[`Thread should match thread snapshot 1`] = ` style={ [ { - "position": "absolute", - }, - { - "opacity": undefined, - "transform": [ - { - "translateX": undefined, - }, - ], + "alignItems": "center", + "flexDirection": "row", }, {}, ] } > - - - - - - - - - - - - - - Message5 - + Message5 - + @@ -1450,269 +1188,138 @@ exports[`Thread should match thread snapshot 1`] = ` "left": 750, "right": 750, } - } - style={ - [ - { - "alignItems": "center", - "flexDirection": "row", - }, - {}, - ] - } - > - - - - - - - - - - - + } + style={ + [ + { + "alignItems": "center", + "flexDirection": "row", + }, + {}, + ] + } + > - - - Message4 - + Message4 - + @@ -1969,254 +1576,123 @@ exports[`Thread should match thread snapshot 1`] = ` style={ [ { - "position": "absolute", - }, - { - "opacity": undefined, - "transform": [ - { - "translateX": undefined, - }, - ], + "alignItems": "center", + "flexDirection": "row", }, {}, ] } > - - - - - - - - - - - - - - Message3 - + Message3 - + diff --git a/package/src/contexts/messageInputContext/MessageInputContext.tsx b/package/src/contexts/messageInputContext/MessageInputContext.tsx index 69332c214e..d66efa11ef 100644 --- a/package/src/contexts/messageInputContext/MessageInputContext.tsx +++ b/package/src/contexts/messageInputContext/MessageInputContext.tsx @@ -125,6 +125,7 @@ export type LocalMessageInputContext< url: string; }; }; + giphyEnabled: boolean; closeAttachmentPicker: () => void; /** The time at which the active cooldown will end */ cooldownEndsAt: Date; @@ -151,6 +152,7 @@ export type LocalMessageInputContext< */ fileUploads: FileUpload[]; giphyActive: boolean; + hasText: boolean; /** * An array of image objects which are set for upload. It has the following structure: * @@ -608,6 +610,7 @@ export const MessageInputProvider = < text, } = useMessageDetailsForState(editing, initialValue); const { endsAt: cooldownEndsAt, start: startCooldown } = useCooldown(); + const { onChangeText } = value; const threadId = thread?.id; useEffect(() => { @@ -655,20 +658,23 @@ export const MessageInputProvider = < return false; }; - const onChange = (newText: string) => { - if (sending.current) { - return; - } - setText(newText); + const onChange = useCallback( + (newText: string) => { + if (sending.current) { + return; + } + setText(newText); - if (newText && channel && channelCapabities.sendTypingEvents && isOnline) { - logChatPromiseExecution(channel.keystroke(thread?.id), 'start typing event'); - } + if (newText && channel && channelCapabities.sendTypingEvents && isOnline) { + logChatPromiseExecution(channel.keystroke(thread?.id), 'start typing event'); + } - if (value.onChangeText) { - value.onChangeText(newText); - } - }; + if (onChangeText) { + onChangeText(newText); + } + }, + [channel, channelCapabities.sendTypingEvents, isOnline, setText, thread?.id, onChangeText], + ); const openCommandsPicker = () => { appendText('/'); @@ -948,10 +954,6 @@ export const MessageInputProvider = < const prevText = giphyEnabled && giphyActive ? `/giphy ${text}` : text; setText(''); - if (inputBoxRef.current) { - inputBoxRef.current.clear(); - } - const attachments = [] as Attachment[]; for (const image of imageUploads) { if (enableOfflineSupport) { @@ -1446,6 +1448,7 @@ export const MessageInputProvider = < cooldownEndsAt, fileUploads, giphyActive, + giphyEnabled, imageUploads, inputBoxRef, isValidMessage, @@ -1490,6 +1493,7 @@ export const MessageInputProvider = < uploadNewImage, ...value, closePollCreationDialog, + hasText: !!text, openPollCreationDialog, sendMessage, // overriding the originally passed in sendMessage showPollCreationDialog, diff --git a/package/src/contexts/messageInputContext/__tests__/pickFile.test.tsx b/package/src/contexts/messageInputContext/__tests__/pickFile.test.tsx index 2eeb4520bb..e4719624ec 100644 --- a/package/src/contexts/messageInputContext/__tests__/pickFile.test.tsx +++ b/package/src/contexts/messageInputContext/__tests__/pickFile.test.tsx @@ -61,7 +61,7 @@ describe("MessageInputContext's pickFile", () => { maxNumberOfFiles: 2, }; - it.each([[3, 1]])( + it.each([[3, 2]])( 'run pickFile when numberOfUploads is %d and alert is triggered %d number of times', async (numberOfUploads, numberOfTimesCalled) => { const { rerender, result } = renderHook(() => useMessageInputContext(), { @@ -87,7 +87,6 @@ describe("MessageInputContext's pickFile", () => { }); expect(Alert.alert).toHaveBeenCalledTimes(numberOfTimesCalled); - expect(Alert.alert).toHaveBeenCalledWith('Maximum number of files reached'); }, ); diff --git a/package/src/contexts/messageInputContext/hooks/useCreateMessageInputContext.ts b/package/src/contexts/messageInputContext/hooks/useCreateMessageInputContext.ts index ea42939ca3..b9e48da58f 100644 --- a/package/src/contexts/messageInputContext/hooks/useCreateMessageInputContext.ts +++ b/package/src/contexts/messageInputContext/hooks/useCreateMessageInputContext.ts @@ -41,11 +41,13 @@ export const useCreateMessageInputContext = < FileUploadPreview, fileUploads, giphyActive, + giphyEnabled, handleAttachButtonPress, hasCameraPicker, hasCommands, hasFilePicker, hasImagePicker, + hasText, ImageUploadPreview, imageUploads, initialValue, @@ -164,11 +166,13 @@ export const useCreateMessageInputContext = < FileUploadPreview, fileUploads, giphyActive, + giphyEnabled, handleAttachButtonPress, hasCameraPicker, hasCommands, hasFilePicker, hasImagePicker, + hasText, ImageUploadPreview, imageUploads, initialValue, @@ -246,6 +250,8 @@ export const useCreateMessageInputContext = < editingdep, fileUploadsValue, giphyActive, + giphyEnabled, + hasText, imageUploadsValue, maxMessageLength, mentionedUsersLength, diff --git a/package/src/contexts/messageInputContext/hooks/useMessageDetailsForState.ts b/package/src/contexts/messageInputContext/hooks/useMessageDetailsForState.ts index 8708b6e4e1..9ff7843a73 100644 --- a/package/src/contexts/messageInputContext/hooks/useMessageDetailsForState.ts +++ b/package/src/contexts/messageInputContext/hooks/useMessageDetailsForState.ts @@ -22,20 +22,22 @@ export const useMessageDetailsForState = < const [imageUploads, setImageUploads] = useState([]); const [mentionedUsers, setMentionedUsers] = useState([]); const [numberOfUploads, setNumberOfUploads] = useState(0); - const [showMoreOptions, setShowMoreOptions] = useState(true); const initialTextValue = initialValue || ''; const [text, setText] = useState(initialTextValue); + const isEqualToInitialText = text === initialTextValue; + + const [showMoreOptions, setShowMoreOptions] = useState(true); + useEffect(() => { - if (text !== initialTextValue) { + if (!isEqualToInitialText) { setShowMoreOptions(false); } if (fileUploads.length || imageUploads.length) { setShowMoreOptions(false); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [text, imageUploads.length, fileUploads.length]); + }, [isEqualToInitialText, imageUploads.length, fileUploads.length]); const messageValue = message ? stringifyMessage(message) : ''; diff --git a/package/src/contexts/suggestionsContext/SuggestionsContext.tsx b/package/src/contexts/suggestionsContext/SuggestionsContext.tsx index 16fbb2c137..42794e6607 100644 --- a/package/src/contexts/suggestionsContext/SuggestionsContext.tsx +++ b/package/src/contexts/suggestionsContext/SuggestionsContext.tsx @@ -1,4 +1,4 @@ -import React, { PropsWithChildren, useContext, useState } from 'react'; +import React, { PropsWithChildren, useCallback, useContext, useMemo, useState } from 'react'; import type { CommandResponse, UserResponse } from 'stream-chat'; @@ -95,35 +95,54 @@ export const SuggestionsProvider = < children, value, }: PropsWithChildren<{ value?: Partial> }>) => { + const { AutoCompleteSuggestionHeader, AutoCompleteSuggestionItem, AutoCompleteSuggestionList } = + value ?? {}; const [triggerType, setTriggerType] = useState(null); const [suggestions, setSuggestions] = useState>(); const [suggestionsViewActive, setSuggestionsViewActive] = useState(false); - const openSuggestions = (component: SuggestionComponentType) => { + const openSuggestions = useCallback((component: SuggestionComponentType) => { setTriggerType(component); setSuggestionsViewActive(true); - }; - - const updateSuggestions = (newSuggestions: Suggestions) => { - setSuggestions(newSuggestions); - setSuggestionsViewActive(!!triggerType); - }; + }, []); + + const updateSuggestions = useCallback( + (newSuggestions: Suggestions) => { + setSuggestions(newSuggestions); + setSuggestionsViewActive(!!triggerType); + }, + [triggerType], + ); - const closeSuggestions = () => { + const closeSuggestions = useCallback(() => { setTriggerType(null); setSuggestions(undefined); setSuggestionsViewActive(false); - }; - - const suggestionsContext = { - ...value, + }, []); + + const suggestionsContext = useMemo(() => { + return { + AutoCompleteSuggestionHeader, + AutoCompleteSuggestionItem, + AutoCompleteSuggestionList, + closeSuggestions, + openSuggestions, + suggestions, + suggestionsViewActive, + triggerType, + updateSuggestions, + }; + }, [ + AutoCompleteSuggestionHeader, + AutoCompleteSuggestionItem, + AutoCompleteSuggestionList, closeSuggestions, openSuggestions, suggestions, suggestionsViewActive, triggerType, updateSuggestions, - }; + ]); return (