From 2d9ae5363239939f615e733761d82a856dba6ffb Mon Sep 17 00:00:00 2001 From: SukkaW Date: Sun, 29 Oct 2023 01:34:39 +0800 Subject: [PATCH] feat: add unstable `useUrlHashState` --- package-lock.json | 108 ++++++++++++++++---------------- package.json | 2 +- src/use-url-hash-state/index.ts | 104 ++++++++++++++++++++++++++++++ 3 files changed, 159 insertions(+), 55 deletions(-) create mode 100644 src/use-url-hash-state/index.ts diff --git a/package-lock.json b/package-lock.json index 85617f45..dfa33624 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "foxact", - "version": "0.2.21", + "version": "0.2.22", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "foxact", - "version": "0.2.21", + "version": "0.2.22", "license": "MIT", "dependencies": { "client-only": "^0.0.1", @@ -34,7 +34,7 @@ "gzip-size": "^6.0.0", "next": "^13.4.9", "react-router-dom": "^6.14.1", - "rollup": "^4.1.4", + "rollup": "^4.1.5", "rollup-plugin-dts": "^6.1.0", "rollup-plugin-swc3": "^0.10.3", "rollup-swc-preserve-directives": "^0.3.0" @@ -899,9 +899,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.1.4.tgz", - "integrity": "sha512-WlzkuFvpKl6CLFdc3V6ESPt7gq5Vrimd2Yv9IzKXdOpgbH4cdDSS1JLiACX8toygihtH5OlxyQzhXOph7Ovlpw==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.1.5.tgz", + "integrity": "sha512-/fwx6GS8cIbM2rTNyLMxjSCOegHywOdXO+kN9yFy018iCULcKZCyA3xvzw4bxyKbYfdSxQgdhbsl0egNcxerQw==", "cpu": [ "arm" ], @@ -912,9 +912,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.1.4.tgz", - "integrity": "sha512-D1e+ABe56T9Pq2fD+R3ybe1ylCDzu3tY4Qm2Mj24R9wXNCq35+JbFbOpc2yrroO2/tGhTobmEl2Bm5xfE/n8RA==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.1.5.tgz", + "integrity": "sha512-tmXh7dyEt+JEz/NgDJlB1UeL/1gFV0v8qYzUAU42WZH4lmUJ5rp6/HkR2qUNC5jCgYEwd8/EfbHKtGIEfS4CUg==", "cpu": [ "arm64" ], @@ -925,9 +925,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.1.4.tgz", - "integrity": "sha512-7vTYrgEiOrjxnjsgdPB+4i7EMxbVp7XXtS+50GJYj695xYTTEMn3HZVEvgtwjOUkAP/Q4HDejm4fIAjLeAfhtg==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.1.5.tgz", + "integrity": "sha512-lTDmLxdEVhzI3KCesZUrNbl3icBvPrDv/85JasY5gh4P2eAuDFmM4uj9HC5DdH0anLC0fwJ+1Uzasr4qOXcjRQ==", "cpu": [ "arm64" ], @@ -938,9 +938,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.1.4.tgz", - "integrity": "sha512-eGJVZScKSLZkYjhTAESCtbyTBq9SXeW9+TX36ki5gVhDqJtnQ5k0f9F44jNK5RhAMgIj0Ht9+n6HAgH0gUUyWQ==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.1.5.tgz", + "integrity": "sha512-v6qEHZyjWnIgcc4oiy8AIeFsUJAx+Kg0sLj+RE7ICwv3u7YC/+bSClxAiBASRjMzqsq0Z+I/pfxj+OD8mjBYxg==", "cpu": [ "x64" ], @@ -951,9 +951,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.1.4.tgz", - "integrity": "sha512-HnigYSEg2hOdX1meROecbk++z1nVJDpEofw9V2oWKqOWzTJlJf1UXVbDE6Hg30CapJxZu5ga4fdAQc/gODDkKg==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.1.5.tgz", + "integrity": "sha512-WngCfwPEDUNbZR1FNO2TCROYUwJvRlbvPi3AS85bDUkkoRDBcjUIz42cuB1j4PKilmnZascL5xTMF/yU8YFayA==", "cpu": [ "arm" ], @@ -964,9 +964,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.1.4.tgz", - "integrity": "sha512-TzJ+N2EoTLWkaClV2CUhBlj6ljXofaYzF/R9HXqQ3JCMnCHQZmQnbnZllw7yTDp0OG5whP4gIPozR4QiX+00MQ==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.1.5.tgz", + "integrity": "sha512-Q2A/PEP/UTPTOBwgar3mmCaApahoezai/8e/7f4GCLV6XWCpnU4YwkQQtla7d7nUnc792Ps7g1G0WMovzIknrA==", "cpu": [ "arm64" ], @@ -977,9 +977,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.1.4.tgz", - "integrity": "sha512-aVPmNMdp6Dlo2tWkAduAD/5TL/NT5uor290YvjvFvCv0Q3L7tVdlD8MOGDL+oRSw5XKXKAsDzHhUOPUNPRHVTQ==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.1.5.tgz", + "integrity": "sha512-84aBKNAVzTU/eG3tb2+kR4NGRAtm2YVW/KHwkGGDR4z1k4hyrDbuImsfs/6J74t6y0YLOe9HOSu7ejRjzUBGVQ==", "cpu": [ "arm64" ], @@ -990,9 +990,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.1.4.tgz", - "integrity": "sha512-77Fb79ayiDad0grvVsz4/OB55wJRyw9Ao+GdOBA9XywtHpuq5iRbVyHToGxWquYWlEf6WHFQQnFEttsAzboyKg==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.1.5.tgz", + "integrity": "sha512-mldtP9UEBurIq2+GYMdNeiqCLW1fdgf4KdkMR/QegAeXk4jFHkKQl7p0NITrKFVyVqzISGXH5gR6GSTBH4wszw==", "cpu": [ "x64" ], @@ -1003,9 +1003,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.1.4.tgz", - "integrity": "sha512-/t6C6niEQTqmQTVTD9TDwUzxG91Mlk69/v0qodIPUnjjB3wR4UA3klg+orR2SU3Ux2Cgf2pWPL9utK80/1ek8g==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.1.5.tgz", + "integrity": "sha512-36p+nMcSxjAEzfU47+by102HolUtf/EfgBAidocTKAofJMTqG5QD50qzaFLk4QO+z7Qvg4qd0wr99jGAwnKOig==", "cpu": [ "x64" ], @@ -1016,9 +1016,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.1.4.tgz", - "integrity": "sha512-ZY5BHHrOPkMbCuGWFNpJH0t18D2LU6GMYKGaqaWTQ3CQOL57Fem4zE941/Ek5pIsVt70HyDXssVEFQXlITI5Gg==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.1.5.tgz", + "integrity": "sha512-5oxhubo0A3J8aF/tG+6jHBg785HF8/88kl1YnfbDKmnqMxz/EFiAQDH9cq6lbnxofjn8tlq5KiTf0crJGOGThg==", "cpu": [ "arm64" ], @@ -1029,9 +1029,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.1.4.tgz", - "integrity": "sha512-XG2mcRfFrJvYyYaQmvCIvgfkaGinfXrpkBuIbJrTl9SaIQ8HumheWTIwkNz2mktCKwZfXHQNpO7RgXLIGQ7HXA==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.1.5.tgz", + "integrity": "sha512-uVQyBREKX9ErofL8KAZ4iVlqzSZOXSIG+BOLYuz5FD+Cg6jh1eLIeUa3Q4SgX0QaTRFeeAgSNqCC+8kZrZBpSw==", "cpu": [ "ia32" ], @@ -1042,9 +1042,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.1.4.tgz", - "integrity": "sha512-ANFqWYPwkhIqPmXw8vm0GpBEHiPpqcm99jiiAp71DbCSqLDhrtr019C5vhD0Bw4My+LmMvciZq6IsWHqQpl2ZQ==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.1.5.tgz", + "integrity": "sha512-FQ5qYqRJ2vUBSom3Fos8o/6UvAMOvlus4+HGCAifH1TagbbwVnVVe0o01J1V52EWnQ8kmfpJDJ0FMrfM5yzcSA==", "cpu": [ "x64" ], @@ -3962,9 +3962,9 @@ } }, "node_modules/rollup": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.1.4.tgz", - "integrity": "sha512-U8Yk1lQRKqCkDBip/pMYT+IKaN7b7UesK3fLSTuHBoBJacCE+oBqo/dfG/gkUdQNNB2OBmRP98cn2C2bkYZkyw==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.1.5.tgz", + "integrity": "sha512-AEw14/q4NHYQkQlngoSae2yi7hDBeT9w84aEzdgCr39+2RL+iTG84lGTkgC1Wp5igtquN64cNzuzZKVz+U6jOg==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -3974,18 +3974,18 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.1.4", - "@rollup/rollup-android-arm64": "4.1.4", - "@rollup/rollup-darwin-arm64": "4.1.4", - "@rollup/rollup-darwin-x64": "4.1.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.1.4", - "@rollup/rollup-linux-arm64-gnu": "4.1.4", - "@rollup/rollup-linux-arm64-musl": "4.1.4", - "@rollup/rollup-linux-x64-gnu": "4.1.4", - "@rollup/rollup-linux-x64-musl": "4.1.4", - "@rollup/rollup-win32-arm64-msvc": "4.1.4", - "@rollup/rollup-win32-ia32-msvc": "4.1.4", - "@rollup/rollup-win32-x64-msvc": "4.1.4", + "@rollup/rollup-android-arm-eabi": "4.1.5", + "@rollup/rollup-android-arm64": "4.1.5", + "@rollup/rollup-darwin-arm64": "4.1.5", + "@rollup/rollup-darwin-x64": "4.1.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.1.5", + "@rollup/rollup-linux-arm64-gnu": "4.1.5", + "@rollup/rollup-linux-arm64-musl": "4.1.5", + "@rollup/rollup-linux-x64-gnu": "4.1.5", + "@rollup/rollup-linux-x64-musl": "4.1.5", + "@rollup/rollup-win32-arm64-msvc": "4.1.5", + "@rollup/rollup-win32-ia32-msvc": "4.1.5", + "@rollup/rollup-win32-x64-msvc": "4.1.5", "fsevents": "~2.3.2" } }, diff --git a/package.json b/package.json index fcbf5811..fa146fc4 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "gzip-size": "^6.0.0", "next": "^13.4.9", "react-router-dom": "^6.14.1", - "rollup": "^4.1.4", + "rollup": "^4.1.5", "rollup-plugin-dts": "^6.1.0", "rollup-plugin-swc3": "^0.10.3", "rollup-swc-preserve-directives": "^0.3.0" diff --git a/src/use-url-hash-state/index.ts b/src/use-url-hash-state/index.ts new file mode 100644 index 00000000..f7e5db2a --- /dev/null +++ b/src/use-url-hash-state/index.ts @@ -0,0 +1,104 @@ +import 'client-only'; + +import { useCallback, useSyncExternalStore } from 'react'; +import { noop } from '../noop'; +import { useStableHandler } from '../use-stable-handler-only-when-you-know-what-you-are-doing-or-you-will-be-fired'; + +const identity = (x: string) => x as T; + +const subscribe: Parameters[0] = (() => { + if (typeof window === 'undefined') { + return (_callback: () => void) => noop; + } + + let hasSubscribedToHashChange = false; + + const listeners = new Set<() => void>(); + + // call every listener when hash changes + const handleHashChange = () => { + listeners.forEach((listener) => listener()); + }; + + // subscribe to hash change event by useSyncExternalStore + return (callback: () => void) => { + listeners.add(callback); + + if (!hasSubscribedToHashChange) { + hasSubscribedToHashChange = true; + window.addEventListener('hashchange', handleHashChange); + } + + return () => { + listeners.delete(callback); + }; + }; +})(); + +// This type utility is only used for workaround https://github.com/microsoft/TypeScript/issues/37663 +// eslint-disable-next-line @typescript-eslint/ban-types -- workaround TypeScript bug +const isFunction = (x: unknown): x is Function => typeof x === 'function'; + +function useUrlHashState( + key: string, + defaultValue?: undefined +): readonly [T | undefined, React.Dispatch>]; +function useUrlHashState( + key: string, + defaultValue: T, + transform?: (value: string) => T +): readonly [T, React.Dispatch>]; +function useUrlHashState( + key: string, + defaultValue?: T | undefined, + transform: (value: string) => T = identity +): readonly [T | undefined, React.Dispatch>] { + const memoized_transform = useStableHandler(transform); + + return [ + useSyncExternalStore( + subscribe, + () => { + const searchParams = new URLSearchParams(location.hash.slice(1)); + const storedValue = searchParams.get(key); + return storedValue !== null ? transform(storedValue) : defaultValue; + }, + () => defaultValue + ), + useCallback((updater) => { + const searchParams = new URLSearchParams(location.hash.slice(1)); + + const currentHash = location.hash; + + let newValue; + + if (isFunction(updater)) { + const storedValue = searchParams.get(key); + newValue = updater(storedValue !== null ? memoized_transform(storedValue) : defaultValue); + } else { + newValue = updater; + } + + if ( + (defaultValue !== undefined && newValue === defaultValue) + || newValue === undefined + ) { + searchParams.delete(key); + } else { + searchParams.set(key, JSON.stringify(newValue)); + } + + const newHash = searchParams.toString(); + + if (currentHash === newHash) { + return; + } + + location.hash = searchParams.toString(); + }, [defaultValue, key, memoized_transform]) + ] as const; +} + +export { + useUrlHashState as unstable_useUrlHashState +};