UNPKG

@bhammond/react-stateful

Version:

A Signal and Querystate backed React State management utility library.

202 lines (201 loc) 6.51 kB
import { useState, useCallback, useEffect, useDebugValue, useRef } from 'react'; function isNumber(value) { return typeof value === 'number' && !isNaN(value); } function isBoolean(value) { return typeof value === 'boolean'; } function isObject(value) { return typeof value === 'object' && value !== null && !Array.isArray(value); } function isArray(value) { return Array.isArray(value); } function parseValue(value, defaultValue) { try { if (value === null) return defaultValue; const decoded = decodeURIComponent(value); // Handle different expected types based on defaultValue or type T if (defaultValue !== undefined) { if (isNumber(defaultValue)) { const num = Number(decoded); return isNaN(num) ? defaultValue : num; } if (isBoolean(defaultValue)) { return (decoded === 'true' ? true : decoded === 'false' ? false : defaultValue); } if (isObject(defaultValue) || isArray(defaultValue)) { try { const parsed = JSON.parse(decoded); return typeof parsed === typeof defaultValue ? parsed : defaultValue; } catch (_a) { return defaultValue; } } } // Type inference without defaultValue if (decoded === 'true') return true; if (decoded === 'false') return false; if (/^\d+$/.test(decoded)) { const num = Number(decoded); return isNaN(num) ? decoded : num; } if (decoded.length > 0 && (decoded[0] == '{' || decoded[0] == '[')) { try { return JSON.parse(decoded); } catch (_b) { return decoded; } } return decoded; } catch (_c) { return defaultValue; } } function stringifyValue(value) { try { if (value === null || value === undefined) return ''; if (typeof value === 'object') return JSON.stringify(value); return String(value); } catch (_a) { return ''; } } function isURLParamsLike(params) { return typeof params.get === 'function'; } function getParam(params, key) { var _a; try { if (isURLParamsLike(params)) { return params.get(key); } const value = params[key]; if (Array.isArray(value)) return (_a = value[0]) !== null && _a !== void 0 ? _a : null; return value !== null && value !== void 0 ? value : null; } catch (_b) { return null; } } class Signal { constructor(value) { this.listeners = []; this.value = value; } notify(newValue) { this.value = newValue; this.listeners.forEach(listener => { try { listener(newValue); } catch (e) { console.error('Signal listener error:', e); } }); } subscribe(fn) { this.listeners.push(fn); return () => { this.listeners = this.listeners.filter(f => f !== fn); }; } } const store = new Map(); function getSignal(key, value) { let signal = store.get(key); if (!signal) { signal = new Signal(value); store.set(key, signal); } return signal; } function useQueryState(name, params, defaultValue) { if (!name) { console.error('useQueryState requires a name parameter'); name = 'unnamed'; } const key = encodeURIComponent(name); const paramValue = getParam(params, key); const initialValue = parseValue(paramValue, defaultValue); const [isInitialRender, setIsInitialRender] = useState(true); const signalRef = useRef(getSignal(key, initialValue)); const [value, setValue] = useState(initialValue); useEffect(() => { setIsInitialRender(false); }, []); useEffect(() => { let mounted = true; if (!isInitialRender && typeof window !== 'undefined') { try { const params = new URLSearchParams(window.location.search); const urlValue = parseValue(params.get(key), defaultValue); if (mounted) { signalRef.current.notify(urlValue); } } catch (e) { console.error('Error syncing with URL:', e); } } return () => { mounted = false; }; }, [isInitialRender, key, defaultValue]); useEffect(() => { return signalRef.current.subscribe(setValue); }, []); const setQueryValue = useCallback((newValue) => { try { const nextValue = typeof newValue === 'function' ? newValue(signalRef.current.value) : newValue; signalRef.current.notify(nextValue); if (typeof window !== 'undefined') { const params = new URLSearchParams(window.location.search); if (nextValue === null || nextValue === false || nextValue === '') { params.delete(key); } else { params.set(key, encodeURIComponent(stringifyValue(nextValue))); } const newUrl = `${window.location.pathname}${params.toString() ? `?${params.toString()}` : ''}`; window.history.pushState({}, '', newUrl); } } catch (e) { console.error('Error updating query state:', e); } }, [key]); useEffect(() => { if (typeof window === 'undefined') return; function handleUrlChange() { try { const params = new URLSearchParams(window.location.search); const urlValue = parseValue(params.get(key), defaultValue); signalRef.current.notify(urlValue); } catch (e) { console.error('Error handling URL change:', e); } } window.addEventListener('popstate', handleUrlChange); return () => { window.removeEventListener('popstate', handleUrlChange); }; }, [key, defaultValue]); useDebugValue(value); return [isInitialRender ? initialValue : value, setQueryValue]; } export default useQueryState;