This guide covers the utility React hooks provided by maneuver-core. These are general-purpose hooks for responsive design, state management, navigation, and keyboard shortcuts.
For a complete list of all hooks, see HOOKS_REFERENCE.md.
Detects if the viewport is below the 2xl breakpoint (1536px).
import { useIsMobile } from '@maneuver-core/hooks';
function MyComponent() {
const isMobile = useIsMobile();
return (
<div>
{isMobile ? (
<MobileView />
) : (
<DesktopView />
)}
</div>
);
}Returns: boolean - true if viewport < 1536px, false otherwise
Detects if the app is running as an installed PWA.
import { usePWA } from '@maneuver-core/hooks';
function MyComponent() {
const isPWA = usePWA();
return (
<div>
{isPWA ? (
<p>Running as installed PWA</p>
) : (
<p>Running in browser</p>
)}
</div>
);
}Returns: boolean - true if installed as PWA, false otherwise
Detection methods:
- Checks
display-mode: standalonemedia query - Checks
display-mode: fullscreenmedia query - Checks
navigator.standalone(iOS)
Generic media query hook for responsive design.
import { useMediaQuery, mediaQueries } from '@maneuver-core/hooks';
function MyComponent() {
// Custom query
const isWideScreen = useMediaQuery('(min-width: 1920px)');
// Using presets
const isDarkMode = useMediaQuery(mediaQueries.darkMode);
const isPortrait = useMediaQuery(mediaQueries.portrait);
const isRetina = useMediaQuery(mediaQueries.retina);
return <div>Wide screen: {isWideScreen ? 'Yes' : 'No'}</div>;
}Parameters:
query(string) - CSS media query string
Returns: boolean - true if query matches, false otherwise
Available presets:
mediaQueries = {
// Tailwind breakpoints
sm: '(min-width: 640px)',
md: '(min-width: 768px)',
lg: '(min-width: 1024px)',
xl: '(min-width: 1280px)',
'2xl': '(min-width: 1536px)',
// Device types
mobile: '(max-width: 767px)',
tablet: '(min-width: 768px) and (max-width: 1023px)',
desktop: '(min-width: 1024px)',
// Orientation
portrait: '(orientation: portrait)',
landscape: '(orientation: landscape)',
// Display modes
standalone: '(display-mode: standalone)',
fullscreen: '(display-mode: fullscreen)',
// Preferences
darkMode: '(prefers-color-scheme: dark)',
lightMode: '(prefers-color-scheme: light)',
reducedMotion: '(prefers-reduced-motion: reduce)',
// High DPI
retina: '(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi)',
}Convenience hooks:
import {
useIsDarkMode,
useIsPortrait,
useIsRetina,
usePrefersReducedMotion
} from '@maneuver-core/hooks';
const isDark = useIsDarkMode();
const isPortrait = useIsPortrait();
const isRetina = useIsRetina();
const prefersReducedMotion = usePrefersReducedMotion();Persist state to localStorage with automatic JSON serialization.
import { useLocalStorage } from '@maneuver-core/hooks';
function MyComponent() {
const [name, setName, removeName] = useLocalStorage('user-name', 'Anonymous');
return (
<div>
<input
value={name}
onChange={(e) => setName(e.target.value)}
/>
<button onClick={removeName}>Clear</button>
</div>
);
}Parameters:
key(string) - localStorage keyinitialValue(T) - Default value if key doesn't exist
Returns: [T, (value: T | ((val: T) => T)) => void, () => void]
[0]- Current value[1]- Set value function (same API asuseState)[2]- Remove value function
Features:
- Automatic JSON serialization/deserialization
- Syncs across tabs/windows via
storageevent - SSR-safe (returns initialValue on server)
- Error handling with console warnings
Debounce a value to prevent excessive updates.
import { useDebounce } from '@maneuver-core/hooks';
function SearchInput() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 500);
useEffect(() => {
// This only runs 500ms after user stops typing
if (debouncedSearchTerm) {
searchAPI(debouncedSearchTerm);
}
}, [debouncedSearchTerm]);
return (
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
);
}Parameters:
value(T) - Value to debouncedelay(number) - Delay in milliseconds (default: 500)
Returns: T - Debounced value
useDebouncedCallback - Debounce a callback function:
import { useDebouncedCallback } from '@maneuver-core/hooks';
function AutoSaveForm() {
const debouncedSave = useDebouncedCallback((data) => {
saveToServer(data);
}, 1000);
return (
<textarea onChange={(e) => debouncedSave(e.target.value)} />
);
}Use cases:
- Search inputs
- Form validation
- Auto-save
- Window resize handlers
- API calls
Prompt user before navigating away with unsaved data.
import { useNavigationConfirm } from '@maneuver-core/hooks';
function FormPage() {
const [hasUnsavedData, setHasUnsavedData] = useState(false);
useNavigationConfirm({
when: hasUnsavedData,
message: 'You have unsaved changes. Are you sure you want to leave?',
onConfirm: (message) => {
// Custom confirmation (optional)
return confirm(message);
}
});
return (
<form onChange={() => setHasUnsavedData(true)}>
{/* form fields */}
</form>
);
}Parameters:
interface UseNavigationConfirmOptions {
when: boolean; // Whether to block navigation
message?: string; // Confirmation message
onConfirm?: (message: string) => boolean; // Custom confirm function
}Returns:
{
shouldBlock: boolean; // Current blocking state
confirmNavigation: () => boolean; // Manually prompt user
}Blocks:
- React Router navigation (via
useBlocker) - Browser navigation (via
beforeunloadevent) - Back button, refresh, closing tab
Detect online/offline network status (critical for offline-first PWAs).
import { useOnlineStatus } from '@maneuver-core/hooks';
function NetworkIndicator() {
const isOnline = useOnlineStatus();
return (
<div className={isOnline ? 'text-green-500' : 'text-red-500'}>
{isOnline ? 'Online' : 'Offline'}
</div>
);
}Returns: boolean - true if online, false if offline
useOnlineStatusWithCallback - With status change callbacks:
import { useOnlineStatusWithCallback } from '@maneuver-core/hooks';
function MyComponent() {
const isOnline = useOnlineStatusWithCallback(
() => {
console.log('Connection restored!');
syncDataToServer();
},
() => {
console.log('Connection lost!');
showOfflineMessage();
}
);
return <div>Status: {isOnline ? 'Online' : 'Offline'}</div>;
}Parameters:
onOnline(function) - Called when connection is restoredonOffline(function) - Called when connection is lost
Register keyboard shortcuts for your app.
import { useKeyboardShortcut, shortcuts } from '@maneuver-core/hooks';
function MyComponent() {
const [isOpen, setIsOpen] = useState(false);
// Ctrl+K (or Cmd+K on Mac) to open search
useKeyboardShortcut(
() => setIsOpen(true),
{ key: 'k', ctrl: true }
);
// Escape to close
useKeyboardShortcut(
() => setIsOpen(false),
{ key: 'Escape', enabled: isOpen }
);
// Using presets
useKeyboardShortcut(
handleSave,
shortcuts.save // Ctrl+S
);
return <SearchModal open={isOpen} />;
}Parameters:
interface KeyboardShortcutOptions {
key: string; // Key to listen for (e.g., 'k', 'Enter')
ctrl?: boolean; // Require Ctrl (Cmd on Mac)
shift?: boolean; // Require Shift
alt?: boolean; // Require Alt (Option on Mac)
enabled?: boolean; // Enable the shortcut (default: true)
preventDefault?: boolean; // Prevent default (default: true)
}Available presets:
shortcuts = {
// Navigation
escape: { key: 'Escape' },
enter: { key: 'Enter' },
// Search
search: { key: 'k', ctrl: true },
// Actions
save: { key: 's', ctrl: true },
undo: { key: 'z', ctrl: true },
redo: { key: 'z', ctrl: true, shift: true },
// App navigation
home: { key: 'h', ctrl: true },
settings: { key: ',', ctrl: true },
// Data entry
submitForm: { key: 'Enter', ctrl: true },
}Common shortcuts:
Ctrl+K/Cmd+K- SearchCtrl+S/Cmd+S- SaveCtrl+Z/Cmd+Z- UndoCtrl+Shift+Z/Cmd+Shift+Z- RedoEscape- Close modal/dialog
Many of these hooks work great together:
function ScoutingForm() {
const [formData, setFormData] = useLocalStorage('scouting-form', {});
const [isDirty, setIsDirty] = useState(false);
const isOnline = useOnlineStatus();
const isMobile = useIsMobile();
// Prompt before leaving with unsaved data
useNavigationConfirm({
when: isDirty && isOnline,
message: 'You have unsaved data. Leave anyway?'
});
// Ctrl+S to save
useKeyboardShortcut(
() => handleSave(formData),
{ key: 's', ctrl: true, enabled: isDirty }
);
return (
<form>
{!isOnline && (
<div className="bg-yellow-100 p-2">
Offline mode - data will sync when connected
</div>
)}
{/* form fields */}
</form>
);
}- useLocalStorage - Always provide a sensible
initialValue - useDebounce - Choose delay based on use case (search: 300-500ms, autosave: 1000-2000ms)
- useNavigationConfirm - Only enable when there's actually unsaved data
- useOnlineStatus - Show clear indicators to users about their connection status
- useKeyboardShortcut - Document shortcuts in your UI (tooltips, help menu)
- useMediaQuery - Use Tailwind CSS classes where possible; hooks for complex logic only
Framework Philosophy: Hooks should be generic, well-tested, and solve common problems. Game-specific hooks belong in your game implementation, not in the framework.