{"id":22903937,"url":"https://github.com/tracktor/react-utils","last_synced_at":"2026-01-22T10:28:49.430Z","repository":{"id":65798869,"uuid":"600046232","full_name":"Tracktor/react-utils","owner":"Tracktor","description":null,"archived":false,"fork":false,"pushed_at":"2024-11-04T17:23:39.000Z","size":245,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"main","last_synced_at":"2024-12-04T18:09:09.131Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Tracktor.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-02-10T13:14:46.000Z","updated_at":"2024-11-04T17:23:06.000Z","dependencies_parsed_at":"2023-06-02T16:15:24.681Z","dependency_job_id":"768dfae1-72bf-49a4-bf0b-22c02dba7bc9","html_url":"https://github.com/Tracktor/react-utils","commit_stats":{"total_commits":16,"total_committers":1,"mean_commits":16.0,"dds":0.0,"last_synced_commit":"52f3b3a5fc28430edf7df6b74aaa92659373125b"},"previous_names":[],"tags_count":38,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Tracktor%2Freact-utils","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Tracktor%2Freact-utils/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Tracktor%2Freact-utils/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Tracktor%2Freact-utils/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Tracktor","download_url":"https://codeload.github.com/Tracktor/react-utils/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":229639971,"owners_count":18102834,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-12-14T02:39:33.372Z","updated_at":"2026-01-22T10:28:49.419Z","avatar_url":"https://github.com/Tracktor.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# @tracktor/react-utils\n\n[![npm version](https://badge.fury.io/js/@tracktor%2Freact-utils.svg)](https://badge.fury.io/js/@tracktor%2Freact-utils)\n[![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC)\n\n\u003e A comprehensive collection of modern React utilities and custom hooks to accelerate your development workflow.\n\n## 📦 Installation\n\n```bash\n# npm\nnpm install @tracktor/react-utils\n\n# yarn\nyarn add @tracktor/react-utils\n\n# pnpm\npnpm add @tracktor/react-utils\n```\n\n## 🚀 Quick Start\n\n```typescript\nimport { useInputState, capitalize, phoneNumberAdapter } from '@tracktor/react-utils';\n\nfunction App() {\n  const [email, onEmailChange] = useInputState('');\n  const [phone, onPhoneChange] = useInputState('');\n\n  return (\n    \u003cform\u003e\n      \u003cinput \n        type=\"email\" \n        value={email} \n        onChange={onEmailChange}\n        placeholder={capitalize('email address')}\n      /\u003e\n      \u003cinput \n        type=\"tel\" \n        value={phoneNumberAdapter(phone)} \n        onChange={onPhoneChange}\n        placeholder=\"Phone number\"\n      /\u003e\n    \u003c/form\u003e\n  );\n}\n```\n\n## 📚 API Documentation\n\n### 🎣 Hooks\n\n#### `useLocalStorage(key, initialValue?, options?)`\nManages data persistence in localStorage with automatic synchronization.\n\n```typescript\nconst [user, setUser, removeUser] = useLocalStorage('user', { name: '', email: '' });\n\n// With custom options\nconst [data, setData] = useLocalStorage('data', [], {\n  serializer: JSON.stringify,\n  deserializer: JSON.parse\n});\n```\n\n**Parameters:**\n- `key`: localStorage key\n- `initialValue`: Initial value (optional)\n- `options`: Serialization options (optional)\n\n**Returns:** `[value, setValue, removeValue]`\n\n---\n\n#### `useInputState(initialValue)`\nSimplifies input state management with automatic event handling.\n\n```typescript\nconst [name, onNameChange] = useInputState('');\nconst [isChecked, onCheckChange] = useInputState(false);\n\n\u003cinput type=\"text\" value={name} onChange={onNameChange} /\u003e\n\u003cinput type=\"checkbox\" checked={isChecked} onChange={onCheckChange} /\u003e\n```\n\n---\n\n#### `useDebounce(value, delayOrOptions)`\nDelays execution of a value to prevent excessive calls.\n\n```typescript\n// Simple usage\nconst debouncedSearchTerm = useDebounce(searchTerm, 500);\n\n// With callback\nconst debouncedValue = useDebounce(inputValue, {\n  delay: 300,\n  onDebounce: (value) =\u003e console.log('Debounced:', value)\n});\n```\n\n---\n\n#### `useToggle(initialState?)`\nManages boolean state with toggle functionality.\n\n```typescript\nconst [isOpen, toggle, setIsOpen] = useToggle(false);\n\n\u003cbutton onClick={toggle}\u003e\n  {isOpen ? 'Close' : 'Open'}\n\u003c/button\u003e\n```\n\n---\n\n#### `useWindowSize()`\nTracks window dimensions in real-time.\n\n```typescript\nconst { width, height } = useWindowSize();\n\nreturn (\n  \u003cdiv\u003e\n    Size: {width}x{height}\n  \u003c/div\u003e\n);\n```\n\n---\n\n#### `useInView(ref, options?)`\nDetects if an element is visible in the viewport.\n\n```typescript\nconst ref = useRef(null);\nconst isInView = useInView(ref, {\n  threshold: 0.5,\n  triggerOnce: true\n});\n\n\u003cdiv ref={ref}\u003e\n  {isInView ? 'Visible!' : 'Not visible'}\n\u003c/div\u003e\n```\n\n---\n\n#### `useDocumentTitle(title)`\nManages document title declaratively.\n\n```typescript\nconst { setTitle, title } = useDocumentTitle('My App');\n\n// Change title dynamically\nsetTitle('New Page');\n```\n\n---\n\n#### `useEventListener(eventName, handler, element?, options?)`\nAdds event listeners with automatic cleanup.\n\n```typescript\nconst buttonRef = useRef(null);\n\nuseEventListener('click', () =\u003e console.log('Clicked!'), buttonRef);\nuseEventListener('keydown', handleKeyDown); // window by default\n```\n\n---\n\n#### `useScript(src, options?)`\nLoads external scripts declaratively.\n\n```typescript\nconst status = useScript('https://example.com/script.js', {\n  position: 'body-end',\n  enable: true\n});\n\n// status: 'idle' | 'loading' | 'ready' | 'error'\n```\n\n---\n\n#### `useIntersectionObserver(elementRef, options)`\nAdvanced intersection observer hook for complex visibility detection.\n\n```typescript\nconst elementRef = useRef(null);\nconst entry = useIntersectionObserver(elementRef, {\n  threshold: 0.1,\n  freezeOnceVisible: true\n});\n\nconst isVisible = entry?.isIntersecting;\n```\n\n---\n\n#### `useIsMounted()`\nReturns a function to check if component is still mounted.\n\n```typescript\nconst isMounted = useIsMounted();\n\nuseEffect(() =\u003e {\n  fetchData().then(data =\u003e {\n    if (isMounted()) {\n      setData(data);\n    }\n  });\n}, []);\n```\n\n---\n\n#### `useEventCallback(fn)`\nEnsures callback stability while maintaining current references.\n\n```typescript\nconst handleClick = useEventCallback((id) =\u003e {\n  // This callback is stable but has access to current state\n  onItemClick(id, currentState);\n});\n```\n\n### 🛠️ Utilities\n\n#### Type Validation\n\n```typescript\nimport { isArray, isObject, isString, isNumber, isBoolean, isFunction, isRef } from '@tracktor/react-utils';\n\nif (isArray(data)) {\n  // TypeScript knows data is an array\n  console.log(data.length);\n}\n\nif (isObject(value)) {\n  // TypeScript knows value is an object\n  console.log(Object.keys(value));\n}\n```\n\n#### Object Manipulation\n\n```typescript\nimport { removeObjectProperty, isDeepEqualObject } from '@tracktor/react-utils';\n\n// Remove property without mutation\nconst newObj = removeObjectProperty(originalObj, 'propertyToRemove');\n\n// Deep comparison of objects\nconst areEqual = isDeepEqualObject(obj1, obj2);\n```\n\n#### String Manipulation\n\n```typescript\nimport { capitalize, capitalizeWords } from '@tracktor/react-utils';\n\ncapitalize('hello world');     // \"Hello world\"\ncapitalizeWords('hello world'); // \"Hello World\"\n```\n\n#### Number Conversion\n\n```typescript\nimport { toNumberOrZero } from '@tracktor/react-utils';\n\ntoNumberOrZero('42');    // 42\ntoNumberOrZero('abc');   // 0\ntoNumberOrZero(null);    // 0\ntoNumberOrZero(true);    // 0 (booleans return 0)\n```\n\n### 🔄 Adapters\n\n#### `phoneNumberAdapter(phoneNumber, options?)`\nFormats phone numbers according to international standards.\n\n```typescript\n// Supported formats\nphoneNumberAdapter('0123456789');              // \"01 23 45 67 89\" (France)\nphoneNumberAdapter('441234567890');            // \"1234 567 890\" (UK)\nphoneNumberAdapter('1234567890');              // \"(123) 456-7890\" (US)\n\n// With international prefix\nphoneNumberAdapter('33123456789', { addPrefix: true });  // \"+33 01 23 45 67 89\"\n\n// Custom separator\nphoneNumberAdapter('0123456789', { separator: '-' });    // \"01-23-45-67-89\"\n```\n\n**Supported Countries:**\n| Code | Country | Format |\n|------|---------|--------|\n| 33 | France | 01 23 45 67 89 |\n| 44 | United Kingdom | 1234 567 890 |\n| 49 | Germany | 0151 234 56789 |\n| 34 | Spain | 987 654 321 |\n| 1 | United States | (123) 456-7890 |\n\n---\n\n#### `priceAdapter(value, options?)`\nFormats prices according to locales and currencies.\n\n```typescript\npriceAdapter(1000);                           // \"1 000 €\"\npriceAdapter(1000.50);                        // \"1 000,50 €\"\npriceAdapter(500, { local: 'en-US' });       // \"€500\"\npriceAdapter('-');                            // \"-€\"\npriceAdapter(null);                           // \"0 €\"\n```\n\n**Options:**\n- `local`: Locale string (default: 'fr-FR')\n- `currency`: Currency code (default: 'EUR')\n- `style`: Number format style (default: 'currency')\n\n---\n\n#### `formatCreditCardNumber(number, maxLength?)`\nFormats credit card numbers with proper spacing.\n\n```typescript\nformatCreditCardNumber('1234567890123456');   // \"1234 5678 9012 3456\"\nformatCreditCardNumber('1234567890123456', 15); // \"1234 5678 9012 3\"\nformatCreditCardNumber(1234567890123456);     // \"1234 5678 9012 3456\"\n```\n\n---\n\n#### `addressToString(address)`\nConverts an address object to a formatted string.\n\n```typescript\nconst address = {\n  streetNumber: 82,\n  route: 'Chemin de cafon 2',\n  postalCode: '83720',\n  city: 'Trans en provence',\n  country: 'France'\n};\n\naddressToString(address); // \"82 Chemin de cafon 2, 83720 Trans en provence, France\"\n\n// Partial addresses work too\naddressToString({ city: 'Paris', country: 'France' }); // \"Paris, France\"\n```\n\n---\n\n#### `getInitials(name, capitalize?)`\nExtracts initials from a name.\n\n```typescript\n// With first and last name\ngetInitials({ firstName: 'John', lastName: 'Doe' });        // \"JD\"\n\n// With full name\ngetInitials({ fullName: 'John Doe Smith' });               // \"JD\"\n\n// With forced capitalization\ngetInitials({ firstName: 'john', lastName: 'doe' }, true); // \"JD\"\ngetInitials({ firstName: 'john', lastName: 'doe' }, false); // \"jd\"\n```\n\n## 🎨 Usage Examples\n\n### Complete Contact Form\n\n```typescript\nimport React from 'react';\nimport {\n  useInputState,\n  useToggle,\n  phoneNumberAdapter,\n  capitalize,\n  formatCreditCardNumber\n} from '@tracktor/react-utils';\n\nfunction ContactForm() {\n  const [name, onNameChange] = useInputState('');\n  const [email, onEmailChange] = useInputState('');\n  const [phone, onPhoneChange] = useInputState('');\n  const [cardNumber, onCardChange] = useInputState('');\n  const [acceptTerms, toggleTerms] = useToggle(false);\n\n  const handleSubmit = (e) =\u003e {\n    e.preventDefault();\n    console.log({\n      name: capitalize(name),\n      email,\n      phone: phoneNumberAdapter(phone, { addPrefix: true }),\n      cardNumber: formatCreditCardNumber(cardNumber)\n    });\n  };\n\n  return (\n    \u003cform onSubmit={handleSubmit}\u003e\n      \u003cinput\n        placeholder=\"Full name\"\n        value={name}\n        onChange={onNameChange}\n      /\u003e\n      \n      \u003cinput\n        type=\"email\"\n        placeholder=\"Email\"\n        value={email}\n        onChange={onEmailChange}\n      /\u003e\n      \n      \u003cinput\n        type=\"tel\"\n        placeholder=\"Phone\"\n        value={phoneNumberAdapter(phone)}\n        onChange={onPhoneChange}\n      /\u003e\n      \n      \u003cinput\n        placeholder=\"Card number\"\n        value={formatCreditCardNumber(cardNumber)}\n        onChange={onCardChange}\n        maxLength={19}\n      /\u003e\n      \n      \u003clabel\u003e\n        \u003cinput\n          type=\"checkbox\"\n          checked={acceptTerms}\n          onChange={toggleTerms}\n        /\u003e\n        I accept the terms\n      \u003c/label\u003e\n      \n      \u003cbutton type=\"submit\" disabled={!acceptTerms}\u003e\n        Submit\n      \u003c/button\u003e\n    \u003c/form\u003e\n  );\n}\n```\n\n### Search Hook with Debounce\n\n```typescript\nimport React, { useEffect, useState } from 'react';\nimport { useDebounce, useInputState } from '@tracktor/react-utils';\n\nfunction SearchComponent() {\n  const [query, onQueryChange] = useInputState('');\n  const [results, setResults] = useState([]);\n  const [loading, setLoading] = useState(false);\n  \n  const debouncedQuery = useDebounce(query, {\n    delay: 300,\n    onDebounce: () =\u003e setLoading(true)\n  });\n\n  useEffect(() =\u003e {\n    if (debouncedQuery) {\n      // Simulate API call\n      fetch(`/api/search?q=${debouncedQuery}`)\n        .then(res =\u003e res.json())\n        .then(data =\u003e {\n          setResults(data);\n          setLoading(false);\n        });\n    } else {\n      setResults([]);\n      setLoading(false);\n    }\n  }, [debouncedQuery]);\n\n  return (\n    \u003cdiv\u003e\n      \u003cinput\n        placeholder=\"Search...\"\n        value={query}\n        onChange={onQueryChange}\n      /\u003e\n      \n      {loading \u0026\u0026 \u003cdiv\u003eSearching...\u003c/div\u003e}\n      \n      \u003cul\u003e\n        {results.map(item =\u003e (\n          \u003cli key={item.id}\u003e{item.title}\u003c/li\u003e\n        ))}\n      \u003c/ul\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\n### Visibility Detection\n\n```typescript\nimport React, { useRef } from 'react';\nimport { useInView, useIntersectionObserver } from '@tracktor/react-utils';\n\nfunction VisibilityExample() {\n  const simpleRef = useRef(null);\n  const advancedRef = useRef(null);\n  \n  // Simple visibility detection\n  const isSimpleInView = useInView(simpleRef, {\n    threshold: 0.5,\n    triggerOnce: true\n  });\n  \n  // Advanced intersection observer\n  const entry = useIntersectionObserver(advancedRef, {\n    threshold: [0, 0.25, 0.5, 0.75, 1],\n    rootMargin: '50px'\n  });\n\n  return (\n    \u003cdiv\u003e\n      \u003cdiv ref={simpleRef} style={{ height: '100px', marginTop: '1000px' }}\u003e\n        {isSimpleInView ? '✅ Simple: Visible' : '❌ Simple: Hidden'}\n      \u003c/div\u003e\n      \n      \u003cdiv ref={advancedRef} style={{ height: '100px', marginTop: '1000px' }}\u003e\n        Advanced: {entry?.intersectionRatio.toFixed(2) || 0}% visible\n      \u003c/div\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\n## 🧪 Testing\n\nThe library is fully tested with Vitest. To run tests:\n\n```bash\n# Unit tests\nyarn test\n\n# Coverage report\nyarn coverage\n\n# Lint code\nyarn lint\n```\n\n## 🛡️ TypeScript Support\n\nAll utilities and hooks are fully typed with TypeScript. Types are automatically inferred:\n\n```typescript\nconst [count, setCount] = useLocalStorage('count', 0); // count is number\nconst [user, setUser] = useLocalStorage('user', { name: '', age: 0 }); // user is typed\n\n// Type guards work perfectly\nif (isArray(data)) {\n  data.map(item =\u003e item); // TypeScript knows data is an array\n}\n\nif (isString(value)) {\n  value.toUpperCase(); // TypeScript knows value is a string\n}\n```\n\n## 📋 Requirements\n\n- React ≥ 18.0.0\n- React DOM ≥ 18.0.0\n- TypeScript support included\n\n## 🌟 Features\n\n- ✅ **Fully typed** with TypeScript\n- ✅ **Tree-shakeable** - Import only what you need\n- ✅ **SSR compatible** - Works with Next.js, Gatsby, etc.\n- ✅ **Lightweight** - Minimal bundle impact\n- ✅ **Well tested** - Comprehensive test suite\n- ✅ **Modern** - Uses latest React patterns\n- ✅ **Performance focused** - Optimized hooks and utilities\n\n## 🤝 Contributing\n\nContributions are welcome! Please check our [contributing guide](CONTRIBUTING.md).\n\n1. Fork the project\n2. Create your feature branch (`git checkout -b feature/AmazingFeature`)\n3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)\n4. Push to the branch (`git push origin feature/AmazingFeature`)\n5. Open a Pull Request\n\n## 📝 License\n\nThis project is licensed under the ISC License. See the [LICENSE](LICENSE) file for details.\n\n## 🏷️ Keywords\n\n- React\n- React Hooks\n- Utilities\n- TypeScript\n- Performance\n- Developer Experience\n- Form Handling\n- State Management\n\n## 📞 Support\n\n- 🐛 [Report a bug](https://github.com/Tracktor/react-utils/issues)\n- 💡 [Request a feature](https://github.com/Tracktor/react-utils/issues)\n- 📖 [Documentation](https://github.com/Tracktor/react-utils#readme)\n- 💬 [Discussions](https://github.com/Tracktor/react-utils/discussions)\n\n## 🚀 What's Next?\n\nWe're constantly improving! Upcoming features:\n- 🔄 More form utilities\n- 📱 Mobile-specific hooks\n- 🎨 Animation helpers\n- 📊 Data manipulation utilities\n\n---\n\nBuilt with ❤️ by [Mickaël Austoni](https://github.com/MickaelAustoni)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftracktor%2Freact-utils","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftracktor%2Freact-utils","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftracktor%2Freact-utils/lists"}