nextjs-app-hooks v1.8.0
NextJS App Hooks
A comprehensive library of React hooks for Next.js applications(app router compatible), designed to simplify state management and streamline component logic.
Features
- 📱 Full TypeScript support with comprehensive type definitions
- 🔄 SSR compatible with proper hydration support for Next.js
- 🎯 Focused utilities for common UI patterns and browser APIs
- 🧩 Modular design allowing for tree-shaking to minimize bundle size
- 📚 Comprehensive documentation with examples for each hook
- ⚡ Performance optimized implementations
Installation
# npm
npm install nextjs-app-hooks
# yarn
yarn add nextjs-app-hooks
# pnpm
pnpm add nextjs-app-hooks
Contributing
Contributions are welcome! Here's how you can help improve this library:
Setup
- Fork the repository
- Clone your fork:
git clone https://github.com/your-username/nextjs-app-hooks.git
- Install dependencies:
npm install
- Create a branch for your changes:
git checkout -b feature/your-feature-name
Development
- Run
npm run dev
to start the development build with watch mode - Follow the existing code style and TypeScript patterns
- Add appropriate JSDoc comments and type definitions
- Ensure your changes are properly typed and exports are added to
index.ts
Testing
- Write tests for your hooks in the
__tests__
directory - Run tests with
npm test
ornpm run test:watch
- Ensure all tests pass before submitting a PR
Pull Requests
- Update documentation for any new hooks or changes to existing ones
- Make sure your code passes all tests and linting
- Submit a PR with a clear description of the changes and any relevant issue numbers
- Wait for review and address any feedback
Adding a New Hook
When adding a new hook, please ensure it:
- Has a clear, focused purpose
- Works with SSR and Next.js App Router
- Includes comprehensive TypeScript definitions
- Includes detailed JSDoc comments with examples
- Is exported from the main index file
- Follows the naming conventions of existing hooks
Thank you for helping improve nextjs-app-hooks!
Usage
All hooks are exported from the package root:
"use client";
import { useIsBrowser, useDarkMode, useLocalStorage } from "nextjs-app-hooks";
function MyComponent() {
const isBrowser = useIsBrowser();
const isDarkMode = useDarkMode();
const { value, setValue } = useLocalStorage("user-settings", {
defaultValue: { theme: "light" },
});
// Your component logic
}
Hooks Overview
Environment Detection
Hook | Description |
---|---|
useIsBrowser | Detect if code is running in browser or server environment |
useIsServer | Detect if code is running on the server |
useHasRendered | Track whether component has rendered at least once |
Browser API Access
Hook | Description |
---|---|
useBattery | Access and monitor device battery status |
useClipboard | Copy text to clipboard with status tracking |
useCookie | Manage browser cookies with advanced features |
useGeolocation | Access and track device geolocation |
useLocalStorage | Manage localStorage with SSR support |
useSessionStorage | Manage sessionStorage with SSR support |
useNetwork | Monitor network connection status |
usePermission | Check and request browser permissions |
usePreferredLanguage | Detect and manage user's preferred language |
UI and Interaction
Hook | Description |
---|---|
useClickOutside | Detect clicks outside of a specified element |
useDebounce | Create debounced functions and values |
useHover | Track hover state of an element |
useIdle | Track user idle/active state |
useIntersectionObserver | Track element visibility using Intersection Observer |
useLockBodyScroll | Lock body scrolling for modals and drawers |
useLongPress | Detect long press gestures |
useMediaQuery | Respond to media queries with extensive options |
useMouse | Track mouse position and state |
Media Hooks
Hook | Description |
---|---|
useDarkMode | Check if the screen is in dark mode |
usePrefersReducedMotion | Check if the user prefers reduced motion |
useOrientation | Check current screen orientation |
useResponsive | Convenience hook for responsive design |
Detailed API Documentation
Environment Detection
useIsBrowser
function useIsBrowser(): boolean;
Determines if the code is running in the browser or on the server.
Example:
"use client";
import { useIsBrowser } from "nextjs-app-hooks";
export default function MyComponent() {
const isBrowser = useIsBrowser();
useEffect(() => {
if (isBrowser) {
// Safe to use browser APIs like localStorage, window, etc.
localStorage.setItem("visited", "true");
}
}, [isBrowser]);
return (
<div>
{isBrowser ? "Browser APIs are available" : "Running on the server"}
</div>
);
}
useIsServer
function useIsServer(): boolean;
Similar to useIsBrowser
but returns true when running on the server.
Example:
"use client";
import { useIsServer } from "nextjs-app-hooks";
export default function MyComponent() {
const isServer = useIsServer();
return <div>{isServer ? "Rendering on server" : "Rendering on client"}</div>;
}
useHasRendered
function useHasRendered(): boolean;
Returns false on first render and true afterward, allowing for logic that should only run after initial render.
Example:
"use client";
import { useHasRendered } from "nextjs-app-hooks";
export default function MyComponent() {
const hasRendered = useHasRendered();
return (
<div>{!hasRendered ? "First render" : "Component has rendered before"}</div>
);
}
Browser API Hooks
useBattery
function useBattery(): BatteryHookState;
Access and monitor the device's battery status.
Example:
"use client";
import { useBattery, formatBatteryTime } from "nextjs-app-hooks";
export default function BatteryStatus() {
const { battery, isSupported, isLoading, error } = useBattery();
if (!isSupported) return <p>Battery API is not supported on this device</p>;
if (isLoading) return <p>Loading battery information...</p>;
if (error) return <p>Error: {error.message}</p>;
if (!battery) return <p>No battery information available</p>;
return (
<div>
<h2>Battery Status</h2>
<p>Level: {(battery.level * 100).toFixed(0)}%</p>
<p>Charging: {battery.charging ? "Yes" : "No"}</p>
{battery.charging ? (
<p>Full in: {formatBatteryTime(battery.chargingTime)}</p>
) : (
<p>Time remaining: {formatBatteryTime(battery.dischargingTime)}</p>
)}
</div>
);
}
useClipboard
function useClipboard(options?: UseClipboardOptions): UseClipboardReturn;
Copy text to clipboard with status tracking.
Example:
"use client";
import { useClipboard } from "nextjs-app-hooks";
export default function CopyButton() {
const { copyToClipboard, isSuccess, status } = useClipboard();
return (
<button
onClick={() => copyToClipboard("Text to copy")}
className={isSuccess ? "bg-green-500" : "bg-blue-500"}
>
{status === "idle" && "Copy to clipboard"}
{status === "copied" && "Copied!"}
{status === "error" && "Failed to copy"}
</button>
);
}
useCookie
function useCookie<T = string>(
key: string,
options?: UseCookieOptions<T>
): UseCookieReturn<T>;
Manage browser cookies with advanced features.
Example:
"use client";
import { useCookie } from "nextjs-app-hooks";
export default function CookieConsent() {
const {
value: consent,
setCookie,
removeCookie,
hasValue,
} = useCookie("cookie-consent");
return (
<div>
{!hasValue && (
<div className="cookie-banner">
<p>We use cookies to enhance your experience.</p>
<div>
<button
onClick={() =>
setCookie("accepted", { maxAge: 60 * 60 * 24 * 365 })
}
>
Accept
</button>
<button onClick={() => setCookie("declined")}>Decline</button>
</div>
</div>
)}
{hasValue && (
<button onClick={removeCookie}>Reset Cookie Preferences</button>
)}
</div>
);
}
useGeolocation
function useGeolocation(options?: UseGeolocationOptions): UseGeolocationReturn;
Access and track the device's geolocation.
Example:
"use client";
import { useGeolocation } from "nextjs-app-hooks";
export default function LocationComponent() {
const { position, error, isLoading, getPosition, isSupported } =
useGeolocation({ enableHighAccuracy: true });
if (!isSupported) {
return <p>Geolocation is not supported in your browser.</p>;
}
return (
<div>
<button onClick={getPosition} disabled={isLoading}>
{isLoading ? "Getting location..." : "Get My Location"}
</button>
{position && (
<div>
<p>Latitude: {position.latitude}</p>
<p>Longitude: {position.longitude}</p>
<p>Accuracy: {position.accuracy} meters</p>
</div>
)}
{error && <p>Error: {error.message}</p>}
</div>
);
}
useLocalStorage
function useLocalStorage<T>(
key: string,
options?: UseLocalStorageOptions<T>
): UseLocalStorageReturn<T>;
Manage localStorage with SSR support.
Example:
"use client";
import { useLocalStorage } from "nextjs-app-hooks";
export default function UserPreferences() {
const { value: theme, setValue: setTheme } = useLocalStorage("theme", {
defaultValue: "light",
});
return (
<div>
<p>Current theme: {theme}</p>
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
Toggle theme
</button>
</div>
);
}
useSessionStorage
function useSessionStorage<T>(
key: string,
options?: UseSessionStorageOptions<T>
): UseSessionStorageReturn<T>;
Manage sessionStorage with SSR support.
Example:
"use client";
import { useSessionStorage } from "nextjs-app-hooks";
export default function FormWithSessionPersistence() {
const {
value: formData,
setValue: setFormData,
removeValue: clearForm,
} = useSessionStorage("form_data", {
defaultValue: { name: "", email: "", message: "" },
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
const handleSubmit = (e) => {
e.preventDefault();
// Submit form data
console.log("Submitting:", formData);
clearForm();
};
return (
<form onSubmit={handleSubmit}>
<input
name="name"
value={formData.name}
onChange={handleChange}
placeholder="Name"
/>
<input
name="email"
value={formData.email}
onChange={handleChange}
placeholder="Email"
/>
<textarea
name="message"
value={formData.message}
onChange={handleChange}
placeholder="Message"
/>
<button type="submit">Submit</button>
</form>
);
}
useNetwork
function useNetwork(options?: UseNetworkOptions): UseNetworkReturn;
Monitor network connection status.
Example:
"use client";
import { useNetwork } from "nextjs-app-hooks";
export default function NetworkStatus() {
const network = useNetwork({
autoReconnect: true,
onOffline: () => console.log("Connection lost"),
onOnline: () => console.log("Connection restored"),
});
return (
<div className={network.online ? "online" : "offline"}>
<h1>Network Status</h1>
<p>Status: {network.online ? "Online" : "Offline"}</p>
<p>Connection: {network.connection.type}</p>
<p>Speed: {network.connection.effectiveType}</p>
{!network.online && (
<button onClick={network.reconnect} disabled={network.reconnecting}>
{network.reconnecting ? "Reconnecting..." : "Reconnect Manually"}
</button>
)}
</div>
);
}
usePermission
function usePermission(
name: PermissionName,
options?: UsePermissionOptions
): UsePermissionReturn;
Check and request browser permissions.
Example:
"use client";
import { usePermission } from "nextjs-app-hooks";
export default function CameraComponent() {
const { state, isGranted, request, error } = usePermission("camera");
return (
<div>
<p>Camera permission: {state}</p>
{!isGranted && (
<button onClick={request} disabled={state === "denied"}>
{state === "denied"
? "Permission denied (check browser settings)"
: "Request camera access"}
</button>
)}
{isGranted && <div className="camera-view">Camera is available</div>}
{error && <p className="error">Error: {error.message}</p>}
</div>
);
}
usePreferredLanguage
function usePreferredLanguage(
options?: UsePreferredLanguageOptions
): UsePreferredLanguageReturn;
Detect and manage user's preferred language.
Example:
"use client";
import { usePreferredLanguage } from "nextjs-app-hooks";
export default function LanguageSwitcher() {
const { language, setLanguage, resetToSystemDefault } = usePreferredLanguage({
supportedLanguages: ["en", "fr", "es", "de"],
onChange: (lang) => console.log(`Language changed to ${lang}`),
});
const languages = [
{ code: "en", name: "English" },
{ code: "fr", name: "Français" },
{ code: "es", name: "Español" },
{ code: "de", name: "Deutsch" },
];
return (
<div className="language-switcher">
<select value={language} onChange={(e) => setLanguage(e.target.value)}>
{languages.map((lang) => (
<option key={lang.code} value={lang.code}>
{lang.name}
</option>
))}
</select>
<button onClick={resetToSystemDefault}>Reset to System Default</button>
</div>
);
}
UI and Interaction Hooks
useClickOutside
function useClickOutside<T extends HTMLElement = HTMLElement>(
onClickOutside: (event: MouseEvent | TouchEvent) => void,
options?: UseClickOutsideOptions
): RefObject<T | null>;
Detect clicks outside of a specified element.
Example:
"use client";
import { useClickOutside } from "nextjs-app-hooks";
export default function Modal({ isOpen, onClose, children }) {
const modalRef = useClickOutside(() => {
if (isOpen) onClose();
});
if (!isOpen) return null;
return (
<div className="modal-overlay">
<div ref={modalRef} className="modal-content">
{children}
<button onClick={onClose}>Close</button>
</div>
</div>
);
}
useDebounce
function useDebounce<T extends (...args: any[]) => any>(
fn: T,
options?: UseDebounceOptions
): [
(...args: Parameters<T>) => void,
() => void,
boolean,
() => ReturnType<T> | undefined
];
function useDebounceValue<T>(
value: T,
delay?: number,
options?: Omit<UseDebounceOptions, "delay">
): T;
Create debounced functions and values.
Example:
"use client";
import { useDebounce, useDebounceValue } from "nextjs-app-hooks";
export default function SearchInput() {
const [searchTerm, setSearchTerm] = useState("");
const debouncedSearchTerm = useDebounceValue(searchTerm, 300);
// Use debounced value for API calls
useEffect(() => {
if (debouncedSearchTerm) {
searchAPI(debouncedSearchTerm);
}
}, [debouncedSearchTerm]);
// Using debounced function
const searchAPI = async (term) => {
// API call logic
console.log("Searching for:", term);
};
const [debouncedSearch, cancelSearch, isPending] = useDebounce(searchAPI, {
delay: 300,
trackPending: true,
});
const handleInputChange = (e) => {
const value = e.target.value;
setSearchTerm(value);
debouncedSearch(value);
};
return (
<div>
<input
type="text"
value={searchTerm}
onChange={handleInputChange}
placeholder="Search..."
/>
{isPending && <span>Searching...</span>}
</div>
);
}
useHover
function useHover<T extends HTMLElement = HTMLDivElement>(
options?: UseHoverOptions
): UseHoverReturn<T>;
Track hover state of an element.
Example:
"use client";
import { useHover } from "nextjs-app-hooks";
export default function HoverCard() {
const { hoverRef, isHovered } =
useHover <
HTMLDivElement >
{
enterDelay: 100,
leaveDelay: 300,
};
return (
<div ref={hoverRef} className={`card ${isHovered ? "card-hovered" : ""}`}>
Hover over me!
{isHovered && <div className="tooltip">Hello there!</div>}
</div>
);
}
useIdle
function useIdle(options?: UseIdleOptions): UseIdleReturn;
Track user idle/active state.
Example:
"use client";
import { useIdle } from "nextjs-app-hooks";
export default function IdleDetectionExample() {
const { isIdle, reset, idleTime, remainingTime } = useIdle({
idleTime: 60000, // 1 minute
onIdle: () => console.log("User is idle"),
onActive: () => console.log("User is active"),
});
return (
<div>
<p>User is currently {isIdle ? "idle" : "active"}</p>
<p>Time since last activity: {Math.floor(idleTime / 1000)}s</p>
<p>Time until idle: {Math.ceil(remainingTime / 1000)}s</p>
{isIdle && <button onClick={reset}>I'm still here!</button>}
</div>
);
}
useIntersectionObserver
function useIntersectionObserver<T extends Element = HTMLDivElement>(
options?: UseIntersectionObserverOptions
): UseIntersectionObserverReturn<T>;
// Simplified version
function useInView<T extends Element = HTMLDivElement>(
options?: UseInViewOptions
): UseInViewReturn<T>;
Track element visibility using the Intersection Observer API.
Example:
"use client";
import { useInView } from "nextjs-app-hooks";
export default function LazyImage() {
const { ref, inView } = useInView({
threshold: 0.1,
triggerOnce: true,
});
return (
<div ref={ref} className="image-container">
{inView ? (
<img src="https://example.com/image.jpg" alt="Lazy loaded" />
) : (
<div className="placeholder" />
)}
</div>
);
}
useLockBodyScroll
function useLockBodyScroll(
initialLocked?: boolean,
options?: UseLockBodyScrollOptions
): UseLockBodyScrollReturn;
Lock body scrolling for modals, drawers, etc.
Example:
"use client";
import { useLockBodyScroll } from "nextjs-app-hooks";
export default function Modal({ isOpen, onClose, children }) {
// Lock scrolling when modal is open
useLockBodyScroll(isOpen);
if (!isOpen) return null;
return (
<div className="modal-overlay">
<div className="modal-content">
{children}
<button onClick={onClose}>Close</button>
</div>
</div>
);
}
useLongPress
function useLongPress(
callback?: ((e: React.MouseEvent | React.TouchEvent) => void) | null,
options?: UseLongPressOptions
): UseLongPressReturn;
Detect long press gestures.
Example:
"use client";
import { useLongPress } from "nextjs-app-hooks";
export default function LongPressButton() {
const onLongPress = () => alert("Long pressed!");
const { handlers, isLongPressed } = useLongPress(onLongPress, {
delay: 500,
onPressStart: () => console.log("Press started"),
onLongPressEnd: () => console.log("Long press ended"),
});
return (
<button {...handlers} className={isLongPressed ? "active" : ""}>
Press and hold me
</button>
);
}
useMediaQuery
function useMediaQuery(
features: MediaQueryFeatures | string,
options?: UseMediaQueryOptions
): UseMediaQueryReturn;
Respond to media queries with extensive options.
Example:
"use client";
import { useMediaQuery, useDarkMode } from "nextjs-app-hooks";
export default function ResponsiveComponent() {
const isMobile = useMediaQuery({ maxWidth: "sm" });
const isTablet = useMediaQuery({ minWidth: "md", maxWidth: "lg" });
const isDesktop = useMediaQuery({ minWidth: "xl" });
// Detect dark mode
const isDarkMode = useDarkMode();
return (
<div className={isDarkMode ? "dark-theme" : "light-theme"}>
{isMobile.matches && <MobileView />}
{isTablet.matches && <TabletView />}
{isDesktop.matches && <DesktopView />}
</div>
);
}
useMouse
function useMouse(options?: UseMouseOptions): UseMouseReturn;
Track mouse position and state.
Example:
"use client";
import { useMouse } from "nextjs-app-hooks";
export default function MouseTracker() {
const mouse = useMouse();
return (
<div style={{ height: "300px", border: "1px solid black" }}>
<p>
Mouse position: {mouse.position.x}, {mouse.position.y}
</p>
<p>Left button: {mouse.buttons.left ? "Pressed" : "Released"}</p>
<p>Inside element: {mouse.isInside ? "Yes" : "No"}</p>
</div>
);
}
Advanced Examples
Theme Switcher with Persistence
"use client";
import { useState, useEffect } from "react";
import { useDarkMode, useLocalStorage, useMediaQuery } from "nextjs-app-hooks";
export default function ThemeSwitcher() {
// Get OS theme preference
const systemPrefersDark = useDarkMode();
// Load theme from localStorage
const { value: savedTheme, setValue: saveTheme } = useLocalStorage("theme", {
defaultValue: "system",
});
// Derived theme state
const [theme, setTheme] = useState(savedTheme);
const [isDark, setIsDark] = useState(false);
// Update the active theme based on system preference and user selection
useEffect(() => {
const isDarkTheme =
theme === "dark" || (theme === "system" && systemPrefersDark);
setIsDark(isDarkTheme);
// Update document class for CSS theme switching
if (isDarkTheme) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
}, [theme, systemPrefersDark]);
// Handle theme changes
const handleThemeChange = (newTheme) => {
setTheme(newTheme);
saveTheme(newTheme);
};
return (
<div className="theme-switcher">
<h2>Theme Settings</h2>
<div className="theme-options">
<button
onClick={() => handleThemeChange("light")}
className={theme === "light" ? "active" : ""}
>
Light
</button>
<button
onClick={() => handleThemeChange("dark")}
className={theme === "dark" ? "active" : ""}
>
Dark
</button>
<button
onClick={() => handleThemeChange("system")}
className={theme === "system" ? "active" : ""}
>
System ({systemPrefersDark ? "Dark" : "Light"})
</button>
</div>
<div className="current-theme">
Currently using: <strong>{isDark ? "Dark" : "Light"}</strong> theme
</div>
</div>
);
}
Form with Session Persistence and Validation
"use client";
import { useState } from "react";
import { useSessionStorage, useNetwork } from "nextjs-app-hooks";
export default function PersistentForm() {
const {
value: formData,
setValue: setFormData,
removeValue: clearForm,
} = useSessionStorage("contact_form", {
defaultValue: {
name: "",
email: "",
message: "",
},
});
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const network = useNetwork();
const validateForm = () => {
const newErrors = {};
if (!formData.name.trim()) {
newErrors.name = "Name is required";
}
if (!formData.email.trim()) {
newErrors.email = "Email is required";
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = "Email is invalid";
}
if (!formData.message.trim()) {
newErrors.message = "Message is required";
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
// Clear error when user starts typing
if (errors[name]) {
setErrors((prev) => ({ ...prev, [name]: "" }));
}
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateForm()) {
return;
}
if (!network.online) {
alert(
"You are offline. Please try again when you have internet connection."
);
return;
}
setIsSubmitting(true);
try {
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1500));
// Success
setIsSuccess(true);
clearForm();
} catch (error) {
console.error("Error submitting form:", error);
alert("Failed to submit the form. Please try again.");
} finally {
setIsSubmitting(false);
}
};
if (isSuccess) {
return (
<div className="success-message">
<h2>Thank you for your message!</h2>
<p>We'll get back to you soon.</p>
<button onClick={() => setIsSuccess(false)}>
Send another message
</button>
</div>
);
}
return (
<form onSubmit={handleSubmit} className="contact-form">
<div className="form-group">
<label htmlFor="name">Name</label>
<input
id="name"
name="name"
type="text"
value={formData.name}
onChange={handleChange}
className={errors.name ? "error" : ""}
/>
{errors.name && <div className="error-message">{errors.name}</div>}
</div>
<div className="form-group">
<label htmlFor="email">Email</label>
<input
id="email"
name="email"
type="email"
value={formData.email}
onChange={handleChange}
className={errors.email ? "error" : ""}
/>
{errors.email && <div className="error-message">{errors.email}</div>}
</div>
<div className="form-group">
<label htmlFor="message">Message</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleChange}
className={errors.message ? "error" : ""}
rows={4}
/>
{errors.message && (
<div className="error-message">{errors.message}</div>
)}
</div>
<div className="form-actions">
<button type="submit" className="submit-button" disabled={isSubmitting}>
{isSubmitting ? "Sending..." : "Send Message"}
</button>
<button
type="button"
className="clear-button"
onClick={clearForm}
disabled={isSubmitting}
>
Clear Form
</button>
</div>
{!network.online && (
<div className="network-warning">
You appear to be offline. The form will be saved but cannot be
submitted until you're back online.
</div>
)}
</form>
);
}