A library of custom React hooks for simplified state management and streamlined component logic.
npm install nextjs-app-hooksA comprehensive library of React hooks for Next.js applications(app router compatible), designed to simplify state management and streamline component logic.



- 📱 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
``bashnpm
npm install nextjs-app-hooks
Contributing
Contributions are welcome! Here's how you can help improve this library:
$3
1. Fork the repository
2. Clone your fork:
git clone https://github.com/your-username/nextjs-app-hooks.git
3. Install dependencies: npm install
4. Create a branch for your changes: git checkout -b feature/your-feature-name$3
- 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$3
- Write tests for your hooks in the
__tests__ directory
- Run tests with npm test or npm run test:watch
- Ensure all tests pass before submitting a PR$3
1. Update documentation for any new hooks or changes to existing ones
2. Make sure your code passes all tests and linting
3. Submit a PR with a clear description of the changes and any relevant issue numbers
4. Wait for review and address any feedback
$3
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:
`jsx
"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
$3
| 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 |$3
| 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 |$3
| 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 |$3
| 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
$3
####
useIsBrowser`tsx
function useIsBrowser(): boolean;
`Determines if the code is running in the browser or on the server.
Example:
`jsx
"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 (
{isBrowser ? "Browser APIs are available" : "Running on the server"}
);
}
`####
useIsServer`tsx
function useIsServer(): boolean;
`Similar to
useIsBrowser but returns true when running on the server.Example:
`jsx
"use client";import { useIsServer } from "nextjs-app-hooks";
export default function MyComponent() {
const isServer = useIsServer();
return
{isServer ? "Rendering on server" : "Rendering on client"};
}
`####
useHasRendered`tsx
function useHasRendered(): boolean;
`Returns false on first render and true afterward, allowing for logic that should only run after initial render.
Example:
`jsx
"use client";import { useHasRendered } from "nextjs-app-hooks";
export default function MyComponent() {
const hasRendered = useHasRendered();
return (
{!hasRendered ? "First render" : "Component has rendered before"}
);
}
`$3
####
useBattery`tsx
function useBattery(): BatteryHookState;
`Access and monitor the device's battery status.
Example:
`jsx
"use client";import { useBattery, formatBatteryTime } from "nextjs-app-hooks";
export default function BatteryStatus() {
const { battery, isSupported, isLoading, error } = useBattery();
if (!isSupported) return
Battery API is not supported on this device
;
if (isLoading) return Loading battery information...
;
if (error) return Error: {error.message}
;
if (!battery) return No battery information available
; return (
Battery Status
Level: {(battery.level * 100).toFixed(0)}%
Charging: {battery.charging ? "Yes" : "No"}
{battery.charging ? (
Full in: {formatBatteryTime(battery.chargingTime)}
) : (
Time remaining: {formatBatteryTime(battery.dischargingTime)}
)}
);
}
`####
useClipboard`tsx
function useClipboard(options?: UseClipboardOptions): UseClipboardReturn;
`Copy text to clipboard with status tracking.
Example:
`jsx
"use client";import { useClipboard } from "nextjs-app-hooks";
export default function CopyButton() {
const { copyToClipboard, isSuccess, status } = useClipboard();
return (
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"}
);
}
`####
useCookie`tsx
function useCookie(
key: string,
options?: UseCookieOptions
): UseCookieReturn;
`Manage browser cookies with advanced features.
Example:
`jsx
"use client";import { useCookie } from "nextjs-app-hooks";
export default function CookieConsent() {
const {
value: consent,
setCookie,
removeCookie,
hasValue,
} = useCookie("cookie-consent");
return (
{!hasValue && (
We use cookies to enhance your experience.
onClick={() =>
setCookie("accepted", { maxAge: 60 60 24 * 365 })
}
>
Accept
)} {hasValue && (
)}
);
}
`####
useGeolocation`tsx
function useGeolocation(options?: UseGeolocationOptions): UseGeolocationReturn;
`Access and track the device's geolocation.
Example:
`jsx
"use client";import { useGeolocation } from "nextjs-app-hooks";
export default function LocationComponent() {
const { position, error, isLoading, getPosition, isSupported } =
useGeolocation({ enableHighAccuracy: true });
if (!isSupported) {
return
Geolocation is not supported in your browser.
;
} return (
{position && (
Latitude: {position.latitude}
Longitude: {position.longitude}
Accuracy: {position.accuracy} meters
)} {error &&
Error: {error.message}
}
);
}
`####
useLocalStorage`tsx
function useLocalStorage(
key: string,
options?: UseLocalStorageOptions
): UseLocalStorageReturn;
`Manage localStorage with SSR support.
Example:
`jsx
"use client";import { useLocalStorage } from "nextjs-app-hooks";
export default function UserPreferences() {
const { value: theme, setValue: setTheme } = useLocalStorage("theme", {
defaultValue: "light",
});
return (
Current theme: {theme}
);
}
`####
useSessionStorage`tsx
function useSessionStorage(
key: string,
options?: UseSessionStorageOptions
): UseSessionStorageReturn;
`Manage sessionStorage with SSR support.
Example:
`jsx
"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 (
);
}
`####
useNetwork`tsx
function useNetwork(options?: UseNetworkOptions): UseNetworkReturn;
`Monitor network connection status.
Example:
`jsx
"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 (
Network Status
Status: {network.online ? "Online" : "Offline"}
Connection: {network.connection.type}
Speed: {network.connection.effectiveType}
{!network.online && (
)}
);
}
`####
usePermission`tsx
function usePermission(
name: PermissionName,
options?: UsePermissionOptions
): UsePermissionReturn;
`Check and request browser permissions.
Example:
`jsx
"use client";import { usePermission } from "nextjs-app-hooks";
export default function CameraComponent() {
const { state, isGranted, request, error } = usePermission("camera");
return (
Camera permission: {state}
{!isGranted && (
)}
{isGranted &&
Camera is available} {error &&
Error: {error.message}
}
);
}
`####
usePreferredLanguage`tsx
function usePreferredLanguage(
options?: UsePreferredLanguageOptions
): UsePreferredLanguageReturn;
`Detect and manage user's preferred language.
Example:
`jsx
"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 (
);
}
`$3
####
useClickOutside`tsx
function useClickOutside(
onClickOutside: (event: MouseEvent | TouchEvent) => void,
options?: UseClickOutsideOptions
): RefObject;
`Detect clicks outside of a specified element.
Example:
`jsx
"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 (
{children}
);
}
`####
useDebounce`tsx
function useDebounce any>(
fn: T,
options?: UseDebounceOptions
): [
(...args: Parameters) => void,
() => void,
boolean,
() => ReturnType | undefined
];function useDebounceValue(
value: T,
delay?: number,
options?: Omit
): T;
`Create debounced functions and values.
Example:
`jsx
"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 (
type="text"
value={searchTerm}
onChange={handleInputChange}
placeholder="Search..."
/>
{isPending && Searching...}
);
}
`####
useHover`tsx
function useHover(
options?: UseHoverOptions
): UseHoverReturn;
`Track hover state of an element.
Example:
`jsx
"use client";import { useHover } from "nextjs-app-hooks";
export default function HoverCard() {
const { hoverRef, isHovered } =
useHover <
HTMLDivElement >
{
enterDelay: 100,
leaveDelay: 300,
};
return (
card ${isHovered ? "card-hovered" : ""}}>
Hover over me!
{isHovered && Hello there!}
);
}
`####
useIdle`tsx
function useIdle(options?: UseIdleOptions): UseIdleReturn;
`Track user idle/active state.
Example:
`jsx
"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 (
User is currently {isIdle ? "idle" : "active"}
Time since last activity: {Math.floor(idleTime / 1000)}s
Time until idle: {Math.ceil(remainingTime / 1000)}s
{isIdle && }
);
}
`####
useIntersectionObserver`tsx
function useIntersectionObserver(
options?: UseIntersectionObserverOptions
): UseIntersectionObserverReturn;// Simplified version
function useInView(
options?: UseInViewOptions
): UseInViewReturn;
`Track element visibility using the Intersection Observer API.
Example:
`jsx
"use client";import { useInView } from "nextjs-app-hooks";
export default function LazyImage() {
const { ref, inView } = useInView({
threshold: 0.1,
triggerOnce: true,
});
return (
{inView ? (

) : (
)}
);
}
`####
useLockBodyScroll`tsx
function useLockBodyScroll(
initialLocked?: boolean,
options?: UseLockBodyScrollOptions
): UseLockBodyScrollReturn;
`Lock body scrolling for modals, drawers, etc.
Example:
`jsx
"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 (
{children}
);
}
`####
useLongPress`tsx
function useLongPress(
callback?: ((e: React.MouseEvent | React.TouchEvent) => void) | null,
options?: UseLongPressOptions
): UseLongPressReturn;
`Detect long press gestures.
Example:
`jsx
"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 (
);
}
`####
useMediaQuery`tsx
function useMediaQuery(
features: MediaQueryFeatures | string,
options?: UseMediaQueryOptions
): UseMediaQueryReturn;
`Respond to media queries with extensive options.
Example:
`jsx
"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 (
{isMobile.matches && }
{isTablet.matches && }
{isDesktop.matches && }
);
}
`####
useMouse`tsx
function useMouse(options?: UseMouseOptions): UseMouseReturn;
`Track mouse position and state.
Example:
`jsx
"use client";import { useMouse } from "nextjs-app-hooks";
export default function MouseTracker() {
const mouse = useMouse();
return (
Mouse position: {mouse.position.x}, {mouse.position.y}
Left button: {mouse.buttons.left ? "Pressed" : "Released"}
Inside element: {mouse.isInside ? "Yes" : "No"}
);
}
`Advanced Examples
$3
`jsx
"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 (
Theme Settings
onClick={() => handleThemeChange("light")}
className={theme === "light" ? "active" : ""}
>
Light
onClick={() => handleThemeChange("dark")}
className={theme === "dark" ? "active" : ""}
>
Dark
onClick={() => handleThemeChange("system")}
className={theme === "system" ? "active" : ""}
>
System ({systemPrefersDark ? "Dark" : "Light"})
Currently using: {isDark ? "Dark" : "Light"} theme
);
}
`$3
`jsx
"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 (
Thank you for your message!
We'll get back to you soon.
);
} return (
);
}
``