Last active
February 27, 2023 01:42
-
-
Save freehuntx/a78ae3162b350af67b24d4a68ffc1217 to your computer and use it in GitHub Desktop.
reactive-react
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import hash from 'hash-it'; | |
import { useEffect, useState } from 'react'; | |
const MIN_DELAY = 50; | |
const MAX_DELAY = 1000; | |
const STEP_DELAY = 50; | |
export function useReactive(target) { | |
const [val, setVal] = useState(target); | |
const [_toggle, setToggle] = useState(false); | |
useEffect(() => { | |
let delay = MIN_DELAY; | |
let lastHash = 0; | |
let timeout; | |
const wait = () => { | |
const newHash = hash(val); | |
const changed = lastHash !== newHash; | |
lastHash = newHash; | |
if (changed) { | |
setToggle((b) => !b); | |
delay -= STEP_DELAY; | |
if (delay < MIN_DELAY) delay = MIN_DELAY; | |
} else { | |
delay += STEP_DELAY; | |
if (delay > MAX_DELAY) delay = MAX_DELAY; | |
} | |
timeout = setTimeout(wait, delay); | |
}; | |
wait(); | |
return () => { | |
clearTimeout(timeout); | |
}; | |
}, [val]); | |
return [val, setVal]; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useEffect, useState } from 'react'; | |
function getAllPropertyDescriptors(obj, descriptors = Object.create(null)) { | |
if (!obj) return descriptors; | |
const parentProto = Object.getPrototypeOf(obj); | |
if ( | |
parentProto && | |
parentProto !== Object.prototype && | |
parentProto !== Function.prototype | |
) { | |
Object.assign(descriptors, parentProto); | |
getAllPropertyDescriptors(parentProto, descriptors); | |
} | |
const ownProto = Object.getOwnPropertyDescriptors(obj); | |
if (ownProto !== Object.prototype && ownProto !== Function.prototype) { | |
Object.assign(descriptors, ownProto); | |
} | |
return descriptors; | |
} | |
class Reactive { | |
_watcher = []; | |
_unwatcher = []; | |
_target = null; | |
static from(target) { | |
if (!target) return null; | |
if (target instanceof Reactive) return target; | |
if (target?.$reactive) return target.$reactive; | |
const type = typeof target; | |
if (type !== 'object' && type !== 'function') return null; | |
return new Reactive(target); | |
} | |
constructor(target) { | |
if (target.$reactive) throw new Error('Target already reactive'); | |
if (target instanceof Reactive) | |
throw new Error('Target is an "Reactive" instance'); | |
this._target = target; | |
this._init(); | |
} | |
_init() { | |
if (!this._target) return; | |
if (this._target === null) return; | |
if (typeof this._target !== 'object' && typeof this._target !== 'function') | |
return; | |
if (Array.isArray(this._target)) { | |
for (const method of ['pop', 'reverse', 'shift', 'splice', 'sort']) { | |
this._target[method] = (...args) => { | |
const result = Array.prototype[method].apply(this._target, args); | |
this._emitChange(); | |
return result; | |
}; | |
} | |
for (const method of ['push', 'unshift']) { | |
this._target[method] = (...args) => { | |
const result = Array.prototype[method].apply(this._target, args); | |
args.forEach((arg) => { | |
Reactive.from(arg)?.watch(() => { | |
this._emitChange(); | |
}); | |
// TODO: Unwatch when popped/removed | |
}); | |
this._emitChange(); | |
return result; | |
}; | |
} | |
} | |
if (this._target instanceof Map) { | |
for (const method of ['set', 'delete', 'clear']) { | |
this._target[method] = (...args) => { | |
const result = Map.prototype[method].apply(this._target, args); | |
this._emitChange(); | |
return result; | |
}; | |
} | |
} | |
if (this._target instanceof Set) { | |
for (const method of ['add', 'delete', 'clear']) { | |
this._target[method] = (...args) => { | |
const result = Set.prototype[method].apply(this._target, args); | |
this._emitChange(); | |
return result; | |
}; | |
} | |
} | |
const descriptors = getAllPropertyDescriptors(this._target); | |
for (const [propName, descriptor] of Object.entries(descriptors)) { | |
if (propName === 'constructor') continue; | |
if (descriptor.get) { | |
Reactive.from(descriptor.get.apply(this._target))?.watch(() => { | |
this._emitChange(); | |
}); | |
} | |
if (descriptor.set && descriptor.configurable) { | |
Object.defineProperty(this._target, propName, { | |
configurable: true, | |
set: (val) => { | |
const result = descriptor.set.apply(this._target, [val]); | |
this._emitChange(); | |
return result; | |
}, | |
}); | |
} | |
if (descriptor.value !== undefined) { | |
if ( | |
typeof descriptor.value === 'object' || | |
typeof descriptor.value === 'function' | |
) { | |
Reactive.from(descriptor.value)?.watch(() => { | |
this._emitChange(); | |
}); | |
} else if (!descriptor.set && descriptor.configurable) { | |
let val = this._target[propName]; | |
Object.defineProperty(this._target, propName, { | |
configurable: true, | |
get: () => val, | |
set: (newVal) => { | |
val = newVal; | |
this._emitChange(); | |
}, | |
}); | |
} | |
} | |
if (this._target[propName] !== undefined) { | |
Reactive.from(this._target[propName])?.watch(() => { | |
this._emitChange(); | |
}); | |
} | |
} | |
} | |
_emitChange() { | |
this._watcher.forEach((cb) => cb()); | |
} | |
watch(cb) { | |
this.unwatch(cb); | |
this._watcher.push(cb); | |
return () => { | |
this.unwatch(cb); | |
}; | |
} | |
unwatch(cb) { | |
this._watcher.splice(this._watcher.indexOf(cb), 1); | |
} | |
} | |
export function useReactive(target) { | |
const [val, setVal] = useState(target); | |
const [_toggle, setToggle] = useState(false); | |
useEffect(() => { | |
return Reactive.from(val)?.watch(() => { | |
setToggle((b) => !b); | |
}); | |
}, [val]); | |
return [val, setVal]; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useEffect, useState } from 'react'; | |
function getPrototypeChain(obj, chain = []) { | |
if (obj === null || obj === undefined) return chain; | |
let prototype = Object.getPrototypeOf(obj); | |
if (prototype) { | |
chain.push(prototype); | |
getPrototypeChain(prototype, chain); | |
} | |
return chain; | |
} | |
function getPrototypeDescriptorChain( | |
obj, | |
chain = [] | |
): (TypedPropertyDescriptor<any> | PropertyDescriptor)[] { | |
if (obj === null || obj === undefined) return chain; | |
const descriptor = Object.getOwnPropertyDescriptors(obj); | |
if (descriptor) { | |
chain.push(descriptor); | |
} | |
getPrototypeChain(obj).forEach((prototype) => { | |
if (prototype === Object.prototype || prototype === Function.prototype) | |
return; | |
const descriptor = Object.getOwnPropertyDescriptors(prototype); | |
if (descriptor) { | |
chain.push(descriptor); | |
} | |
}); | |
return chain; | |
} | |
function getAllPropertyDescriptors( | |
obj | |
): TypedPropertyDescriptor<any> | PropertyDescriptor { | |
const allDescriptors = Object.create(null); | |
if (!obj) return allDescriptors; | |
getPrototypeDescriptorChain(obj) | |
.reverse() | |
.forEach((descriptors) => { | |
if (descriptors) Object.assign(allDescriptors, descriptors); | |
}); | |
delete allDescriptors['prototype']; | |
Object.getOwnPropertyNames(Object.prototype).forEach((propName) => { | |
delete allDescriptors[propName]; | |
}); | |
if (typeof obj === 'function') { | |
Object.getOwnPropertyNames(Function.prototype).forEach((propName) => { | |
delete allDescriptors[propName]; | |
}); | |
} | |
return allDescriptors; | |
} | |
class Reactive { | |
_watcher = []; | |
_unwatcher = []; | |
_target = null; | |
static from(target) { | |
if (!target) return null; | |
if (target instanceof Reactive) return target; | |
if (target?.$reactive) return target.$reactive; | |
const type = typeof target; | |
if (type !== 'object' && type !== 'function') return null; | |
return new Reactive(target); | |
} | |
constructor(target) { | |
if (target.$reactive) throw new Error('Target already reactive'); | |
if (target instanceof Reactive) | |
throw new Error('Target is an "Reactive" instance'); | |
this._target = target; | |
this._init(); | |
} | |
_init() { | |
if (!this._target) return; | |
if (this._target === null) return; | |
if (typeof this._target !== 'object' && typeof this._target !== 'function') | |
return; | |
if (Array.isArray(this._target)) { | |
for (const method of ['pop', 'reverse', 'shift', 'splice', 'sort']) { | |
this._target[method] = (...args) => { | |
const result = Array.prototype[method].apply(this._target, args); | |
this._emitChange(); | |
return result; | |
}; | |
} | |
for (const method of ['push', 'unshift']) { | |
this._target[method] = (...args) => { | |
const result = Array.prototype[method].apply(this._target, args); | |
args.forEach((arg) => { | |
Reactive.from(arg)?.watch(() => { | |
this._emitChange(); | |
}); | |
// TODO: Unwatch when popped/removed | |
}); | |
this._emitChange(); | |
return result; | |
}; | |
} | |
} | |
if (this._target instanceof Map) { | |
for (const method of ['set', 'delete', 'clear']) { | |
this._target[method] = (...args) => { | |
const result = Map.prototype[method].apply(this._target, args); | |
this._emitChange(); | |
return result; | |
}; | |
} | |
} | |
if (this._target instanceof Set) { | |
for (const method of ['add', 'delete', 'clear']) { | |
this._target[method] = (...args) => { | |
const result = Set.prototype[method].apply(this._target, args); | |
this._emitChange(); | |
return result; | |
}; | |
} | |
} | |
const descriptors = getAllPropertyDescriptors(this._target); | |
for (const [propName, descriptor] of Object.entries(descriptors)) { | |
if (descriptor.get) { | |
Reactive.from(descriptor.get.apply(this._target))?.watch(() => { | |
this._emitChange(); | |
}); | |
} | |
if (descriptor.set && descriptor.configurable) { | |
Object.defineProperty(this._target, propName, { | |
configurable: true, | |
set: (val) => { | |
const result = descriptor.set.apply(this._target, [val]); | |
this._emitChange(); | |
return result; | |
}, | |
}); | |
} | |
if (descriptor.value !== undefined) { | |
if ( | |
typeof descriptor.value === 'object' || | |
typeof descriptor.value === 'function' | |
) { | |
Reactive.from(descriptor.value)?.watch(() => { | |
this._emitChange(); | |
}); | |
} else if (!descriptor.set && descriptor.configurable) { | |
let val = this._target[propName]; | |
Object.defineProperty(this._target, propName, { | |
configurable: true, | |
get: () => val, | |
set: (newVal) => { | |
val = newVal; | |
this._emitChange(); | |
}, | |
}); | |
} | |
} | |
if (this._target[propName] !== undefined) { | |
Reactive.from(this._target[propName])?.watch(() => { | |
this._emitChange(); | |
}); | |
} | |
} | |
} | |
_emitChange() { | |
this._watcher.forEach((cb) => cb()); | |
} | |
watch(cb) { | |
this.unwatch(cb); | |
this._watcher.push(cb); | |
return () => { | |
this.unwatch(cb); | |
}; | |
} | |
unwatch(cb) { | |
this._watcher.splice(this._watcher.indexOf(cb), 1); | |
} | |
} | |
export function useReactive(target) { | |
const [val, setVal] = useState(target); | |
const [_toggle, setToggle] = useState(false); | |
useEffect(() => { | |
return Reactive.from(val)?.watch(() => { | |
setToggle((b) => !b); | |
}); | |
}, [val]); | |
return [val, setVal]; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment