solidjs / solid

A declarative, efficient, and flexible JavaScript library for building user interfaces.
https://solidjs.com
MIT License
32.17k stars 919 forks source link

The same derived signal used in an effect, a memo, or JSX produces difference values #2221

Closed jsimonrichard closed 2 months ago

jsimonrichard commented 2 months ago

Describe the bug

I am trying to create an input that dynamically changes its padding based on the element included to the left of the input text (kind of like this component, but dynamic: https://tailwindui.com/components/application-ui/forms/input-groups#component-2607d970262ada86428f063c72b1e7bd). However, sometimes this element isn't displayed (display: none), which means I can't just calculate the size on mount using offsetWidth.

I was able to create a derived ref that calculates the correct size when the display mode is controlled by another signal (note the log statements in the effect in the MRE). However, if this is wrapped in a memo, the reported value changes. I'm probably doing something wrong here, but I'm opening this as an issue because I would not expect memoizing a value to change that value. In addition, that memoized value reflects what's actually used in the DOM regardless of whether the value specified in the JSX is memoized (in the MRE, I'm not actually using the memoized value in the JSX, but I'm still getting 0px).

I hope this MRE is sufficient. I would have liked to make it more minimal, but I really don't know what is essential here.

Your Example Website or App

https://stackblitz.com/edit/solidjs-templates-jmnrua?file=src%2FApp.tsx

Steps to Reproduce the Bug or Issue

  1. Click on the "Display" button
  2. Observe that the left padding on the input is not equal to the width of the span containing "asdf"
  3. Optionally toggle visibility with the same button
  4. Open "Inspect Element" and observe the difference in values between inputPadding and inputPaddingMemoized

Expected behavior

As a user, I expect derived signals and memos to behave the same when only used once in reactive context. In addition, I expect the value of a derived signal inside an effect to be the same as the value of that same signal when used in JSX.

Screenshots or Videos

No response

Platform

Additional context

No response

Brendan-csel commented 2 months ago

Things that call the inputPadding function will ultimately get called again when isDisplayed is toggled BUT that doesn't mean the leftContainer()!.offsetWidth has resized yet. In fact, your example shows it hasn't - and since offsetWidth isn't a reactive property it won't trigger an update when it does resize.

The console logged effect runs later, after the rendering updates, so it just so happens that offsetWidth has changed by that time.

You may have more luck using resizeObserver to watch the size of leftContainer instead. There is a solid-primitive that might help you with that.

EDIT: Oh and regarding the createMemo part of your question. That will also only rerun when isDisplayed toggles - and it will be remembering the 0px measurement and returning that when the effect asks for it later. It too doesn't know that offsetWidth has changed in the meantime.

jsimonrichard commented 2 months ago

Ah, I see now. Thanks for the detailed explanation!

Using createResizeObserver from solid-primitive works great. For anyone else looking at this later, my solution looks something like this:

const [leftContainer, setLeftContainer] = createSignal();
const [leftWidth, setLeftWidth] = createSignal();

createEffect(() => {
  if (!isDisplayed() || !leftContainer()) {
    setLeftWidth(undefined);
    return;
  }
  createResizeObserver(
    leftContainer(),
    ({ width }) => {
      setLeftWidth(width + 'px');
    },
    { box: 'border-box' },
  );
});