Open DustinJSilk opened 1 year ago
If we update the child component to this, we see that the error is fixed but the useComputed$ still runs when it shouldnt and we go against the type system as someValue should not be undefined:
const Child = component$((props: { someValue: number[] }) => {
const max = useComputed$(() => {
console.log("Computing");
return Math.max(...(props.someValue ?? [1, 2]));
});
return <div>{max.value}</div>;
});
@manucorporat sorry to be a pain, any chance you could take a look at this or know a work around? 🙏
Unfortunately i dont know qwik well enough to fix it myself (tho i have tried!) and I haven't found a way around it yet either.
@manucorporat Ive added an e2e test to the reproduction repository here and here, hopefully that helps simulating and testing a fix for this issue.
Let me know if there's anything I can do to help here!
The issue also occurs when using a useTask$
. You can see the console.log prints undefined
when it shouldnt run at all. Swapping undefined for null doesn't help and I havent found a way around it yet.
import { component$, useSignal, useTask$ } from "@builder.io/qwik";
const Child = component$((props: { someValue: number[] }) => {
const data = useSignal(props.someValue);
useTask$(({ track }) => {
track(() => props.someValue);
data.value = props.someValue;
console.log(props.someValue);
});
return <div>{data.value}</div>;
});
export default component$(() => {
const data = useSignal<number[] | undefined>([1, 2, 3, 4, 5]);
return (
<>
<div>{data.value && <Child someValue={data.value} />}</div>
<button onClick$={() => (data.value = undefined)}>Click me</button>
</>
);
});
I keep running into this issue, am I doing something wrong here?
I wish this bug had more attention!
Here's a playground example just to show how bananas this behavior is 🙈
@cmbartschat I don't understand, what are you demostrating? You're throwing on purpose there?
The "problem" is that the {sig.value &&
runs at a different time than the updating of the passed signal prop function sig.value.data
.
I don't consider this a bug per se, it's more of an unexpected result of splitting your app in tiny parts that update via signals.
Why wouldn’t it be a bug? Signals are unavoidable and so is splitting an app into smaller parts. So surely it’s a bug if the result is unexpected? Please let me know if I’m missing something fundamental here :)
Hmm.
So a solution would be that the optimizer keeps track of statements that cause a signal to be forwarded, converting then to a filter function, right? And it should first call the filter function and only then the signal function?
I wonder how deep this rabbit hole goes, what statements should be converted etc.
To be honest, I don’t know what a solution would look like, I don’t know the inner workings of qwik or what the various trade offs would be.
Do you know if any other framework has solved or suffers from the same issue?
I don't know of any other framework that converts values into signals and re-renders asynchronously thanks to that.
The workaround is to add a ? after signal.value, which the linter doesn't complain about because it doesn't know that it will become a separate function called separately.
For me this feels like a P1 issue, maybe P2 because it's unintuitive.
Yep so the only real work around is to avoid the conditional completely, it kind of is bananas lol
Is there any way we can get this issue escalated?
Sorry for the cryptic example!
What I was trying to show is that based on the contract of the Child
component, as long as you don't pass "bananas", everything is fine. And naively, it does seem like we're following that contract in that example.
Here's another example to show the sorts of things a real world app might try to do.
I would describe the problem simply that type narrowing does not work on signals.
No matter what we write in the render logic, at runtime, the signal is not type narrowed, because it gets flushed through before the narrowing logic can run.
This can be a source of bugs that Typescript cannot detect.
Ok your real-world example is convincing. It's easy to see that this could lead to frustrating debug sessions.
So indeed I think the proper solution is that the optimizer supports filter functions before updating prop signals. But I don't think that's trivial to add.
@manucorporat @mhevery is there anything we can do about this issue?
Yeah, this is a hard one. I need to dig deeper into this to see what a fix could be.
The issue is that the order in which we process events is not preserved.
Does this happen in other situations than when signals are proxied in props by the optimizer? If not, I believe a filter function should work
Does this happen in other situations than when signals are proxied in props by the optimizer? If not, I believe a filter function should work
I have seen other issues on this topic, but I can't find them now.
The issue is that the effects should run in the correct order and then prune its children. I think the code should work as written, but we don't process the effects in the correct order which results in this issue. Happy to have a chat if you want to dig deeper into this.
Thanks @mhevery and @wmertens i appreciate you guys taking the time to think about this one!
I'm getting a similar issue where due to conditional rendering I'm getting two of the same component in the DOM—one hidden and one not hidden. Both components' useTask$
calls are being fired when the signal value they're tracking changes. Since I'm updating a store value in that task, a double store value change happens.
@brandonpittman that's by design, because children and parents render independently.
So if you render items in slots, they will always render and update even when the slot is not visible.
@wmertens
So if you render items in slots, they will always render and update even when the slot is not visible.
That was the conclusion I came to. I created a <Show>
component similar to Solid.js' but it's causing more trouble than it's worth. I wanted to embed the show/hide logic inside child components, but it seems like I can't do that without unintended consequences.
If the projected content isn't displayed at render, it'll go into a q:template
. And if that project content is subject to dynamic updates that change the structure of the slotted HTML, it's possible the initial server q:template
can exist in the DOM next to a dynamically created element that gets placed in the slot?
If the projected content isn't displayed at render, it'll go into a q:template. And if that project content is subject to dynamic updates that change the structure of the slotted HTML, it's possible the initial server q:template can exist in the DOM next to a dynamically created element that gets placed in the slot?
Uhm, yes? Not quite sure what you mean, a playground repro would be nice.
Looks like this will be fixed in V2 🫨❤️ Is there a roadmap for V2 at all?
This simple example shows how this bug breaks code that appears to work in Qwik. TypeScript does not show any warnings or errors either.
For useTask$
, when I switch to useVisibleTask$
it works (because all input signals are correctly their type at that point).
For useComputed$
, could the solution be to allow it to run after rendering, like useVisibleTask$
?
const _ = useVisibleComputed$(() => {
})
This way the computation is not async anymore, but at least it's a safe alternative to circumvent situation like this.
Which component is affected?
Qwik Runtime
Describe the bug
The following code causes Qwik to crash when the button is clicked. Conditionals aren't checked before child component tasks are run when using Signals.
Reproduction
https://github.com/DustinJSilk/qwik-issue-rendering/blob/main/src/routes/index.tsx
Steps to reproduce
Run the app Click the button
System Info
Additional Information
No response