Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/perf-useismacOS-sync-external-store.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': patch
---

perf(useIsMacOS): replace useState+useEffect with useSyncExternalStore to eliminate unnecessary re-render
37 changes: 24 additions & 13 deletions packages/react/src/hooks/useIsMacOS.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
import {isMacOS as ssrUnsafeIsMacOS} from '@primer/behaviors/utils'
import {useEffect, useState} from 'react'
import {canUseDOM} from '../utils/environment'
import {useSyncExternalStore} from 'react'

// No-op. The platform never changes at runtime, so there is nothing to
// subscribe to. Hoisted to avoid creating a new function on every call.
const subscribe = () => () => {}

// Safe default for SSR since we can't detect the platform on the server.
const getServerSnapshot = () => false

/**
* SSR-safe hook for determining if the current platform is MacOS. When rendering
* server-side, will default to non-MacOS and then re-render in an effect if the
* client turns out to be a MacOS device.
* SSR-safe hook for determining if the current platform is MacOS.
*
* Uses `useSyncExternalStore` to read the platform value:
*
* - On the **client**, `ssrUnsafeIsMacOS` reads `navigator.userAgent` and
* returns the real value immediately, with no extra render pass.
*
* - On the **server**, returns `false`. During hydration, if the snapshots
* differ, React handles the mismatch internally in a single synchronous
* pass, avoiding the layout shift that a deferred `useEffect` + `setState`
* would cause.
*
* Previous implementation used `useState` + `useEffect`, which caused an
* unconditional second render on every mount (even on the client where the
* initial value was already correct).
*/
export function useIsMacOS() {
const [isMacOS, setIsMacOS] = useState(() => (canUseDOM ? ssrUnsafeIsMacOS() : false))

useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
setIsMacOS(ssrUnsafeIsMacOS())
}, [])

return isMacOS
return useSyncExternalStore(subscribe, ssrUnsafeIsMacOS, getServerSnapshot)
}
Comment on lines 28 to 30
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useIsMacOS is a publicly exported hook and its behavior around SSR/hydration is subtle. There are hook unit tests for other hooks under packages/react/src/hooks/__tests__, but none for this one—please add a small useIsMacOS test that at least verifies SSR defaults to false (e.g. via react-dom/server), and client behavior can be controlled via a mocked @primer/behaviors/utils implementation.

Copilot uses AI. Check for mistakes.
Loading