Skip to content

Commit

Permalink
- replaced native useId from react-v18 to a non-native solution for…
Browse files Browse the repository at this point in the history
… backward-compatibility support

- general refactor, mainly DRY
- added logic for unmounted components, so the watchers would be removed
  • Loading branch information
Yair Even Or authored and Yair Even Or committed Oct 17, 2022
1 parent cd4fabe commit d659ed2
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 41 deletions.
40 changes: 31 additions & 9 deletions src/propWatcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
32 changes: 15 additions & 17 deletions src/useWatchableEffect.js
Original file line number Diff line number Diff line change
@@ -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;
export default useWatchableEffect
23 changes: 8 additions & 15 deletions src/useWatchableListener.js
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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
}
Expand Down
7 changes: 7 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -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;

0 comments on commit d659ed2

Please sign in to comment.