Skip to content

React Hooks ​

Import from os-detect/react. Requires React 17+.

tsx
import { useOS, useIsWindows11, useFormFactor, useRuntime, usePrimaryInput } from 'os-detect/react'

Current OS ​

useOS()

OS

Returns the current OS string synchronously, computed once via useState(() => getOS()) and stable across re-renders.

tsx
function Banner() {
  const os = useOS() // 'windows' | 'macos' | 'ios' | ...

  return <p>Running on {os}</p>
}

Windows 11 Detection ​

useIsWindows11()

boolean | null

Starts the async detectIsWindows11() check inside useEffect and updates state when it resolves; the value is null while detection is in progress.

tsx
function WindowsBadge() {
  const isWin11 = useIsWindows11() // null → true | false

  if (isWin11 === null) return <Spinner />
  return <p>{isWin11 ? 'Windows 11' : 'Windows 10 or older'}</p>
}

Form Factor ​

useFormFactor()

FormFactor

Computed once via useState(() => getFormFactor()) and stable across re-renders.

tsx
function Layout() {
  const formFactor = useFormFactor() // 'phone' | 'tablet' | 'desktop' | 'tv' | 'unknown'

  return formFactor === 'phone' ? <MobileNav /> : <DesktopNav />
}

Runtime Context ​

useRuntime()

Runtime

Computed once via useState(() => getRuntime()) and stable across re-renders.

tsx
function DebugPanel() {
  const runtime = useRuntime() // 'node' | 'browser' | 'webworker' | 'unknown'

  return <p>Running in: {runtime}</p>
}

Primary Input ​

usePrimaryInput()

PrimaryInput

'unknown' until the first useEffect run, then updates live. Unlike useOS()/useFormFactor()/useRuntime(), this one is genuinely reactive: it listens for matchMedia('(pointer: …)') change events inside useEffect and re-renders if a hybrid device's keyboard/mouse gets attached or detached mid-session.

tsx
function TapTargets() {
  const input = usePrimaryInput() // 'mouse' | 'touch' | 'unknown'

  return input === 'touch' ? <BigButtons /> : <CompactButtons />
}

SSR (Next.js) ​

useOS() computes its value once via useState(() => getOS()), which runs during render on the server too — in SSR, that means the server's OS gets baked into the initial HTML, which can differ from the client's, and can cause a hydration mismatch when the two disagree. useFormFactor() and useRuntime() follow the exact same pattern and carry the same risk. If OS-dependent content must exactly match on the client, defer it to useEffect instead:

tsx
import { useEffect, useState } from 'react'
import { getOS } from 'os-detect'
import type { OS } from 'os-detect'

function OSBanner() {
  const [os, setOS] = useState<OS | null>(null)

  useEffect(() => {
    setOS(getOS())
  }, [])

  if (!os) return null
  return <p>OS: {os}</p>
}

useIsWindows11() already follows this pattern internally (it resolves inside useEffect), so it never needs this workaround — and neither does usePrimaryInput(), for the same reason: it starts at 'unknown' and only calls getPrimaryInput() for the first time inside useEffect.