The following is a code snippet from a solid.js application. I am attempting to update the state of a top-level signal from within an IntersectionObserver's callback. The state is being updated within the scope of the callback, but not outside.
Question one: how is this possible? If I can access setIds()
and then manipulate the value - what would stop this value update from taking place outside of the scope of the callback?
Question two: does anyone know how I can update the state of the signal from within the callbacK?
const [ids, setIds] = createSignal(new Set<string>());
createEffect(() => {
const callback = (entries: any, observer: any) => {
const current = ids()
entries.forEach((e: any) => {
const intersecting = e.isIntersecting;
const id = e.target.id;
if (intersecting) {
setIds(current.add(id));
}
else {
setIds(current.remove(id));
}
});
// NOTE: The following outputs the correct value
console.log("This is the correct value:", ids())
};
const observer = new IntersectionObserver(callback);
const elements = Array.from(document.querySelectorAll("p"));
elements.forEach((element) => observer.observe(element));
return () => observer.disconnect();
}, [setIds]);
// NOTE: The following does NOT update at all.
createEffect(() => console.log("This value is not being updated:", ids()));
The following is for people that prefer to test things out in real-world scenarios:
I have also provided a code block with an entire solid.js application that can be used for testing. The app provides a loop with 1,000 paragraph tags within an autoscroll element with a max height of ${n} pixels - creating a scrolling effect. Each paragraph comes with a(n?) unique id. It is my objective to have a constantly updating list (stored in a top level signal) that i can use to determine exactly which id's are visible. So far, I have been trying to do this with an IntersectionObserver
containing a callback
. This is working within the callback
however, the value is not being updated outside of the callback
function.
import {
For,
createSignal,
createEffect,
} from "solid-js";
import { render } from 'solid-js/web';
const App = () => {
const [ids, setIds] = createSignal(new Set<string>());
createEffect(() => {
const callback = (entries: any, observer: any) => {
const current = ids()
entries.forEach((e: any) => {
const intersecting = e.isIntersecting;
const id = e.target.id;
if (intersecting) {
setIds(current.add(id));
}
else {
setIds(current.remove(id));
}
});
// NOTE: The following outputs the correct value
console.log("This is the correct value:", ids())
};
let options = {
root: document.getElementById("main"),
threshold: 1.0
}
const observer = new IntersectionObserver(callback, options);
const elements = Array.from(document.querySelectorAll("p"));
elements.forEach((element) => observer.observe(element));
return () => observer.disconnect();
}, [setIds]);
// NOTE: The following does NOT update at all.
createEffect(() => console.log("This value is not being updated:", ids()));
const arr = Array.from(Array(1000).keys())
return (
<div id="main" style="max-height: 550px;">
<For each={arr}>
{(number) => {
return (
<p id={`${number}`}>
{number}
</p>
)
}}
</For>
</div>
);
};
render(() => <App />, document.getElementById('root') as HTMLElement);
CodePudding user response:
Signals do not care what scope they are in, effect or not, so being in an intersection observer is irrelevant.
The setter function returned from createSignal
runs an identity check on the prev and next values to avoid unnecessary updates.
In your example you are not setting a new value but assigning it into a variable, mutating it and setting it back. Since Sets are reference values, prev and next values pointing to the same object, ids() === current
returns true, so signal fails to update.
const [ids, setIds] = createSignal(new Set<string>());
createEffect(() => {
const current = ids();
setIds(current); // Not a new value
});
You need to pass a new Set to trigger update:
const [ids, setIds] = createSignal<Set<string>>(new Set<string>());
createEffect(() => {
console.log(ids());
});
let i = 0;
setInterval(() => {
setIds(new Set([...ids(), String(i )]));
}, 1000);
Or you can force an update even if the value remains same by passing { equals: false }
:
const [ids, setIds] = createSignal(new Set<string>(), { equals: false });
Now signal always update:
createEffect(() => {
console.log(ids());
});
let i = 0;
setInterval(() => {
ids().add(String(i ));
setIds(ids()); // Not a new value
}, 1000);