react-exo-hooks
Version:
A collection of useful hooks for data structures and logic, designed for efficiency
169 lines (168 loc) • 4.8 kB
JavaScript
import { useState, useEffect } from 'react';
/**
* This is an array that causes rerenders on updates
* @note Effects and memos that use this array should also listen for its signal: `+INSTANCE`
*/
export class StatefulArray extends Array {
/** The dispatch function for the signal */
_dispatchSignal;
/** The update signal */
_signal;
/** THe dispatch function for redefining the array */
_dispatchRedefine;
/**
* Construct a StatefulArray
* @param initial The initial value (parameter for a vanilla array)
* @param dispatchSignal The dispatch function for the signal
*/
constructor(initial, dispatchSignal) {
if (initial)
super(...initial);
else
super();
this._signal = 0;
this._dispatchSignal = dispatchSignal;
}
/**
* Set the redefine dispatch
* @private
* @param callback The function
*/
_setRedefine(callback) {
this._dispatchRedefine = callback;
}
/**
* Force a signal update
*/
forceUpdate() {
this._dispatchSignal?.(++this._signal);
}
/**
* Set the instance to an entirely new instance
* @param value The new instance
* @returns The new instance
* @throws {Error} If there's no redefinition callback defined
*/
reset(value) {
if (!this._dispatchRedefine)
throw new Error('Cannot redefine Array. No redefine callback set.');
const instance = new StatefulArray(value, this._dispatchSignal);
if (this._signal === 0)
instance._signal = 1; // Force update
this._dispatchRedefine(instance);
instance._dispatchSignal?.(instance._signal);
return instance;
}
/**
* @override
*/
copyWithin(target, start, end) {
let different = false;
for (let offset = 0; offset < (end ?? this.length - 1); ++offset) {
if (this[target + offset] !== this[start + offset]) {
different = true;
break;
}
}
if (different)
this._dispatchSignal?.(++this._signal);
return super.copyWithin(target, start, end);
}
/**
* @override
*/
fill(value, start, end) {
let different = false;
for (let i = start ?? 0; i < (end ?? this.length - 1); ++i) {
if (this[i] !== value) {
different = true;
break;
}
}
if (different)
this._dispatchSignal?.(++this._signal);
return super.fill(value, start, end);
}
/**
* @override
*/
pop() {
if (this.length)
this._dispatchSignal?.(++this._signal);
return super.pop();
}
/**
* @override
*/
push(...items) {
if (items.length)
this._dispatchSignal?.(++this._signal);
return super.push(...items);
}
/**
* @override
*/
reverse() {
let palindrome = true;
for (let i = 0; i < Math.floor(this.length / 2); ++i) {
if (this[i] !== this[this.length - i - 1]) {
palindrome = false;
break;
}
}
if (!palindrome)
this._dispatchSignal?.(++this._signal);
return super.reverse();
}
/**
* @override
*/
shift() {
if (this.length)
this._dispatchSignal?.(++this._signal);
return super.shift();
}
/**
* @override
*/
sort(compareFn) {
// No way to efficiently compare this without copying; always signal
this._dispatchSignal?.(++this._signal);
return this.sort(compareFn);
}
/**
* @override
*/
splice(start, deleteCount, ...rest) {
if (deleteCount || rest.length)
this._dispatchSignal?.(++this._signal);
return super.splice(start, deleteCount, ...rest);
}
/**
* @override
*/
unshift(...items) {
if (items.length)
this._dispatchSignal?.(++this._signal);
return super.unshift(...items);
}
/**
* Returns the array's signal. Used for effects and memos that use this array
* @returns A numeric signal
*/
valueOf() {
return this._signal;
}
}
/**
* Use a stately array
* @note Any effects or memos that use this set should also listen for its signal (`+INSTANCE`)
* @param initial The initial array value
* @returns The stately array
*/
export function useArray(initial) {
const [, setSignal] = useState(0);
const [array, setArray] = useState(new StatefulArray(initial, setSignal));
useEffect(() => array._setRedefine(setArray), [array]);
return array;
}