diff --git a/src/components/Message/QuotedMessage.tsx b/src/components/Message/QuotedMessage.tsx index 45e472b1a..db2cf1908 100644 --- a/src/components/Message/QuotedMessage.tsx +++ b/src/components/Message/QuotedMessage.tsx @@ -12,6 +12,7 @@ import { useTranslationContext } from '../../context/TranslationContext'; import { useChannelActionContext } from '../../context/ChannelActionContext'; import { renderText as defaultRenderText } from './renderText'; import type { MessageContextValue } from '../../context/MessageContext'; +import { useActionHandler } from './'; export type QuotedMessageProps = Pick; @@ -26,6 +27,7 @@ export const QuotedMessage = ({ renderText: propsRenderText }: QuotedMessageProp } = useMessageContext('QuotedMessage'); const { t, userLanguage } = useTranslationContext('QuotedMessage'); const { jumpToMessage } = useChannelActionContext('QuotedMessage'); + const actionHandler = useActionHandler(message); const renderText = propsRenderText ?? contextRenderText ?? defaultRenderText; @@ -96,7 +98,7 @@ export const QuotedMessage = ({ renderText: propsRenderText }: QuotedMessageProp {message.attachments?.length ? ( - + ) : null} ); diff --git a/src/components/Message/hooks/useActionHandler.ts b/src/components/Message/hooks/useActionHandler.ts index a66c6881e..2c43b6847 100644 --- a/src/components/Message/hooks/useActionHandler.ts +++ b/src/components/Message/hooks/useActionHandler.ts @@ -3,6 +3,7 @@ import { useChannelStateContext } from '../../../context/ChannelStateContext'; import type React from 'react'; import type { LocalMessage } from 'stream-chat'; +import { useStableCallback } from '../../../utils/useStableCallback'; export type FormData = Record; @@ -19,7 +20,7 @@ export function useActionHandler(message?: LocalMessage): ActionHandlerReturnTyp const { removeMessage, updateMessage } = useChannelActionContext('useActionHandler'); const { channel } = useChannelStateContext('useActionHandler'); - return async (dataOrName, value, event) => { + return useStableCallback(async (dataOrName, value, event) => { if (event) event.preventDefault(); if (!message || !updateMessage || !removeMessage || !channel) { @@ -27,7 +28,7 @@ export function useActionHandler(message?: LocalMessage): ActionHandlerReturnTyp return; } - const messageID = message.id; + const messageId = message.id; let formData: FormData = {}; // deprecated: value&name should be removed in favor of data obj @@ -37,8 +38,8 @@ export function useActionHandler(message?: LocalMessage): ActionHandlerReturnTyp formData = { ...dataOrName }; } - if (messageID) { - const data = await channel.sendAction(messageID, formData); + if (messageId) { + const data = await channel.sendAction(messageId, formData); if (data?.message) { updateMessage(data.message); @@ -46,5 +47,5 @@ export function useActionHandler(message?: LocalMessage): ActionHandlerReturnTyp removeMessage(message); } } - }; + }); } diff --git a/src/utils/useStableCallback.ts b/src/utils/useStableCallback.ts new file mode 100644 index 000000000..8d2c0d38c --- /dev/null +++ b/src/utils/useStableCallback.ts @@ -0,0 +1,35 @@ +import { useCallback, useRef } from 'react'; + +export type StableCallback = (...args: A) => R; + +/** + * A utility hook implementing a stable callback. It takes in an unstable method that + * is supposed to be invoked somewhere deeper in the DOM tree without making it + * change its reference every time the parent component rerenders. It will also return + * the value of the callback if it does return one. + * A common use-case would be having a function whose invocation depends on state + * somewhere high up in the DOM tree and wanting to use the same function deeper + * down, for example in a leaf node and simply using useCallback results in + * cascading dependency hell. If we wrap it in useStableCallback, we would be able + * to: + * - Use the same function as a dependency of another hook (since it is stable) + * - Still invoke it and get the latest state + * + * **Caveats:** + * - Never wrap a function that is supposed to return a React.ReactElement in + * useStableCallback, since React will not know that the DOM needs to be updated + * whenever the callback value changes (for example, renderItem from FlatList must + * never be wrapped in this hook) + * - Always prefer using a standard useCallback/stable function wherever possible + * (the purpose of useStableCallback is to bridge the gap between top level contexts + * and cascading rereders in downstream components - **not** as an escape hatch) + * @param callback - the callback we want to stabilize + */ +export const useStableCallback = ( + callback: StableCallback, +): StableCallback => { + const ref = useRef(callback); + ref.current = callback; + + return useCallback>((...args) => ref.current(...args), []); +};