QwikDev / qwik

Instant-loading web apps, without effort
https://qwik.dev
MIT License
20.76k stars 1.29k forks source link

[✨] - Built-in Conditionals #2678

Closed jdgamble555 closed 7 months ago

jdgamble555 commented 1 year ago

Is your feature request related to a problem?

Yes, JSX does not make conditionals easy. If statements need ternary or && operators, arrays need maps, and complex switch statements are not possible without creating custom components. JSX is just not as simple as say:

Describe the solution you'd like

In a perfect world, I would like to NOT use JSX for these condtional statements, and add the ability to do things like this mixed with JSX:

Similar to Svelte

{if me === true}
  <Header />
{/if}
{if you === 'him'}
  <Component1 />
{else if love === 'hate'}
  <Component2 />
{else}
  <Component3 />
{/if}
{if posts}
  {for posts as post, i, key=post.id}
    <Post {key} {post} />
  {/for}
{/if}

OR

Similar to Vue

<q-if={me === you}>
  <Component1 />
<q-else-if={boy === girl}>
  <Component2 />
</q-if>
<q-for={posts as post} index={i} key={post.id}>
  <Post {key} {post />
</q-for>

Describe alternatives you've considered

If that is not feasible to break JSX specs (not sure the exact spects), you may just need to write your own JSX components that would be a default part of every project.

Ideas:

I would probably write my own components like this:

<qk if={me === you}>
  <Component1 />   
</qk>
<qk switch={condtion}>
  <qk case={case1}>
    <Home />
  </qk>
  <qk case={cas2}>
    <Component1 />
  </qk>
  <qk default>
    <Default-component />
  </qk>
</qk>
<!-- default value for key is 'key' -->
<qk-for={posts} key={'key}'>
  {post => <Post {post} {key} />}
</qk-for>

<!-- honestly not sure this is much better than map -->

Under the hood should not use map, and should use for, as it has been proven to be faster than map.

Additional context

I understand that most programmers know JSX, so JSX is better for React users to learn Qwik, however... popularity should not be confused with facility. Svelte, by far, has mastered this. JSX just does not make basic conditionals and loops easy to use. Vue is second best.

If we have to stick with JSX, let's create the easiest to use jsx-compliant components to be the next Qwik standard, like SolidJS did. I could write my own, but I don't want to if there is a "qwik way" of doing things.

J

manucorporat commented 1 year ago

I am not sure of the value of Qwik providing this:

Until there is a good reason for us to do this, i think this should be provided by a third party library.

manucorporat commented 1 year ago

All the problems and pros and const described just reflect that this might not be a good idea, since we dont have any of these problems today. Adding a <If> will be just a source of:

<If condition={expr} else={() => <div> False condition </div>}>
   {() => <div> True condition </div>}
</If>

The thread mentions how Vue, Svelte, and other have this, but it's because they need to have it, they need to reinvent flow control, in JSX you dont:

{expr
  ? <div> True condition </div>
  : <div> False condition </div>
}
keuller commented 1 year ago

@manucorporat TBH, I don't think it is a negative point for Solid and also if Qwik can provide such a feature could be an option to use it. I agree it is not required, if the developer wants to use JS approach not a problem, otherwise Qwik can provide a JSX approach as well. There are many developers that prefer using JS instead of JSX as well as there are other that prefer JSX instead of JS. Again, it is my opinion, but if everyone agrees that is not a good achievement for Qwik and should be a third-party library, no problem let's move it to a third-party lib.

adamdbradley commented 1 year ago

While JSX isn't perfect, it's a known standard we should avoid adding our own custom rules to. How to do conditionals in JSX is already widely documented and taught in countless tutorials, blogs and courses, and having to teach a new way for just Qwik is something I don't think is worth the effort.

Each of the ideas have just as many pros/cons as standard JSX conditionals, so adding this to the core may have little, if any, gain to Qwik's readability. Since there's no direct consensus on how to do this, or if we even should, and given that JSX developers have been using what's available I think we shelve this until it becomes a problem.

manucorporat commented 1 year ago

if the developer wants to use JS approach not a problem, otherwise Qwik can provide a JSX approach as well.

in fact it is a problem, optimizer does custom JSX transform to improve rendering performance, not using JSX is a very BAD idea in Qwik, and makes it harder to innovate and keep improving. The fact that is possible does not mean we want to make it easier for them, Qwik phylosophy is to reduce footguns, not to please syntax preferences.

Also, more options make sense for advanced users, for new users is more APIs to learn, more questions, more confusion. this has implications on what Qwik providers out of the box or not.

I agree that some flow controls are not great in JSX today, like a switch, but i am not sure it should be provided by Qwik core. I think we need to agree first of what APIs:

jdgamble555 commented 1 year ago

@adam-r-kowalski - I would like to know more about your experiences, not sure what you mean about bias here.

@manucorporat @adamdbradley

I don't agree with your arguments at all, but I agree with what I think you guys are trying to say.

So I definitely started this thread hoping to get a Qwik version of functional logic, not like SolidJS. It turns out that was not an option. However, if you read the whole thread, you will see we have definitely agreed upon a way for this to be implemented; mainly because we have parameters and constraints given by Qwik itself and from a Qwik team member.

Here are facts and opinions most people share:

However...

I honestly don't know if I will use it. I agree that it may be more of the same and not much easier for experienced users. Again, I started this thread hoping for a non-JSX way of doing things (built-in to Qwik), which seems to not be in the cards. That would be a whole different story. Using a map is probably easier than importing another component and adding extra anonymous functions. && and ternary operators are probably easier for the same reason. Multiple if / else statements have never been easy in JSX, so there is perhaps real value in the Switch.

The deciding factor should be from the community IMHO. The only real argument is "who will use it," and "is it overkill?"

🤷🏻‍♂️

I'm only invested on using good code if it is implemented at this point. We should get more opinions on whether or not it SHOULD be implemented.

my 2c

J

adam-r-kowalski commented 1 year ago

@manucorporat I'm actually completely on board with not having these control flow primitives and would rather just use ternaries but it seems like that isn't supported as well as it should in my opinion. For example suppose you have:

export const Toggle = component$((props: { on: boolean }) => {
    return props.on ? <div>on</div> : <div>off</div>
})

export const Example = component$(() => {
    return <Toggle on={false} />
})

Which gives

Components in Qwik must have a single JSX root element.
Your component has multiple roots on lines: 130, 130.
Rewrite your component with a single root such as (`return <>{...}</>`.)
and keep all JSX within

So you need to rewrite to

export const Toggle = component$((props: { on: boolean }) => {
    return <>{props.on ? <div>on</div> : <div>off</div>}</>
})

Which in my opinion has added ceremony and it seems like there is a "single root" even with the primary example.

This problem gets shown even more clearly if you just want to conditionally show something.

export const Toggle = component$((props: { on: boolean }) => {
    return <>{props.on ? <div>on</div> : <></>}</>
})

does not seem extremely readable to me. I suppose you can say

export const Toggle = component$((props: { on: boolean }) => {
    return props.on ? <div>on</div> : null
})

which seems to not tigger the error, but that seems unintuitive about why there are differences.

I think the reason why we end up creating primitives like this is because we are trying to program in a functional expression oriented style in a language that is primarily procedural and statement oriented. Since we cannot create new control flow primitives in the language such as pattern matching or conditional expressions, we end up using custom components and lambdas to emulate them.

TLDR: I think going with the JavaScript approach would be great, but we should embrace that notation and ensure it does not require any additional ceremony

adam-r-kowalski commented 1 year ago

@jdgamble555

export const Toggle = component$((props: { on: boolean }) => (
    <If
        condition={props.on}
        else={() => (
            <div>
                <h1>Here the option is off</h1>
                <ul>
                    <li>Option 1</li>
                    <li>Option 2</li>
                    <li>Option 3</li>
                </ul>
            </div>
        )}
    >
        {() => (
            <div>
                <h1>Here the option is on</h1>
                <ul>
                    <li>Option 1</li>
                    <li>Option 2</li>
                    <li>Option 3</li>
                </ul>
            </div>
        )}
    </If>
))

It's not clear where one branch ends and another begins. Secondly why is the else branch first? That seems backwards in my opinion. Here if we compare with a ternary it might be better.

export const Toggle = component$((props: { on: boolean }) => (
    <>
        {props.on ? (
            <div>
                <h1>Here the option is on</h1>
                <ul>
                    <li>Option 1</li>
                    <li>Option 2</li>
                    <li>Option 3</li>
                </ul>
            </div>
        ) : (
            <div>
                <h1>Here the option is off</h1>
                <ul>
                    <li>Option 1</li>
                    <li>Option 2</li>
                    <li>Option 3</li>
                </ul>
            </div>
        )}
    </>
))

I'm not convinced it's a win either way

Either we want

export const Toggle = component$((props: { on: boolean }) =>
    props.on ? (
        <div>
            <h1>Here the option is on</h1>
            <ul>
                <li>Option 1</li>
                <li>Option 2</li>
                <li>Option 3</li>
            </ul>
        </div>
    ) : (
        <div>
            <h1>Here the option is off</h1>
            <ul>
                <li>Option 1</li>
                <li>Option 2</li>
                <li>Option 3</li>
            </ul>
        </div>
    ),
)

or some sort of declarative pattern matching

export const Toggle = component$((props: { on: boolean }) =>
    <Switch>
        <Case when={props.on}>
            <div>
                <h1>Here the option is on</h1>
                <ul>
                    <li>Option 1</li>
                    <li>Option 2</li>
                    <li>Option 3</li>
                </ul>
            </div>
        </Case>
        <Else>
            <div>
                <h1>Here the option is off</h1>
                <ul>
                    <li>Option 1</li>
                    <li>Option 2</li>
                    <li>Option 3</li>
                </ul>
            </div>
        </Else>
    </Switch>
)
jdgamble555 commented 1 year ago

Secondly why is the else branch first? That seems backwards in my opinion.

I don't understand. Would you think else should be in the children between the components? What exactly is the reverse / forward of backwards here?

Your second example doesn't make any sense. Is Toggle supposed to be any random component, or work like a Toggle ? You basically mixed two different concepts, so not sure what you mean here.

Your third example is not much different from your second. Yes, you can only have one fragment in Qwik, but that is not a relevant problem to this post.

Either way, it has to be a function in the children as we talked about in the above posts, or the children will get eagerly executed. Agreed it is not pretty, but that is the reason why we came up with the final code we did. Go back and read this whole post if you have any questions.

Terminology speaking - Switch goes with Case and Default btw, or it should be If, Else If / Elif and Else. You could mix and match where, when, or condition... as that is not a norm people are used to yet.

The best we could do from your suggestions (from what I do understand) would be:

<Condition>
  <If condition="true">
    {() => <>true statement</>}
  </If>
  <ElseIf condition="false">
    {() => <>false statement</>}
  </ElseIf>
  <Else>
    {() => <>null statement</>}
  </Else>
</Condition>

and

<Switch>
  <Case where={false}>
    {() => <>Case is false</>}
  </Case>
  <Case where={true}>
    {() => <>Case is true</>}
  </Case>
  <Default>
    {() => <>Default case</>}
  </Default>
</Switch>

I'm not opposed to something like this, but curious what others think.

J

jdgamble555 commented 1 year ago

Anyone else have opinions on this? It seems interests has dropped.

Also, SolidJS may have rendering reasons for using these custom components:

https://youtu.be/hdUwDmprSmg

Not sure if this is true for Qwik?

J

DustinJSilk commented 1 year ago

Anyone else have opinions on this?

Sure! For me, the functions wrapping children and the wrong order on the if/else feel wrong. I understand the limitations but it’s enough that I’d prefer to stick with existing solutions .

If those could be solved somehow, I’d definitely use them. Even if it was a third party library or we had to copy it into our own code base would be fine

keuller commented 1 year ago

Hey guys, I'm working on it as a 3rd-party library. I'm discussing it with @shairez in order to put it under qwikifers umbrella. If in the future we decide to put it on qwik-core we only move it into the Qwik repo.

jdgamble555 commented 1 year ago

@mhevery - The SolidJS Video I linked above basically says it is advantageous to have these components in a loop, as only the changed array item will re-render instead of the whole array.

Is this true for Qwik?


@keuller @DustinJSilk - Yeah, it will always be impossible to omit the function wrapping as discussed above due to eager rendering. However, it could be possible to move the else to the bottom like I stated above like this:

<Condition>
  <If condition="true">
    {() => <>true statement</>}
  </If>
  <ElseIf condition="false">
    {() => <>false statement</>}
  </ElseIf>
  <Else>
    {() => <>null statement</>}
  </Else>
</Condition>

Here is a simple implementation:

export type CONDITION = (props: {
    children: JSXNode[];
}) => JSXNode;

export type CONDITIONAL = (props: {
    condition?: boolean;
    children: () => JSXNode;
}) => JSXNode;

export const Condition: CONDITION = (props) => {

    const conditions = props.children;

    for (const [i, c] of conditions.entries()) {

        // else (last el)
        if (i === conditions.length - 1) {
            return c.props.children();
        }

        // if condition is true
        if (c.props.condition) {
            return c.props.children();
        }
    }
    return null;
};

export const If: CONDITIONAL = () => <></>;
export const ElseIf: CONDITIONAL = () => <></>;
export const Else: CONDITIONAL = () => <></>;

What are you guy's thoughts on this instead of else as an input?

J

barel-mishal commented 1 year ago

Personally, I really dislike this :-)

Javascript is right there and has tooling and expressivity, and then these approaches take all that away and use different, more verbose and more limited syntaxes to achieve the same thing.

What's more, implementing this in userspace causes extra components to be created and evaluated, just for DX.

So while I understand some may really like this, I would like to ask that any solution does not come at a cost to non-adopters, nor to the end users.

I think the best way forward would be a Vite plugin that pre-processes the source, converting proposed syntax to regular JSX syntax.

I strongly agree with your perspective. Personally, I prefer a more robust type system over any developer experience components. An idea occurred to me while reading the comments here. We could create a separate components library, similar to the primitive repository of Solid.JS. This would enable our community to focus more on the library's major features and the developer experience associated with its components.

Furthermore, we could bundle these components with Qwik in packages. This approach would allow us to enjoy a great developer experience without unnecessarily complicating the Qwik library with components that may not be needed.

jdgamble555 commented 1 year ago

In my opinion, the real problem is that JSX has bad developer syntax and dx to begin with. However, people have grown fond of functional components. This is where svelte makes everything easy... but it's not qwik :)... and I digress...

I completely agree with this. I could see an npm run qwik add dx-tools or something similar.

Would definitely be cool, and I vote for this.

J

shairez commented 7 months ago

Thanks to @keuller we already have a community project with a PR waiting for review

https://github.com/qwikifiers/qwik-flow/pull/1/files#diff-ef3a7707aac2e56721f0b1a4dec5458ec9308bf58f9a616ce3f22fe908b53295

If we'll see more demand for this we might consider adding it in the future or move it to "qwik labs", but we are curious to see how much traction will that have.

So I'm closing this for now

If anyone wants to help with getting this released, please DM me on discord

jdgamble555 commented 7 months ago

@shairez, so this is problematic. The types have changed in Qwik since last year. The Switch Case does not have a default value. It needs to be updated for other reasons as well. We were working on this together, and people sort of stopped replying to this thread.

What is "qwik-flow"? Is this something that was created by @keuller or qwik team?

The biggest obvious problem with this is how it is implemented with the () => {} functions. There would need to be some error messages when people were using it incorrectly.

@keuller - Are you still interested in this? I would like to work together if so.

J

shairez commented 7 months ago

Thanks @jdgamble555 !

Yeah it is a qwik community project at this point, but there wasn't a lot of interest to complete it back then and I think it died down.

But if you guys are interested in working on it, I can add you to the repo