1

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" class="overflow-auto" style="max-height: 550px;">
            <For each={arr}>
                {(number) => {
                    return (
                        <p id={`${number}`}>
                            {number}
                        </p>
                    )
                }}
            </For>
        </div>
    );
};

render(() => <App />, document.getElementById('root') as HTMLElement);

Display name
  • 753
  • 10
  • 28
  • 1
    I think this has been already answered here: https://stackoverflow.com/questions/72297265/conditional-styling-in-solidjs In short, signals in solid are relying on immutability by default. Hence you cannot just mutate the set instance — you have to recreate it to cause updates. – thetarnav May 21 '22 at 16:16
  • @thetarnav - you are correct - that was the solution. I do not pretend to understand why it must be recreated in this manner but at-least now i know the rule. thank you! – Display name May 21 '22 at 17:27
  • 1
    Reading about [signal options](https://www.solidjs.com/docs/latest/api#options) may help with understanding it. Signals have an `equals` function, with which — after every setter call — they assert if the value has actually been changed, and subscribers (computations) need to be notified. By default that function is `(a, b) => a === b`. So having a signal of value `5` won't be "updated" if assigned to `5` again. But this work in the same way on class instances, if you only mutate the instance — you are gonna have a reference to that instance of both sides of the equality check. – thetarnav May 21 '22 at 17:43

1 Answers1

3

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 re-running effects by passing { equals: false }, even though update is not triggered:

const [ids, setIds] = createSignal(new Set<string>(), { equals: false });

Now effects will always be called:

createEffect(() => {
  console.log(ids());
});

let i = 0;
setInterval(() => {
  ids().add(String(i++));
  setIds(ids()); // Not a new value
}, 1000);
snnsnn
  • 10,486
  • 4
  • 39
  • 44