Fix props, refs and keys that React was warning about (#38238)

* webapp: let Tag forward a ref

WithTooltip anchors its popover by cloning its child with a ref. Tag is a
function component, so the ref was dropped and React warned about it, leaving
the marketplace label tooltips without a positioning reference.

Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com>
(cherry picked from commit 3a34196e47)

* webapp: stop leaking component props onto DOM elements

isReadonly was hardcoded to false, never read, and fell through a prop spread
onto the channel header menu's <li>. SearchChannelSuggestion was connected with
a null mapDispatchToProps, so react-redux injected dispatch, which the
suggestion container then spread onto its <li>.

Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com>
(cherry picked from commit 959ba3b90d)

* webapp: stop connect's injected dispatch reaching suggestion list items

Suggestions are connected components that forward their rest props into
SuggestionContainer, which spreads them onto its <li>. A connect() without a
mapDispatchToProps injects dispatch, so React saw it as an invalid DOM
attribute. Drop it in the container alongside item, which covers every
suggestion rather than one call site.

Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com>
(cherry picked from commit 41d09dfbfd)

* webapp: key the add-workspace menu on a remote cluster's full identity

RemoteClusters is keyed on (RemoteId, Name), so the same remote_id can appear
more than once in the dropdown and React reported duplicate keys.

Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com>
(cherry picked from commit a4025d5271)

* fixup! webapp: let Tag forward a ref

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com>
This commit is contained in:
Jesse Hallam
2026-09-02 16:53:32 +00:00
committed by GitHub
co-authored by Cursor Agent Jesse Hallam
parent 235bddbae0
commit f6f2719165
5 changed files with 15 additions and 9 deletions
@@ -62,8 +62,6 @@ export default function ChannelHeaderMenu({dmUser, gmMembers, isMobile, archived
const isChannelBookmarksEnabled = useSelector(getIsChannelBookmarksEnabled);
const isChannelAutotranslated = useSelector((state: GlobalState) => (channel?.id ? isChannelAutotranslatedSelector(state, channel.id) : false));
const isReadonly = false;
if (!channel) {
return null;
}
@@ -181,7 +179,6 @@ export default function ChannelHeaderMenu({dmUser, gmMembers, isMobile, archived
isFavorite={isFavorite}
isMobile={isMobile || false}
isDefault={isDefault}
isReadonly={isReadonly}
isLicensedForLDAPGroups={isLicensedForLDAPGroups}
isChannelBookmarksEnabled={isChannelBookmarksEnabled}
isChannelAutotranslated={isChannelAutotranslated}
@@ -37,7 +37,6 @@ interface Props extends Menu.FirstMenuItemProps {
channel: Channel;
user: UserProfile;
isMuted: boolean;
isReadonly: boolean;
isDefault: boolean;
isMobile: boolean;
isFavorite: boolean;
@@ -97,10 +97,11 @@ export default function AddWorkspaceDropdown({
disabled={true}
/>
)}
{/* A remote cluster is keyed on (remote_id, name), so remote_id alone can repeat here. */}
{!loading && available.map((rc) => (
<Menu.Item
key={rc.remote_id}
id={`${MENU_ID}-item-${rc.remote_id}`}
key={`${rc.remote_id}-${rc.name}`}
id={`${MENU_ID}-item-${rc.remote_id}-${rc.name}`}
labels={<span>{rc.display_name || rc.name}</span>}
onClick={() => handleSelect(rc)}
/>
@@ -36,6 +36,10 @@ const SuggestionContainer = React.forwardRef<HTMLLIElement, SuggestionProps<unkn
Reflect.deleteProperty(otherProps, 'item');
// Suggestions are usually connected, and a connect() without mapDispatchToProps injects a
// `dispatch` prop that the suggestion then forwards here along with the rest of its props.
Reflect.deleteProperty(otherProps, 'dispatch');
const handleClick = useCallback((e: React.MouseEvent) => {
e.preventDefault();
@@ -127,7 +127,7 @@ const TagText = styled.span`
text-overflow: ellipsis;
`;
const Tag = ({
const Tag = React.forwardRef<HTMLElement, Props>(({
variant,
onClick,
className,
@@ -136,7 +136,7 @@ const Tag = ({
size = 'xs',
uppercase = false,
...rest
}: Props) => {
}, ref) => {
const Icon = iconName ? glyphMap[iconName] : null;
const element = onClick ? 'button' : 'div';
@@ -157,6 +157,10 @@ const Tag = ({
return (
<TagWrapper
{...rest}
// TagWrapper renders a div or a button, but styled-components types
// its ref as one or the other, never HTMLElement.
ref={ref as React.Ref<HTMLDivElement>}
as={element}
uppercase={uppercase}
onClick={onClick}
@@ -166,6 +170,7 @@ const Tag = ({
<TagText>{text}</TagText>
</TagWrapper>
);
};
});
Tag.displayName = 'Tag';
export default memo(Tag);