From d659ed29becb10c528d48105389a1dc9327e1e4f Mon Sep 17 00:00:00 2001 From: Yair Even Or Date: Mon, 17 Oct 2022 22:32:21 +0300 Subject: [PATCH] - replaced native `useId` from react-v18 to a non-native solution for backward-compatibility support - general refactor, mainly DRY - added logic for unmounted components, so the watchers would be removed --- src/propWatcher.js | 40 ++++++++++++++++++++++++++++--------- src/useWatchableEffect.js | 32 ++++++++++++++--------------- src/useWatchableListener.js | 23 ++++++++------------- src/utils.js | 7 +++++++ 4 files changed, 61 insertions(+), 41 deletions(-) create mode 100644 src/utils.js diff --git a/src/propWatcher.js b/src/propWatcher.js index 0f181e3..202fdf8 100644 --- a/src/propWatcher.js +++ b/src/propWatcher.js @@ -10,31 +10,53 @@ const propWatcher = obj => { value : {} }); + // update watchers (when prop gets set/deleted) + function runWatchers(target, prop, value) { + if( prop !== '__WATCHERS' ) { + Object.values(target?.__WATCHERS || []).forEach(cb => cb(prop, value)) + } + } + return new Proxy(obj, { // watch when a property is set set(target, prop, value) { // do nothing if value hasn't changed, to avoid a possible re-render when the same value is set again - if( target[prop] === value) return; + if( target[prop] === value) return true // set the value target[prop] = value; - - // update watchers a prop has changed - if( prop !== '__WATCHERS' ) { - Object.values(target?.__WATCHERS || []).forEach(cb => cb(prop, value)) - } + runWatchers(target, prop, value) + return true }, // watch when a prop is deleted deleteProperty(target, prop) { if (prop in target) { delete target[prop] - if( prop !== '__WATCHERS' ) { - Object.values(target?.__WATCHERS || []).forEach(cb => cb(prop)) - } + runWatchers(target, prop) } + return true }, }) } +export const setWatcher = (obj, id, callback) => { + if( !obj.__WATCHERS ) { + console.warn('Object is not watchable. Did you pass the correct Object?') + return + } + + if( !callback || typeof callback != 'function' ) { + console.warn('Invalid callback') + return + } + + // register a listener for that namespace + obj.__WATCHERS[id] = callback +} + +export const removeWatcher = (obj, id) => { + delete obj.__WATCHERS[id] +} + export default propWatcher diff --git a/src/useWatchableEffect.js b/src/useWatchableEffect.js index 885398d..66292c9 100644 --- a/src/useWatchableEffect.js +++ b/src/useWatchableEffect.js @@ -1,28 +1,26 @@ -import {useId} from 'react' +import {useEffect} from 'react' +import {useId} from './utils' +import {setWatcher, removeWatcher} from './propWatcher' /** - * Similar to "useSmartRefListener" but just listens without automatically re-rendering (no 'useState') + * Similar to "useWatchableListener" but just listens without automatically re-rendering (no 'useState') * @param {*} callback fires when a ref change detetced - * @param {*} dependencies array of watchable "smart" refs + * @param {*} dependencies array of watchable refs */ const useWatchableEffect = (callback, dependencies) => { const id = useId() - dependencies.forEach(ref => { - // catch errors - if( !ref ) { - console.warn("useWatchableEffect - ref does not exists") - return - } + useEffect(() => { + // bind the callback to all dependencies + dependencies.forEach(ref => { + setWatcher(ref, id.current, callback) + }) - if( !ref.__WATCHERS ) { - console.warn("useWatchableEffect - ref is not watchable. Did you pass the correct Object?") - return + // remove callback if component unmounted + return () => { + dependencies.forEach(ref => removeWatcher(ref, id.current)) } - - // register a listener for that namespace - ref.__WATCHERS[id] = callback - }) + }, dependencies) } -export default useWatchableEffect; \ No newline at end of file +export default useWatchableEffect \ No newline at end of file diff --git a/src/useWatchableListener.js b/src/useWatchableListener.js index daa6676..04b8882 100644 --- a/src/useWatchableListener.js +++ b/src/useWatchableListener.js @@ -1,4 +1,6 @@ -import {useId, useState, useCallback} from 'react' +import {useState, useCallback, useEffect} from 'react' +import {useId} from './utils' +import {setWatcher, removeWatcher} from './propWatcher' /** * Listens to refs changes. @@ -19,22 +21,13 @@ const useWatchableListener = ( const [state, setState] = useState() const id = useId() - - const unlisten = useCallback(() => { delete ref.__WATCHERS[id] }, [ref, id]) - - // catch errors - if( !ref ) { - console.warn("useWatchableListener - ref does not exists") - return - } - - if( !ref.__WATCHERS ) { - console.warn("useWatchableListener - ref is not watchable. Did you pass the correct Object?") - return - } + const unlisten = useCallback(() => removeWatcher(ref, id), [ref, id]) // register a listener for that namespace - ref.__WATCHERS[id] = (prop, value) => watcher(propName, prop, value, setState) + setWatcher(ref, id, (prop, value) => watcher(propName, prop, value, setState)) + + // remove callback if component unmounted + useEffect(() => unlisten, []) return unlisten } diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..6fbb9be --- /dev/null +++ b/src/utils.js @@ -0,0 +1,7 @@ +import {useRef} from 'react' + +// https://stackoverflow.com/a/7061193/104380 +export const UUIDv4 = function b(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,b)}; + +// Native is preferable but only for React v18+, so for backward-compatibility, use this: +export const useId = () => useRef(UUIDv4()).current; \ No newline at end of file