codedsaif-react-hooks
Version:
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
400 lines (367 loc) • 13.3 kB
Markdown
# React Hooks
A collection of useful React custom hooks for common functionalities.
## Installation
```bash
npm install codedsaif-react-hooks
```
## Available Hooks
### useClickOutside
A lightweight React hook to detect clicks outside a referenced component. Perfect for dropdowns, modals, and popovers.
```jsx
/**
* Custom hook that triggers a callback when a user clicks outside the referenced element.
* @param {Function} callback - The function to call when clicking outside the element.
* @returns {React.RefObject} - A ref that should be attached to the element you want to monitor.
* @example
* function Dropdown() {
* const [isOpen, setIsOpen] = useState(false);
* const dropdownRef = useClickOutside(() => setIsOpen(false));
* return (
* <div>
* <button onClick={() => setIsOpen(!isOpen)}>Toggle Menu</button>
* {isOpen && (
* <ul ref={dropdownRef}>
* <li>Option 1</li>
* <li>Option 2</li>
* <li>Option 3</li>
* </ul>
* )}
* </div>
* );
*/
```
### useDebounce
Debounces a value with a specified delay.
```jsx
/**
* Custom hook to debounce a value after a specified delay.
* @param {*} value - The value to debounce.
* @param {number} [delay=500] - Delay in milliseconds before updating the debounced value.
* @returns {*} - The debounced value.
* @example
* const [searchTerm, setSearchTerm] = useState("");
* const debouncedSearch = useDebounce(searchTerm, 300);
* useEffect(() => {
* if (debouncedSearch) {
* fetch(`https://api.example.com/search?q=${debouncedSearch}`)
* .then((res) => res.json())
* .then((data) => console.log(data));
* }
* }, [debouncedSearch]);
* return (
* <input
* type="text"
* placeholder="Search..."
* onChange={(e) => setSearchTerm(e.target.value)}
* />
* );
*/
```
### useFetch
Simplifies data fetching with loading and error states.
```jsx
/**
* Custom hook to fetch data from an API.
* @param {string} url - The URL to fetch data from.
* @param {object} options - Optional fetch options (headers, method, etc.).
* @returns {{ data: any, loading: boolean, error: string|null }} - The fetched data, loading state, and error message.
* @example
* const { data, loading, error } = useFetch("https://api.example.com/users");
* if (loading) return <p>Loading...</p>;
* if (error) return <p>Error: {error}</p>;
* return (
* <ul>
* {data.map(user => (
* <li key={user.id}>{user.name}</li>
* ))}
* </ul>
* );
*/
```
### useLocalStorage
Persists state in localStorage with automatic JSON parsing/stringifying.
```jsx
/**
* Custom hook to persist state in localStorage.
* @param {string} key - The localStorage key to store the value under.
* @param {*} initialValue - The initial value to use if none is found in localStorage.
* @returns {[any, function]} - Returns the stored value and a setter function.
* @example
* function Dropdown() {
* const [theme, setTheme] = useLocalStorage("theme", "light");
* useEffect(() => {
* document.body.className = theme;
* }, [theme]);
* return (
* <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
* Toggle Theme
* </button>
* );
* }
*/
```
### usePrevious
Tracks the previous value of a state or prop.
```jsx
/**
* Custom hook to get the previous value of a prop or state.
* @param {*} value - The current value to track.
* @returns {*} - The previous value before the current render.
* @example
* const [count, setCount] = useState(0);
* const prevCount = usePrevious(count);
* return (
* <>
* <p>Current: {count}, Previous: {prevCount}</p>
* <button onClick={() => setCount(count + 1)}>Increase</button>
* </>
* );
*/
```
### useTheme
Manages theme switching with localStorage persistence.
```jsx
/**
* Custom hook to manage dark and light theme modes,
* persisting the theme preference in localStorage.
* @returns {[string, function]} - Current theme and a toggle function.
* @example
* const [theme, toggleTheme] = useDarkMode();
* return (
* <button onClick={toggleTheme}>
* Switch to {theme === "light" ? "Dark" : "Light"} Mode
* </button>
* );
*/
```
### useWindowSize
Tracks window dimensions with responsive updates.
```jsx
/**
* Custom hook to get the current window dimensions.
* @returns {{ width: number, height: number }} - The current width and height of the window.
* @example
* function ResponsiveComponent() {
* const { width } = useWindowSize();
* return (
* <div>
* {width > 1024 ? (
* <h1>Desktop View 🖥️</h1>
* ) : width > 768 ? (
* <h1>Tablet View 📱</h1>
* ) : (
* <h1>Mobile View 📲</h1>
* )}
* </div>
* );
*/
```
### useInfiniteScroll
Supports both automatic callback fetching and manual slicing with scroll.
```jsx
/**
* @param {Function|null} callback - Called when scroll reaches bottom (set null for manual mode)
* @param {Object} options - Configuration options
* @param {React.RefObject} [options.containerRef=null] - Scrollable container ref
* @param {boolean} [options.manual=false] - If true, enables manual mode and returns 'limit'
* @param {number} [options.start=10] - Initial limit in manual mode
* @param {number} [options.pace=10] - Increment step for limit
* @param {number} [options.offset=100] - Distance from bottom to trigger scroll (in px)
* @param {boolean} [options.hasMore=true] - Set false to stop infinite loading when no more data
* @param {boolean} [options.debug=false] - Enable funny console logs
*
* @returns {[boolean, Function]|number}
* - If manual=false: returns [isFetching, setIsFetching]
* - If manual=true: returns limit (number)
*
* Example: Auto-fetch Mode (Default)
* -------------------------------------
* const containerRef = useRef(null);
* const [data, setData] = useState([]);
* const [page, setPage] = useState(1);
* const [hasMore, setHasMore] = useState(true);
* const fetchMore = async () => {
* const res = await fetch(`https://medrum.herokuapp.com/feeds/?source=5718e53e7a84fb1901e05971&page=${page}&sort=latest`);
* const json = await res.json();
* if (json.length === 0) setHasMore(false);
* // OR if(json.count === data.length) setHasMore(false);
* else {
* setData(prev => [...prev, ...json]);
* setPage(prev => prev + 1);
* }
* };
* const [isFetching, setIsFetching] = useInfiniteScroll(fetchMore, { containerRef, hasMore });
*
* Example: Manual Slice Mode
* ------------------------------
* const containerRef = useRef(null);
* const [data, setData] = useState([]);
* const [page, setPage] = useState(1);
* const [hasMore, setHasMore] = useState(true);
* const limit = useInfiniteScroll(null, { containerRef, manual: true, start: 10, pace: 10, hasMore });
* const fetchMore = async () => {
* const res = await fetch(`https://medrum.herokuapp.com/feeds/?source=5718e53e7a84fb1901e05971&page=${page}&sort=latest`);
* const json = await res.json();
* if (json.length === 0) setHasMore(false);
* else {
* setData(prev => [...prev, ...json]);
* setPage(prev => prev + 1);
* }
* };
* useEffect(() => {
* fetchMore();
* }, [limit]);
* return (
* <ul ref={containerRef} style={{ height: "400px", overflowY: "scroll" }}>
* {data.slice(0, limit).map(item => (
* <li key={item.id}>{item.title}</li>
* ))}
* </ul>
* );
*/
```
### useInterval
Runs a callback at a specified interval with support for pausing, debugging, initial delay, and external control.
```jsx
/**
* @param {Function} callback - Function to run at each interval tick.
* @param {number|null} delay - Interval duration in ms. Use `null` to pause.
* @param {Object} [options={}] - Additional options.
* @param {boolean} [options.debug=false] - Log lifecycle events to the console.
* @param {boolean} [options.stop=false] - Stops interval immediately when set to true.
* @param {boolean} [options.immediate=false] - Fires callback immediately on mount.
* @param {number} [options.initialDelay] - Wait this long before starting interval loop.
* @param {number} [options.maxTicks] - Stop after this many ticks.
*
* @returns {
* count: number,
* reset: () => void,
* forceTick: () => void,
* pause: () => void,
* resume: () => void,
* }
*
* @example
* // Logs "Tick" every second
* useInterval(() => console.log('Tick'), 1000);
*
* @example
* const { count, reset } = useInterval(() => {
* console.log('Tick!');
* }, 1000);
*
* @example
* // With pause/resume capability
* const [delay, setDelay] = useState(1000);
* <button onClick={() => setDelay(prev => prev ? null : 1000)}>Toggle</button>
* useInterval(() => console.log('Toggling...'), delay);
*
* @example
* function CountDown({ initialCount, downLimit, initialDelay = 1000, bgColor = 'transparent' }) {
* const [count, setCount] = useState(initialCount);
* const [delay, setDelay] = useState(null);
* useInterval(() => {
* const newValue = count - 1;
* setCount(newValue)
* if (newValue <= downLimit) setDelay(null);
* }, delay);
* return (
* <div style={{ display: 'flex', backgroundColor: bgColor }}>{count}
* <button onClick={() => setDelay(initialDelay)}>Start</button>
* <button onClick={() => setDelay(null)}>Stop</button>
* </div>
* )
* }
*
* @example
* import React, { useState, useCallback } from 'react';
* import useInterval from './useInterval';
* const ShoppingList = () => {
* // Wait 5 seconds before fetching new data
* const POLL_DELAY = 5000;
* const [items, setItems] = useState([]);
* const fetchItems = useCallback(async () => {
* const response = await fetch('/shopping-list/items');
* const json = await response.json();
* setItems(json);
* }, []);
* useEffect(() => {
* // Fetch items from API on mount
* fetchItems();
* }, []);
* useInterval(() => {
* fetchItems();
* }, POLL_DELAY);
* return (
* <ul>
* {items.map((item) => <li>{item.title}</li>)}
* </ul>
* )
* };
*/
```
### useKeyPress
A powerful custom React hook for handling keyboard interactions.
```jsx
/**
* Features:
* - Detect and respond to individual key presses
* - Match specific target key (e.g. 'a')
* - Maintain history of pressed keys
* - Detect modifier key states: Shift, Alt, Ctrl, Meta
* - Detect multi-key combos like 'ctrl+s', 'cmd+z'
* - Pause and resume key tracking
* - Optionally ignore key presses in input, textarea, or contentEditable fields
*
* @param {Object} options - Configuration options
* @param {Function} [options.onKeyPress] - Function to run when a valid key is pressed
* @param {string} [options.targetKey] - Key to match for triggering `keyMatched` (case-insensitive)
* @param {boolean} [options.enableHistory=true] - Whether to track history of pressed keys
* @param {number} [options.maxHistory=10] - Max number of keys to store in history
* @param {boolean} [options.ignoreInput=false] - If true, ignores key presses from editable elements (input, textarea, contentEditable)
* @param {string[]} [options.combos=[]] - List of keyboard combos to detect (e.g. ['ctrl+s', 'cmd+z'])
*
* @returns {Object} Hook output
* @returns {string|null} keyPressed - The current key being pressed (null if none)
* @returns {boolean} keyMatched - True if the most recent key matched `targetKey`
* @returns {string[]} keyHistory - Array of previously pressed keys (up to `maxHistory`)
* @returns {string|null} comboMatched - The matched combo string, if any (e.g. 'ctrl+s')
* @returns {Object} modifiers - Current modifier key states { shift, alt, ctrl, meta }
* @returns {Function} reset - Clears the keyMatched, keyHistory, and comboMatched states
* @returns {Function} setPaused - Function to pause/resume the hook (use `setPaused(true)` to pause)
*
* @example
* // Basic key detection and match tracking
* const { keyPressed, keyMatched, keyHistory } = useKeyPress({
* onKeyPress: (key) => console.log('You pressed:', key),
* targetKey: 'a',
* enableHistory: true,
* maxHistory: 5,
* });
*
* @example
* // Full usage with modifiers, combo detection, pause and reset
* const {
* keyPressed,
* keyMatched,
* keyHistory,
* comboMatched,
* modifiers,
* reset,
* setPaused,
* } = useKeyPress({
* onKeyPress: (key) => console.log('Key:', key),
* targetKey: 's',
* combos: ['ctrl+s', 'cmd+z'],
* ignoreInput: true,
* });
*
* useEffect(() => {
* if (comboMatched === 'ctrl+s') {
* console.log('Save triggered!');
* }
* }, [comboMatched]);
*/
```
## License
This package is licensed under the [MIT License](https://opensource.org/licenses/MIT).