From 43625da0ae36d983c339c0d79ae900c8778f5241 Mon Sep 17 00:00:00 2001 From: Kesha Antonov Date: Sat, 6 Apr 2024 12:17:40 +0300 Subject: [PATCH] optimize: prevent excessive re-renders on typing and keyboard show --- example/App.tsx | 2 +- src/GiftedChat.tsx | 236 +++++++++++++++++---------------------------- 2 files changed, 92 insertions(+), 146 deletions(-) diff --git a/example/App.tsx b/example/App.tsx index aadf33927..6ff6a59d2 100644 --- a/example/App.tsx +++ b/example/App.tsx @@ -225,7 +225,7 @@ const App = () => { const renderSend = useCallback((props: SendProps) => { return ( - + ) diff --git a/src/GiftedChat.tsx b/src/GiftedChat.tsx index 366822d42..2aaa5d954 100644 --- a/src/GiftedChat.tsx +++ b/src/GiftedChat.tsx @@ -7,10 +7,8 @@ import { } from '@expo/react-native-action-sheet' import dayjs from 'dayjs' import localizedFormat from 'dayjs/plugin/localizedFormat' -import PropTypes from 'prop-types' import { FlatList, - LayoutChangeEvent, Platform, StyleProp, StyleSheet, @@ -214,19 +212,11 @@ export interface GiftedChatProps { ): boolean } -export interface GiftedChatState { - isInitialized: boolean - composerHeight?: number - isTypingDisabled: boolean - text?: string -} - function GiftedChat ( props: GiftedChatProps ) { const { messages = [], - text = undefined, initialText = '', isTyping, messageIdGenerator = () => uuidv4(), @@ -252,21 +242,18 @@ function GiftedChat ( textInputRef = createRef(), } = props - const maxHeightRef = useRef(undefined) - const isFirstLayoutRef = useRef(true) const actionSheetRef = useRef(null) const isTextInputWasFocused: MutableRefObject = useRef(false) - const [state, setState] = useState({ - isInitialized: false, // initialization will calculate maxHeight before rendering the chat - composerHeight: minComposerHeight, - isTypingDisabled: false, - text: undefined, - }) + const [isInitialized, setIsInitialized] = useState(false) + const [composerHeight, setComposerHeight] = useState(minComposerHeight!) + const [text, setText] = useState(() => props.text || '') + const [isTypingDisabled, setIsTypingDisabled] = useState(false) const keyboard = useAnimatedKeyboard() const trackingKeyboardMovement = useSharedValue(false) + const debounceEnableTypingTimeoutId = useRef() const insets = useSafeAreaInsets() const keyboardOffsetBottom = useSharedValue(0) @@ -275,11 +262,11 @@ function GiftedChat ( }), [keyboard, keyboardOffsetBottom]) const getTextFromProp = useCallback((fallback: string) => { - if (text === undefined) + if (props.text === undefined) return fallback - return text - }, [text]) + return props.text + }, [props.text]) /** * Store text input focus status when keyboard hide to retrieve @@ -308,38 +295,23 @@ function GiftedChat ( isTextInputWasFocused.current = false }, [textInputRef]) - const onKeyboardWillShow = useCallback(() => { - handleTextInputFocusWhenKeyboardShow() - - setState(state => ({ - ...state, - isTypingDisabled: true, - })) - }, [handleTextInputFocusWhenKeyboardShow]) - - const onKeyboardWillHide = useCallback(() => { - handleTextInputFocusWhenKeyboardHide() - - setState(state => ({ - ...state, - isTypingDisabled: true, - })) - }, [handleTextInputFocusWhenKeyboardHide]) - - const onKeyboardDidShow = useCallback(() => { - setState(state => ({ - ...state, - isTypingDisabled: false, - })) + const diableTyping = useCallback(() => { + clearTimeout(debounceEnableTypingTimeoutId.current) + setIsTypingDisabled(true) }, []) - const onKeyboardDidHide = useCallback(() => { - setState(state => ({ - ...state, - isTypingDisabled: false, - })) + const enableTyping = useCallback(() => { + clearTimeout(debounceEnableTypingTimeoutId.current) + setIsTypingDisabled(false) }, []) + const debounceEnableTyping = useCallback(() => { + clearTimeout(debounceEnableTypingTimeoutId.current) + debounceEnableTypingTimeoutId.current = setTimeout(() => { + enableTyping() + }, 50) + }, [enableTyping]) + const scrollToBottom = useCallback((isAnimated = true) => { if (!messageContainerRef?.current) return @@ -394,18 +366,14 @@ function GiftedChat ( }, [onInputTextChanged]) const resetInputToolbar = useCallback(() => { - if (textInputRef.current) - textInputRef.current.clear() + textInputRef.current?.clear() notifyInputTextReset() - setState(state => ({ - ...state, - text: getTextFromProp(''), - composerHeight: minComposerHeight, - isTypingDisabled: false, - })) - }, [minComposerHeight, getTextFromProp, textInputRef, notifyInputTextReset]) + setComposerHeight(minComposerHeight!) + setText(getTextFromProp('')) + enableTyping() + }, [minComposerHeight, getTextFromProp, textInputRef, notifyInputTextReset, enableTyping]) const _onSend = useCallback(( messages: TMessage[] = [], @@ -424,16 +392,13 @@ function GiftedChat ( }) if (shouldResetInputToolbar === true) { - setState(state => ({ - ...state, - isTypingDisabled: true, - })) + diableTyping() resetInputToolbar() } onSend?.(newMessages) - }, [messageIdGenerator, onSend, user, resetInputToolbar]) + }, [messageIdGenerator, onSend, user, resetInputToolbar, diableTyping]) const onInputSizeChanged = useCallback((size: { height: number }) => { const newComposerHeight = Math.max( @@ -441,22 +406,20 @@ function GiftedChat ( Math.min(maxComposerHeight!, size.height) ) - setState(state => ({ - ...state, - composerHeight: newComposerHeight, - })) + setComposerHeight(newComposerHeight) }, [maxComposerHeight, minComposerHeight]) const _onInputTextChanged = useCallback((_text: string) => { - if (state.isTypingDisabled) + if (isTypingDisabled) return onInputTextChanged?.(_text) // Only set state if it's not being overridden by a prop. - if (text === undefined) - setState(state => ({ ...state, text: _text })) - }, [onInputTextChanged, state.isTypingDisabled, text]) + if (props.text === undefined) { + setText(_text) + } + }, [onInputTextChanged, isTypingDisabled, props.text]) const onInitialLayoutViewLayout = useCallback((e: any) => { const { layout } = e.nativeEvent @@ -466,42 +429,25 @@ function GiftedChat ( notifyInputTextReset() - maxHeightRef.current = layout.height - - setState(state => ({ - ...state, - isInitialized: true, - text: getTextFromProp(initialText), - composerHeight: minComposerHeight, - })) + setIsInitialized(true) + setComposerHeight(minComposerHeight!) + setText(getTextFromProp(initialText)) }, [initialText, minComposerHeight, notifyInputTextReset, getTextFromProp]) - const onMainViewLayout = useCallback((e: LayoutChangeEvent) => { - // TODO: fix an issue when keyboard is dismissing during the initialization - const { layout } = e.nativeEvent - - if ( - maxHeightRef.current !== layout.height || - isFirstLayoutRef.current === true - ) - maxHeightRef.current = layout.height + const inputToolbarFragment = useMemo(() => { + if (!isInitialized) return null - if (isFirstLayoutRef.current === true) - isFirstLayoutRef.current = false - }, []) - - const _renderInputToolbar = useCallback(() => { const inputToolbarProps = { ...props, - text: getTextFromProp(state.text!), - composerHeight: Math.max(minComposerHeight!, state.composerHeight!), + text: getTextFromProp(text), + composerHeight: Math.max(minComposerHeight!, composerHeight), onSend: _onSend, onInputSizeChanged, onTextChanged: _onInputTextChanged, textInputProps: { ...textInputProps, ref: textInputRef, - maxLength: state.isTypingDisabled ? 0 : maxInputLength, + maxLength: isTypingDisabled ? 0 : maxInputLength, }, } @@ -510,16 +456,17 @@ function GiftedChat ( return }, [ + isInitialized, _onSend, getTextFromProp, maxInputLength, minComposerHeight, onInputSizeChanged, props, + text, renderInputToolbar, - state.composerHeight, - state.text, - state.isTypingDisabled, + composerHeight, + isTypingDisabled, textInputRef, textInputProps, _onInputTextChanged, @@ -534,12 +481,9 @@ function GiftedChat ( ) useEffect(() => { - setState(state => ({ - ...state, - // Text prop takes precedence over state. - ...(text !== undefined && text !== state.text && { text }), - })) - }, [text]) + if (props.text != null) + setText(props.text) + }, [props.text]) useEffect(() => { if (!inverted && messages?.length) @@ -549,54 +493,56 @@ function GiftedChat ( useAnimatedReaction( () => keyboard.height.value, (value, prevValue) => { - if (prevValue) - if (value === prevValue) { - if (value === 0) - runOnJS(onKeyboardDidHide)() + if (prevValue && value !== prevValue) { + const isKeyboardMovingUp = value > prevValue + if (isKeyboardMovingUp !== trackingKeyboardMovement.value) { + trackingKeyboardMovement.value = isKeyboardMovingUp + keyboardOffsetBottom.value = withTiming(isKeyboardMovingUp ? insets.bottom : 0, { + duration: 400, + }) + + if (isKeyboardMovingUp) + runOnJS(handleTextInputFocusWhenKeyboardShow)() else - runOnJS(onKeyboardDidShow)() - } else { - const isKeyboardMovingUp = value > prevValue - if (isKeyboardMovingUp !== trackingKeyboardMovement.value) { - trackingKeyboardMovement.value = isKeyboardMovingUp - keyboardOffsetBottom.value = withTiming(isKeyboardMovingUp ? insets.bottom : 0, { - duration: 400, - }) - - if (isKeyboardMovingUp) - runOnJS(onKeyboardWillShow)() - else - runOnJS(onKeyboardWillHide)() + runOnJS(handleTextInputFocusWhenKeyboardHide)() + + if (value === 0) { + runOnJS(enableTyping)() + } else { + runOnJS(diableTyping)() + runOnJS(debounceEnableTyping)() } } + } }, - [keyboard, trackingKeyboardMovement, insets, onKeyboardWillHide, onKeyboardWillShow] + [keyboard, trackingKeyboardMovement, insets, handleTextInputFocusWhenKeyboardHide, handleTextInputFocusWhenKeyboardShow, enableTyping, diableTyping, debounceEnableTyping] ) - if (state.isInitialized) - return ( - - - - - - {renderMessages()} - {_renderInputToolbar()} - - - - - - ) return ( - - {renderLoading?.()} - + + + + { + isInitialized + ? ( + + {renderMessages()} + {inputToolbarFragment} + + ) + : ( + renderLoading?.() + ) + } + + + + ) }