Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[🐞] Qwik doesnt check if-statements before running child useComputed$ or useTask$ #4332

Closed
DustinJSilk opened this issue May 25, 2023 · 30 comments · Fixed by #7088
Closed
Assignees

Comments

@DustinJSilk
Copy link
Contributor

DustinJSilk commented May 25, 2023

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.

import { component$, useSignal, useTask$ } from "@builder.io/qwik";

// A useTask$ in a child components circumvents the parent components conditional checks
const Child = component$((props: { val: string }) => {
  useTask$(({ track }) => {
    track(() => props.val)
  });
  return <>{props.val}</>;
});

export default component$(() => {
  const sig = useSignal<{ data: string } | undefined>({ data: '' });

  return (
    <>
      <button onClick$={() => (sig.value = sig.value ? undefined : { data: '' })}>Toggle</button>

      {/* ERROR: cannot read data of undefined */}
      {sig.value && <Child val={sig.value.data} />}
    </>
  );
});

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

System:
    OS: macOS 12.0.1
    CPU: (12) x64 Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
    Memory: 116.35 MB / 16.00 GB
    Shell: 5.8 - /bin/zsh
  Binaries:
    Node: 18.12.1 - ~/.nvm/versions/node/v18.12.1/bin/node
    Yarn: 1.22.18 - /usr/local/bin/yarn
    npm: 7.13.0 - /usr/local/bin/npm
  Browsers:
    Chrome: 113.0.5672.126
    Firefox: 113.0.1
    Safari: 15.1
  npmPackages:
    @builder.io/qwik: ^1.1.4 => 1.1.4 
    @builder.io/qwik-city: ^1.1.4 => 1.1.4 
    undici: 5.22.1 => 5.22.1 
    vite: 4.3.5 => 4.3.5

Additional Information

No response

@DustinJSilk DustinJSilk added TYPE: bug Something isn't working STATUS-1: needs triage New issue which needs to be triaged labels May 25, 2023
@DustinJSilk DustinJSilk changed the title [🐞] Qwik doesnt check ?? before rendering child useComputed$, causing crash [🐞] Qwik doesnt check && before rendering child useComputed$, causing crash May 25, 2023
@DustinJSilk

This comment was marked as outdated.

@manucorporat manucorporat added the P4: urgent If Bug - it makes Qwik unstable and affects the majority of users label May 25, 2023
@DustinJSilk

This comment was marked as outdated.

@DustinJSilk DustinJSilk changed the title [🐞] Qwik doesnt check && before rendering child useComputed$, causing crash [🐞] Qwik doesnt check if-statements before rendering child useComputed$, causing crash Jun 1, 2023
@DustinJSilk

This comment was marked as outdated.

@DustinJSilk DustinJSilk changed the title [🐞] Qwik doesnt check if-statements before rendering child useComputed$, causing crash [🐞] Qwik doesnt check if-statements before running child useComputed$ or useTask$ Jun 7, 2023
@DustinJSilk

This comment was marked as outdated.

@DustinJSilk
Copy link
Contributor Author

DustinJSilk commented Oct 16, 2023

I keep running into this issue, am I doing something wrong here?

@wmertens
Copy link
Member

@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.

@DustinJSilk
Copy link
Contributor Author

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 :)

@wmertens
Copy link
Member

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.

@DustinJSilk
Copy link
Contributor Author

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?

@wmertens
Copy link
Member

wmertens commented Oct 16, 2023

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.

@DustinJSilk
Copy link
Contributor Author

DustinJSilk commented Oct 17, 2023

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?

@cmbartschat
Copy link
Contributor

cmbartschat commented Oct 17, 2023

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.

@wmertens
Copy link
Member

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.

@DustinJSilk
Copy link
Contributor Author

@manucorporat @mhevery is there anything we can do about this issue?

@mhevery
Copy link
Contributor

mhevery commented Oct 19, 2023

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.

@wmertens
Copy link
Member

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

@mhevery
Copy link
Contributor

mhevery commented Oct 19, 2023

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.

@DustinJSilk
Copy link
Contributor Author

Thanks @mhevery and @wmertens i appreciate you guys taking the time to think about this one!

@brandonpittman
Copy link
Contributor

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.

@wmertens
Copy link
Member

@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.

@brandonpittman
Copy link
Contributor

brandonpittman commented Dec 26, 2023

@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?

@wmertens
Copy link
Member

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.

@wmertens wmertens added P2: important and removed P4: urgent If Bug - it makes Qwik unstable and affects the majority of users STATUS-1: needs triage New issue which needs to be triaged labels Jan 19, 2024
Varixo added a commit to Varixo/qwik that referenced this issue Mar 10, 2024
mhevery pushed a commit that referenced this issue Mar 10, 2024
@DustinJSilk
Copy link
Contributor Author

DustinJSilk commented May 20, 2024

Looks like this will be fixed in V2 🫨❤️
Is there a roadmap for V2 at all?

@tri2820
Copy link

tri2820 commented Aug 19, 2024

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.

@berli888
Copy link
Contributor

how is this a bug ? according to documentation, props are immutable.
https://qwik.dev/docs/components/overview/#props

Props are shallowly immutable, meaning primitive data types (strings, numbers, booleans) cannot be changed once passed. However, internal elements of reference types (objects, arrays, functions) can be changed despite the reference itself being immutable.

The only way I found to overcome this limitation is to pass the signal directly and run again the condition into the child component.

@DustinJSilk
Copy link
Contributor Author

Yep, currently you'll need to add the conditions and handle the failures in the child components. Im sure once v2 is out there will be capacity to have this resolved.

@Varixo
Copy link
Member

Varixo commented Dec 20, 2024

should be fixed by #7088 in v2

@Varixo Varixo closed this as completed Dec 20, 2024
@github-project-automation github-project-automation bot moved this from In progress to Done in Qwik Development Dec 20, 2024
@DustinJSilk
Copy link
Contributor Author

You are a HERO @Varixo. I’ll start testing the alpha versions with my app

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Archived in project
Development

Successfully merging a pull request may close this issue.

10 participants