Skip to content

Commit c52628a

Browse files
fix(web): fix hydration mismatch in KeyboardShortcutHint (#1041)
* fix(web): fix hydration mismatch in KeyboardShortcutHint Replace module-level IS_MAC constant with a useIsMac hook that defers platform detection to after mount via useEffect, ensuring server and initial client renders agree before updating to the correct value. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: update CHANGELOG for #1041 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b04e960 commit c52628a

File tree

5 files changed

+26
-10
lines changed

5 files changed

+26
-10
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Fixed
1111
- Fixed line numbers being selectable in Safari in the lightweight code highlighter. [#1037](https://github.com/sourcebot-dev/sourcebot/pull/1037)
1212
- Fixed GitLab sync deleting repos when the API returns a non-404 error (e.g. 500) during group/user/project fetch. [#1039](https://github.com/sourcebot-dev/sourcebot/pull/1039)
13+
- Fixed React hydration mismatch in `KeyboardShortcutHint` caused by platform detection running at module load time during SSR. [#1041](https://github.com/sourcebot-dev/sourcebot/pull/1041)
1314

1415
### Added
1516
- Added optional copy button to the lightweight code highlighter (`isCopyButtonVisible` prop), shown on hover. [#1037](https://github.com/sourcebot-dev/sourcebot/pull/1037)

packages/web/src/app/components/keyboardShortcutHint.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
'use client';
22

3-
import { cn, IS_MAC } from '@/lib/utils'
4-
import React, { useMemo } from 'react'
3+
import { cn } from '@/lib/utils'
4+
import { useIsMac } from '@/hooks/useIsMac'
5+
import { useMemo } from 'react'
56

67
interface KeyboardShortcutHintProps {
78
shortcut: string
@@ -81,16 +82,17 @@ function mapKey(key: string, keyMap: Record<string, string>): string {
8182
* Converts react-hotkeys syntax to platform-appropriate keyboard shortcut display.
8283
* Accepts formats like: "mod+b", "alt+shift+f12", "ctrl enter"
8384
*/
84-
function getPlatformShortcut(shortcut: string): string {
85+
function getPlatformShortcut(shortcut: string, isMac: boolean): string {
8586
// Split by + or space to handle both "mod+b" and "⌘ B" formats
8687
const keys = shortcut.split(/[+\s]+/).filter(Boolean);
87-
const keyMap = IS_MAC ? MAC_KEY_MAP : WINDOWS_KEY_MAP;
88-
88+
const keyMap = isMac ? MAC_KEY_MAP : WINDOWS_KEY_MAP;
89+
8990
return keys.map(key => mapKey(key, keyMap)).join(' ');
9091
}
9192

9293
export function KeyboardShortcutHint({ shortcut, label, className }: KeyboardShortcutHintProps) {
93-
const platformShortcut = useMemo(() => getPlatformShortcut(shortcut), [shortcut]);
94+
const isMac = useIsMac();
95+
const platformShortcut = useMemo(() => getPlatformShortcut(shortcut, isMac), [shortcut, isMac]);
9496

9597
return (
9698
<div className={cn("inline-flex items-center", className)} aria-label={label || `Keyboard shortcut: ${platformShortcut}`}>

packages/web/src/features/chat/components/chatBox/chatBox.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { Button } from "@/components/ui/button";
55
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
66
import { CustomEditor, LanguageModelInfo, MentionElement, RenderElementPropsFor, SearchScope } from "@/features/chat/types";
77
import { insertMention, slateContentToString } from "@/features/chat/utils";
8-
import { cn, IS_MAC } from "@/lib/utils";
8+
import { cn } from "@/lib/utils";
9+
import { useIsMac } from "@/hooks/useIsMac";
910
import { computePosition, flip, offset, shift, VirtualElement } from "@floating-ui/react";
1011
import { ArrowUp, Loader2, StopCircleIcon } from "lucide-react";
1112
import { Fragment, KeyboardEvent, memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
@@ -367,6 +368,7 @@ const MentionComponent = ({
367368
}: RenderElementPropsFor<MentionElement>) => {
368369
const selected = useSelected();
369370
const focused = useFocused();
371+
const isMac = useIsMac();
370372

371373
if (data.type === 'file') {
372374
return (
@@ -384,7 +386,7 @@ const MentionComponent = ({
384386
>
385387
<span contentEditable={false} className="flex flex-row items-center select-none">
386388
{/* @see: https://github.com/ianstormtaylor/slate/issues/3490 */}
387-
{IS_MAC ? (
389+
{isMac ? (
388390
<Fragment>
389391
{children}
390392
<VscodeFileIcon fileName={data.name} className="w-3 h-3 mr-1" />

packages/web/src/hooks/useIsMac.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
'use client';
2+
3+
import { useState, useEffect } from 'react';
4+
5+
export function useIsMac(): boolean {
6+
const [isMac, setIsMac] = useState(false);
7+
8+
useEffect(() => {
9+
setIsMac(/Mac OS X/.test(navigator.userAgent));
10+
}, []);
11+
12+
return isMac;
13+
}

packages/web/src/lib/utils.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -619,8 +619,6 @@ export const getOrgMetadata = (org: Org): OrgMetadata | null => {
619619
return currentMetadata.success ? currentMetadata.data : null;
620620
}
621621

622-
export const IS_MAC = typeof navigator !== 'undefined' && /Mac OS X/.test(navigator.userAgent);
623-
624622

625623
export const isHttpError = (error: unknown, status: number): boolean => {
626624
return error !== null

0 commit comments

Comments
 (0)