Skip to content

Instantly share code, notes, and snippets.

@Caellian
Last active July 24, 2023 01:46
Show Gist options
  • Save Caellian/f172108fb4b19c38fd718c00978d38d0 to your computer and use it in GitHub Desktop.
Save Caellian/f172108fb4b19c38fd718c00978d38d0 to your computer and use it in GitHub Desktop.
React useEffect that works on iterables.
/**
* Behaves like useEffect, but the hook function recieves changed iterable values as an argument.
*
* @param {(changed: any) => ((changed: any) => void)} call
* @param {Iterable} iter Primary dependency and iterable that's operated on
* @param {any[]} dependencies Additional dependencies
* @param {(a: any, b: any) => boolean} matcher Equality comparator used for comparing previous and new values, dequal (deep-equal) by default
*/
export function useEffectEach(call, iter, dependencies = [], matcher = dequal) {
if (!isIterable(iter)) throw new Error("useEffectEach requires second argument to be a single iterable");
if (!Array.isArray(dependencies))
throw new Error("useEffectEach requires third argument to be a list of dependencies");
const prev = useRef(null);
function appendNew(current) {
if (!current || current.lenght === 0) return;
const added = [];
for (const value of current) {
const destructor = call(value) || (() => {});
added.push([value, destructor]);
}
prev.current = [...(prev.current || []), ...added];
}
useEffect(() => {
const current = Array.from(iter);
if ((!prev.current || prev.current.length === 0) && current.length === 0) {
// nothing to do; dependencies or iterable ref changed
return;
}
// things have only been added to iterable
if (prev.current === null || (prev.current.length === 0 && current.length > 0)) {
return appendNew(current);
}
// everything removed from iterable
if (prev.current.length > 0 && current.length === 0) {
for (const [value, destructor] of prev.current) {
destructor(value);
}
prev.current = null;
return;
}
// - match cross product pairs
// - for prev values which haven't been found, call destructor
// - for newly added values, call the hook
const removed = [];
for (const [a, destructor] of prev.current) {
let found = false;
// could be a for..of loop, but we need internal mutation
let i = 0;
while (i < current.length) {
const b = current[i];
// this conditional would be the actual contents of for..of
if (matcher(a, b)) {
found = true;
// remove doun
current.splice(current.indexOf(b), 1);
continue;
}
i++;
}
// new iterable doesn't contain previous the value anymore,
// so call the destructor function
if (!found) {
destructor(a);
// note that a is the same ref stored in prev
// b might be matching but a different ref
removed.push(a);
}
}
// current now only contains new values,
// everything else was removed during comparison
// update tracked state
if (removed.length === prev.current.lenght) {
// everything old has been removed
prev.current = null;
} else {
// remove only changed values
prev.current = prev.current.filter(([it]) => !removed.includes(it));
}
// add new values
appendNew(current);
}, [...dependencies, iter]);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment