Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 3 additions & 1 deletion src/components/Message/QuotedMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<MessageContextValue, 'renderText'>;

Expand All @@ -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;

Expand Down Expand Up @@ -96,7 +98,7 @@ export const QuotedMessage = ({ renderText: propsRenderText }: QuotedMessageProp
</div>
</div>
{message.attachments?.length ? (
<Attachment attachments={message.attachments} />
<Attachment actionHandler={actionHandler} attachments={message.attachments} />
) : null}
</>
);
Expand Down
11 changes: 6 additions & 5 deletions src/components/Message/hooks/useActionHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>;

Expand All @@ -19,15 +20,15 @@ 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) {
console.warn(handleActionWarning);
return;
}

const messageID = message.id;
const messageId = message.id;
let formData: FormData = {};

// deprecated: value&name should be removed in favor of data obj
Expand All @@ -37,14 +38,14 @@ 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);
} else {
removeMessage(message);
}
}
};
});
}
35 changes: 35 additions & 0 deletions src/utils/useStableCallback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useCallback, useRef } from 'react';

export type StableCallback<A extends unknown[], R> = (...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 = <A extends unknown[], R>(
callback: StableCallback<A, R>,
): StableCallback<A, R> => {
const ref = useRef(callback);
ref.current = callback;

return useCallback<StableCallback<A, R>>((...args) => ref.current(...args), []);
};
Loading