QwikDev / qwik

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

[✨] Reducing boilerplate #5557

Open Nefcanto opened 9 months ago

Nefcanto commented 9 months ago

Is your feature request related to a problem?

After working with Qwik for almost a year and migrating an entire web design company from Next.js to Qwik, we are happy that we made this choice. Qwik is fast, and we add pages without worrying about the performance trade-offs and penalties.

Now, we want to give you some feedback on ways that you can improve your code.

We are very strict about boilerplate code. We do our best and go beyond our ways to reduce boilerplate as much as we can. These are some examples in our internal static-code analyzer:

  1. If there is an arrow function with only one parameter, we ask parentheses to be removed
  2. We don't use parentheses and empty fragments to enclose a JSX, we use one of them but not both of them together
  3. If there are multiple imports from the same from clause, we emphasize merging them together
  4. We use barrelling extensible to reduce the paths inside the from clause
  5. ...

The point is we eliminate boilerplate as much as we can.

However, Qwik is filled almost with boilerplate. These are examples that we see:

  1. The long name of the package. We need to write @builder.io/ to access imports constantly. That's not the case with react or next.
  2. The dollar sign is almost useless for us after a year of working with Qwik and after using it in more than 50 projects.
  3. The need to enclose a component with component$ is useless for us. We wish we could eliminate it.
  4. The head element, which should be a named export, is a pain now. We have hundreds of pages that need to repeat five lines of code.
  5. We found ourselves using strategy: 'document-ready' on all of our useVisibleTask$ usages.

Describe the solution you'd like

1- We wish we could write qwik and qwic-city in our imports. For each import we add @builder.io/, which is 13 characters. Multiply it by two per component. Then, multiply it by thousands of components across many projects.

2- Why onClick$ and why not onClick. I read $ and may we ask for a flag to turn all of my JS code into $ codes automatically? We accept that. We say it after thousands of components in many different real-world projects. We prefer Qwik to consider our code to be fully marked with $. But we're tired of writing it constantly. It meaks code less readable.

3- We wish we could have the option to tell Qwik that all of our components are the same, thus not asking us to enclose them all with component$. We want to write all of the components in Qwik as simply as we can in Next.js.

Instead of:

import { component$ } from '@builder.io/qwik`

const Hero = component$(() => {})

We wish we could write it as:

const Hero = () => {}

And still, be able to use everything inside it. That means reducing 58 boilerplate characters per component. Multiply it by thousands of them, and you'll get a huge number.

4- This is what we have to copy/paste between all of our pages (hundreds of them):

const head = ({ resolveValue }) => {
    return useSeo(getData, resolveValue)
}

export { head }

Not even one character changes. Why should we undergo this boilerplate?

5- Maybe we have used useVisibleTask$ more than a hundred times in our code, and all of them have this at the end:

, {
        strategy: 'document-ready'
    }

Can we get an option to make it the default strategy?

Describe alternatives you've considered

There is no alternative that we can think of. If we remove component$ code won't compile. If we remove head from our index pages, we lose our SEO capabilities. If we remove $ code won't work. If we don't put strategy: 'document-ready' code won't work.

Additional context

Please note that we are thankful for your fantastic library. These are just some feedbacks from real-world projects from a team that is sensitive to boilerplate.

Thank you so much.

wmertens commented 9 months ago

@Nefcanto

Hmm, a lot of what you are saying is automated for me. Prettier takes care of most of what you mentioned as boilerplate, and I hardly ever write $() because the optimizer takes care of it.

I never type the builder.io import, my editor completion does that for me.


Wrapping with component$ is non-negociable because the code needs to be wrapped by a virtual element, and also it makes Typescript work better. Probably the name could be shortened by re-exporting, but I just tried that and it didn't quite work:

export * from '@builder.io/qwik';
export { component$ as c$, componentQrl as cQrl} from '@builder.io/qwik';

It is happy with c$ but it tries to import Qrl as well which doesn't exist. So that's maybe a nice feature to add, that it's possible to re-export qwik.


What do you use $() for? Most of the time the optimizer should handle it automatically, no? Can you give some examples?


Now to answer your second part:

  1. Even if you have a thousand of files all importing qwik and qwik-city, we're talking about 26kB of extra code, not that much and certainly a tiny fraction of the actual useful code. Git will compress that to less than 13kB.
  2. The $ suffix makes the attribute into a QRL. So anything becomes a Promise for the thing. You can't just do that for everything. It's also a nice way to mark a prop as special. Furthermore, onclick still exists and can actually be used to handle events "old-school" when you put js into the string.
  3. There's just no way around the components needing to be QRLs. You could write a Vite plugin that transforms specially named files so it adds component$.
  4. did you try something like:

    export const head = makeHead(...)

    and then just import makeHead from your utils? And if it's always the same you can even do export {head} from '~/utils'

  5. What do you need useVisibleTask$ for? Can you give some examples? Especially since you don't use the on-visible, it means that you need to do things that I suspect could be done in a more Qwik way. Anyway, you can make const useEager$ = fn => useVisibleTask$(fn, {strategy: ...}) and also define the useEagerQrl the optimizer will use.
Nefcanto commented 9 months ago

@wmertens, thank you for responding. There are two dimensions related to boilerplate. The first one is writing it. And you're right. That part would be covered by using a suitable tool.

However, there is a second dimension here. Code maintainability and upgradeability.

While JSX would stay around for decades to come, Qwik might not.

One crucial factor that made us agile enough to migrate from Next.js to Qwik was this strategy. We write our codes as neutrally as possible.

Let's consider these two code examples:

const Customers = ({ customers }) => {
    return <ul>
        {
            customers.map(customer => <li key={customer.id}>{customer.name}</li>)
        }
   </ul>
}

And

import { component$ } from '@builder.io/qwik'

const Customers = component$({ customers }) => {
    return <ul>
        {
            customers.map(customer => <li key={customer.id}>{customer.name}</li>)
        }
   </ul>
})

The first one is more maintainable, more neutral, and less tech-dependent. That's a very important aspect of a codebase.

intellix commented 9 months ago

could you use tsconfig paths to shorten the qwik import and create an alias? I don't know if this works but if it does, then it solves your problem:

"paths": {
  "qwik": "@builder.io/qwik"
}

Regarding the boilerplating, could you use nrwl/nx to generate code? I know it's still boilerplate but it would allow you to generate most of it in one go without having to type anything.

maiieul commented 5 months ago

@Nefcanto

const Customers = ({ customers }) => {
    return <ul>
        {
            customers.map(customer => <li key={customer.id}>{customer.name}</li>)
        }
   </ul>
}

is an inline component (https://qwik.dev/docs/components/overview/#inline-components). How would you make the distinction between the two?

Nefcanto commented 5 months ago

@maiieul, make the distinction between what two?

maiieul commented 5 months ago

@Nefcanto between "inline components" (as in comment above) and "component$" components, which are significantly different. Inline components don't benefit from the lazy-execution of component$ components.