1

I have the following code. The problem is this, inside of the createEffect that listens to topic changes, I want to get new chat messages (which is just an array of randomly generated integers to make this simple), after getting the messages, I want the containerDiv to redraw with the latest messages, and then scroll to the bottom of the div.

The problem is this line doesn't seem to immediately redraw the div:

setMessages(chat);

Causing the scroll to the bottom to be inaccurate, because the scroll is done before the div redraws, hence, sometimes the scroll is not really at the bottom.

How do I fix this so that the setMessages redraw the div, and wait until that redraw is done before I set the scroll on containerDiv, so that the scroll is always at the bottom everytime the messages update?

I'm not sure if I miss anything or there is any bug in the code. But I expect setMessages to immediately redraw the div, before proceed to the next line of the code.

import { For, createEffect, on, type VoidComponent } from "solid-js";
import { createSignal } from "solid-js";
import { createStore } from "solid-js/store";

function getRandomInt(min: number, max: number) {
  min = Math.ceil(min);
  max = Math.floor(max);
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

function getTopicChat() {
  const n = getRandomInt(12, 24);
  return Array.from(Array(n).keys());
}

const Home: VoidComponent = () => {
  let containerDiv!: HTMLDivElement;

  const [topic, setTopic] = createSignal("TOPIC 1");
  const [messages, setMessages] = createStore<number[]>([]);

  createEffect(
    on(topic, async () => {
      const chat = getTopicChat();
      setMessages(chat);
      containerDiv.scroll({ top: containerDiv.scrollHeight });
    })
  );

  return (
    <main class="h-screen">
      <div class="flex justify-center">
        <button
          onClick={() => setTopic("Topic " + getRandomInt(2, 99).toString())}
          class="bg-red-100 py-2 px-4 rounded-md my-2"
        >
          Change topic
        </button>
      </div>

      <div class="text-center">
        <p>{topic()}</p>
      </div>

      <div class="mx-20 overflow-y-auto h-96" ref={containerDiv}>
        <For each={messages}>
          {(i) => <div class="bg-green-200 h-40 my-4">{i}</div>}
        </For>
      </div>
    </main>
  );
};

export default Home;

I have tried to use createRenderEffect to update the messages and the scroll.

  createRenderEffect(
    on(topic, () => {
      const chat = getTopicChat();
      setMessages(chat);
      if (containerDiv) {
        containerDiv.scroll({ top: containerDiv.scrollHeight });
      }
    })
  );

But with createRenderEffect, the messages on the div doesn't seem to update correctly.

Thanks in advance for any help.

Edit

I found 2 solutions:

Solution 1: Execute the scroll after a 0ms timeout

This works after I put the scroll inside a setTimeout

setTimeout(() => containerDiv.scroll({ top: containerDiv.scrollHeight }), 0)

Related question: Why is setTimeout(fn, 0) sometimes useful?

Solution 2: Wrap the messages updating in async

  createEffect(
    on(topic, async () => {
      const update = async () => {
        const chat = getTopicChat();
        setMessages(chat);
      };
      await update();
      containerDiv.scrollTo(0, containerDiv.scrollHeight);
    })
  );

instead of

  createEffect(
    on(topic, () => {
      const chat = getTopicChat();
      setMessages(chat);
      containerDiv.scrollTo(0, containerDiv.scrollHeight);
    })
  );

But am not sure if there is other better solution.

snnsnn
  • 10,486
  • 4
  • 39
  • 44
Max
  • 81
  • 1
  • 5

1 Answers1

0

Solid runs synchronously. When the state updates a new div is created because you are setting new values while containerDiv points to the old div element. You can toggle developer tools to see if div element with ref={containerDiv} is flashing, an indication of re-insert.

setTimeout 0 works because you scroll after the element is created and inserted.

It is best to listen to reactive elements directly, so your effect is a bit problematic in that sense.

Store uses a proxy internally so it relies on getters for reactivity, that is why you can not listen to messages directly, but you can use its properties. For example:

createRenderEffect(
  on(
    () => messages.length,
    () => {
      console.log(messages);
    },
  ),
);
snnsnn
  • 10,486
  • 4
  • 39
  • 44