S
SolidJS6mo ago
adrien

Lazy-initialized createMemo's updating once, but not again

I'm trying to debug this createMemo issue I'm running into. What I'm really trying to get at is a nested memoized object, but given that something like that doesn't appear to exist in the API as is, this is how I'm doing it. I threw together some sample code (I'll leave it as the next comment) to show a minimal example of the issue I'm running into. When you click either of the counter buttons, the count increments, and the fibonacci memoized value for that counter updates. But then any subsequent increment, the fibonacci value does not update. So it appears that the memoized computation performs properly for the initial state, and for the 2nd state (which implies that the dependencies are being inferred properly). But then it just stops working after that. In the real-world thing I'm working on that inspired this sample, there could be thousands of items (counters in this example), and only one might be looked at at a time. So I'd like for the memoized computation to be lazily initialized when needed (so that 1000 computations aren't kicked off when they're not needed), and for the memoized computation to be remembered if the view changes to another "counter", and then back to a previous one, and so that the memoized result can be shared between different views that have access to the memoized computation. Would love to understand why I'm seeing this odd behavior of the memoized computation working for the first 2 states, but then not after that. Thanks!
19 Replies
adrien
adrien6mo ago
Sample code:
import { Accessor, Component, For, createMemo } from "solid-js";
import { createStore } from "solid-js/store";

function fibonacci(num: number): number {
if (num <= 1) return 1;

return fibonacci(num - 1) + fibonacci(num - 2);
}

const MemoSample: Component = () => {
const [counts, setCounts] = createStore<{ [key: string]: number }>({
a: 3,
b: 5,
});

const fibByCounterKey: { [key: string]: Accessor<number> } = {};

const getFibForCounter = (key: string): number => {
let fibForCounter = fibByCounterKey[key];

if (!fibForCounter) {
fibForCounter = fibByCounterKey[key] = createMemo(() => {
const count = counts[key];

if (count === undefined) {
throw new Error();
}

return fibonacci(count);
});
}

return fibForCounter();
};

return (
<For each={Object.keys(counts)}>
{(key) => (
<>
<button onClick={() => setCounts(key, (v) => v + 1)}>
Counter {key}: {counts[key]}
</button>
<div>
{getFibForCounter(key)} {getFibForCounter(key)}&nbsp;
{getFibForCounter(key)} {getFibForCounter(key)}&nbsp;
{getFibForCounter(key)} {getFibForCounter(key)}&nbsp;
{getFibForCounter(key)} {getFibForCounter(key)}&nbsp;
</div>
</>
)}
</For>
);
};

export default MemoSample;
import { Accessor, Component, For, createMemo } from "solid-js";
import { createStore } from "solid-js/store";

function fibonacci(num: number): number {
if (num <= 1) return 1;

return fibonacci(num - 1) + fibonacci(num - 2);
}

const MemoSample: Component = () => {
const [counts, setCounts] = createStore<{ [key: string]: number }>({
a: 3,
b: 5,
});

const fibByCounterKey: { [key: string]: Accessor<number> } = {};

const getFibForCounter = (key: string): number => {
let fibForCounter = fibByCounterKey[key];

if (!fibForCounter) {
fibForCounter = fibByCounterKey[key] = createMemo(() => {
const count = counts[key];

if (count === undefined) {
throw new Error();
}

return fibonacci(count);
});
}

return fibForCounter();
};

return (
<For each={Object.keys(counts)}>
{(key) => (
<>
<button onClick={() => setCounts(key, (v) => v + 1)}>
Counter {key}: {counts[key]}
</button>
<div>
{getFibForCounter(key)} {getFibForCounter(key)}&nbsp;
{getFibForCounter(key)} {getFibForCounter(key)}&nbsp;
{getFibForCounter(key)} {getFibForCounter(key)}&nbsp;
{getFibForCounter(key)} {getFibForCounter(key)}&nbsp;
</div>
</>
)}
</For>
);
};

export default MemoSample;
bigmistqke
bigmistqke6mo ago
mm a bit unusual codestyle with the memo inside the function not sure it is actually doing anything the bug has something to do with cleanups, but don't really understand why tbh
bigmistqke
bigmistqke6mo ago
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
bigmistqke
bigmistqke6mo ago
as in even if it would work i don't think it would actually be memozing the function since you recreate the memo-function with each time you call getFibForCounter
adrien
adrien6mo ago
Thanks for looking into this @bigmistqke !! I'll try out your alternate patterns for now. But I am still curious about why this other pattern isn't working. You're saying that the memo-function is recreated each time getFibForCounter is called, but I don't believe that's true. This is how I'm interpreting what's going on: - getFibForCounter("a") 1st call: the memo-function is created and stored in fibByCounterKey["a"] because fibByCounterKey["a"] isn't defined yet. - getFibForCounter("a") 2nd call: the memo-function that was created in 1st step is retrieved from fibByCounterKey["a"] because fibByCounterKey["a"] is defined now. Am I missing something? Thanks for helping me understand! Would really love to get why this pattern isn't working! I guess I'm really wishing there was something like createMemo but parameterized. Like, in the way I could make a parameterized derived signal like this (I know that's technically not an Accessor, but it still works right):
const doubleCounter = (key: string) => counters[key] * 2;
const doubleCounter = (key: string) => counters[key] * 2;
I wish I could make a memoized parameterized derived signal, where it would key its memoized values by the parameters. And specifically I'd like to understand why the original code appears to work for initial state AND 1st update, but not any subsequent updates. I could rationalize it away if it didn't work for any updates, but it just seems odd that it stop after the 1st update.
adrien
adrien6mo ago
Oooh ok so going off of my first code sample, here is a slight alteration of it where I call getFibForCounter("a") in the Component function for the "a" counter, but not the "b" counter. So this initializes the memo-function for "a" before the return statement, but not for "b". And now, it works for the "a" counter, but not for "b". https://playground.solidjs.com/anonymous/8d6d68ec-60b5-4ca5-b4a3-7081d33badba
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
adrien
adrien6mo ago
So I wonder, is there something incorrect about using createMemo inside a reactive frame (I don't know what to call this)? Like it seems like the memo works correctly when initialized in the Component function (during setup as opposed to during render I guess?). But is acting strange when the memo is initialized in a reactive render? But if that's the case, I would expect Solid to yell at me for doing that, the same way it yells at me for creating computations outside of a render context. Should Solid be able to recognize like "hey, we're in a context where using createMemo doesn't work, here's a warning", the same way it can when you're not in the proper context?
bigmistqke
bigmistqke6mo ago
I wish I could make a memoized parameterized derived signal, where it would key its memoized values by the parameters.
yes, you can at least mapArray and indexArray can provide it for you for arrays and you can twist it to work for objects too
You're saying that the memo-function is recreated each time getFibForCounter is called, but I don't believe that's true.
ye u right missed the cache part
So I wonder, is there something incorrect about using createMemo inside a reactive frame (I don't know what to call this)? Like it seems like the memo works correctly when initialized in the Component function (during setup as opposed to during render I guess?). But is acting strange when the memo is initialized in a reactive render?
ye it has something to do with cleanup, but i am not 100% sure why
bigmistqke
bigmistqke6mo ago
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
bigmistqke
bigmistqke6mo ago
🤷‍♂️
adrien
adrien6mo ago
WHOA! So just returning the accessor instead of the unwrapped value from getFibForCounter, right?? Ok I LOVE that that works, but I can't comprehend how it works....?? I mean it's literally doing the same thing, just moving the unwrapping up to the caller. Right?? What am I missing here?
adrien
adrien6mo ago
All right I've got another one to throw at this quandary! https://playground.solidjs.com/anonymous/13b9938d-f7b5-4227-9b58-f32c58d0c272 I set up A and B the exact same way, and log the result of the lazy memo for each in a createEffect. The only difference is I also render the result of the lazy memo of B in the DOM. The result is that the effect for A only triggers on the first update, and not on any subsequent update. But the effect for B triggers on every update. My expectation is that A's effect should also trigger on each update, so I'm wondering why that's not happening??
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
adrien
adrien6mo ago
Here's a simplified version using 2 signals instead of a store: https://playground.solidjs.com/anonymous/7ff5df2b-02a5-4385-a669-d4bc44d2c99f Same issue as before. And also something interesting (happens in both versions), for B, the bMemo and bLazyMemo effects seem to alternate which order they get triggered in on each change. Not sure why, but that could point toward the reason for this strange behavior.
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
bigmistqke
bigmistqke6mo ago
ye interesting puzzle 🤔
bigmistqke
bigmistqke6mo ago
GitHub
solid-primitives/packages/memo at main · solidjs-community/solid-pr...
A library of high-quality primitives that extend SolidJS reactivity. - solidjs-community/solid-primitives
bigmistqke
bigmistqke6mo ago
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
bigmistqke
bigmistqke6mo ago
ok so i think i know why it doesn't work: https://playground.solidjs.com/anonymous/1ee6512d-a859-4cd4-926f-87a9465ef710 you see when you press it how memo is cleaned up twice? that's because, while the declaration is happening outside of the effect, when you execute the memo, you are doing it inside the createEffect. I am going to give a bit of a botched explanation, so bear with me. In solid there is this concept of ownership. It's a mechanism that is used to automatically cleanup effects. It's quite central to solid as a framework, but not super documented. There was a nice read-up by someone in this discord, i'll see if i can find it. So, because that memo is executed within the effect, it is being cleaned up by that effect the first time it is being run. That's why it is being run the first time, but doesn't update afterwards.
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
bigmistqke
bigmistqke6mo ago
There was a nice read-up by someone in this discord, i'll see if i can find it.
found it: https://special-page-159.notion.site/Solid-Deep-Dive-7fd2d047d54a4e3ea60b4c2597869286
Oren Elbaum's Notion on Notion
Solid Deep Dive
Table of Contents
adrien
adrien6mo ago
Oh man this is awesome, thank you for explaining and digging that up! I'm gonna take some time tomorrow to try to digest this. Thanks also for pointing me to createLazyMemo and the Solid Primitives project, didn't know about that!!