Last active
July 24, 2023 01:46
-
-
Save Caellian/f172108fb4b19c38fd718c00978d38d0 to your computer and use it in GitHub Desktop.
React useEffect that works on iterables.
This file contains 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
/** | |
* 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