Keyboard shortcuts in Next.js, SSR-safe
Most keyboard-shortcut libraries assume window exists the
moment their module loads. Next.js renders that same module on the
server first, where it doesn't — here's how bind-keyboard avoids the
crash, in both routers.
The actual failure mode
A library that reads window/document at module
scope, or inside a constructor called eagerly at render time, throws a
ReferenceError: window is not defined the moment Next.js
renders that component on the server — which it does for every page, App
Router or Pages Router, before any client-side JavaScript runs at all.
App Router
useKeybind only ever touches the DOM inside a
useEffect, which the App Router's server components can't
run — so the component using it needs the
"use client" directive, same as any other hook:
"use client";
import { useKeybind } from "bind-keyboard/react";
export function EditorShortcuts({ onSave }: { onSave: () => void }) {
useKeybind("cmdOrCtrl+s", (event) => {
event.preventDefault();
onSave();
});
return null; // a pure "wire up shortcuts" component, renders nothing
}
Render it from a server component like any other client component —
EditorShortcuts itself is the only part that needs the
directive.
Pages Router
Pages Router components already run their useEffects
client-side only, no directive needed — the same hook call works
directly inside a page or a layout-level component:
import type { AppProps } from "next/app";
import { useKeybind } from "bind-keyboard/react";
import { useRouter } from "next/router";
export default function App({ Component, pageProps }: AppProps) {
const router = useRouter();
useKeybind("cmdOrCtrl+k", () => router.push("/search"));
return <Component {...pageProps} />;
}
If you're not using the React hook
Constructing a plain new BindKeyboard() instance is also
safe during SSR on its own terms — with the default
autostart: true, the constructor checks whether its target
can actually listen (addEventListener exists) before
attaching anything, and warns instead of throwing when it can't. Still,
prefer constructing it inside a useEffect (or, better, just
use useKeybind — see the
React hook recipe) rather than at module
scope, so it's never even attempted server-side in the first place.
Related
React keyboard shortcuts: the useKeybind hook · Building a Cmd/Ctrl+K command palette