diff --git a/.eslintignore b/.eslintignore index dced7b27f9c..e376423dea9 100644 --- a/.eslintignore +++ b/.eslintignore @@ -17,4 +17,5 @@ tsdoc-metadata.json scripts/**/* *.tsbuildinfo packages/docs/src/pages/examples/**/* +packages/docs/src/pages/tutorial/**/* vite.config.ts diff --git a/cspell.json b/cspell.json index 2c3933cf3fd..840b5bd78ab 100644 --- a/cspell.json +++ b/cspell.json @@ -34,5 +34,5 @@ "Pinterest", "Serializability" ], - "enableFiletypes": ["!mdx"] + "enableFiletypes": ["mdx"] } diff --git a/packages/docs/src/pages/docs/INDEX b/packages/docs/src/pages/docs/INDEX index 0ec811689b3..95924c9acdb 100644 --- a/packages/docs/src/pages/docs/INDEX +++ b/packages/docs/src/pages/docs/INDEX @@ -19,6 +19,11 @@ - [Content projection](components/projection.mdx) - [Rendering](components/rendering.mdx) +## Cheat Sheet + +- [Components](cheat/components.mdx) +- [Serialization](cheat/serialization.mdx) + ## Concepts - [Resumable](concepts/resumable.mdx) diff --git a/packages/docs/src/pages/docs/advanced/containers.mdx b/packages/docs/src/pages/docs/advanced/containers.mdx index f63140bfef5..62327d36ec3 100644 --- a/packages/docs/src/pages/docs/advanced/containers.mdx +++ b/packages/docs/src/pages/docs/advanced/containers.mdx @@ -40,7 +40,7 @@ A typical site is composed of two logical parts: 1. The navigation that tends to stay constant across many pages, and 2. The outlet, which is the part of the page that changes based on which route the user navigated to. -We can model the two parts as two navigation and outlet containers. When the user first navigates to a route, the server responds with HTML, which contains containers for both the navigation and the outlet. Once the user navigates to the second route, there are three ways to solve the navigation: +We can model the two parts as two navigation and outlet containers. When the user first navigates to a route, the server responds with HTML, that contains containers for both the navigation and the outlet. Once the user navigates to the second route, there are three ways to solve the navigation: 1. The simplistic approach is to make a full round trip and download an entirely new page. The main downside is that the application loses all of its states on the client. 1. The classical approach is to treat any further navigation in JavaScript. We replace the current outlet component with the new outlet component and let the new component render. The disadvantage is that we need to download and execute the JavaScript. diff --git a/packages/docs/src/pages/docs/advanced/optimizer.mdx b/packages/docs/src/pages/docs/advanced/optimizer.mdx index 9ce839287b0..88d67f3e33d 100644 --- a/packages/docs/src/pages/docs/advanced/optimizer.mdx +++ b/packages/docs/src/pages/docs/advanced/optimizer.mdx @@ -31,22 +31,15 @@ const Counter = component(qrl('./chunk-a.js', 'Counter_onMount')); ```tsx export const Counter_onMount = () => { const store = useStore({ count: 0 }); - return qrl('./chunk-b.js', 'Counter_onRender', [store]); -}; -``` - -`chunk-b.js`: - -```tsx -const Counter_onRender = () => { - const [store] = useLexicalScope(); return ( - + ); }; ``` -`chunk-c.js`: +`chunk-b.js`: ```tsx const Counter_onClick = () => { @@ -55,6 +48,8 @@ const Counter_onClick = () => { }; ``` +Notice that every occurence of `$` results in a new lazy loadable symbol. + # `$` and Optimizer Rules diff --git a/packages/docs/src/pages/docs/cheat/components.mdx b/packages/docs/src/pages/docs/cheat/components.mdx new file mode 100644 index 00000000000..204a602e151 --- /dev/null +++ b/packages/docs/src/pages/docs/cheat/components.mdx @@ -0,0 +1,98 @@ +--- +title: Components +--- + +# Common Component Patterns Cheat Sheet + +## Declartion + +```tsx +import {component$, useStore} from "@builder.io/qwik"; + +export const Greeter = component$(() => { + return Hello World!; +}); +``` + +## Props + +```tsx +import {component$, useStore} from "@builder.io/qwik"; + +interface GreeterProps { + salutation?: string; + name?: string; +} +export const Greeter = component$((props: GreeterProps) => { + const salutation = props.salutation || 'Hello'; + const name = props.name || 'World'; + return {salutation} {name}!; +}); +``` + +### Event Props + +Component props must be serializable, and therefore can not directly reffer to functions. + +```tsx +import {component$, useStore, Qrl} from "@builder.io/qwik"; + +export const Parent = component$(() => { + return ( + console.log('Hello')}> + click + + ); +}); + +interface MyButtonProps { + doSomethingQrl: QRL<() => void> +} +export const MyButton = component$((props: MyButtonProps) => { + return ; +}); +``` + +## Events + +## Watching for Changes + +## Server +### Fetching Data + +```tsx +import {component$, useStore, useServerMount$} from "@builder.io/qwik"; + +export const Greeter = component$(() => { + const store = useStore<{list: null|string[]}>({list: null}); + useServerMount$(async () => { + store.list = await doSomethingToFetchDataOnServer(); + }); + + return ( + + ); +}); +``` + +## Client +### Eagerly Executing Code + +```tsx +import {component$, useStore, useClientEffet} from "@builder.io/qwik"; + +export const Greeter = component$(() => { + const store = useStore<{list: null|string[]}>({list: null}); + useClientEffet$(async () => { + store.list = await doSomethingToFetchDataOnServer(); + }); + + return ( + + ); +}); +``` diff --git a/packages/docs/src/pages/docs/cheat/serialization.mdx b/packages/docs/src/pages/docs/cheat/serialization.mdx new file mode 100644 index 00000000000..8bbc2528614 --- /dev/null +++ b/packages/docs/src/pages/docs/cheat/serialization.mdx @@ -0,0 +1,53 @@ +--- +title: Serialization & Serialization Boundaries +--- + +# `$` Boundaries + +## Rules + +- Only serializable data can cross a `$` boundery. + +## Serialization Boundery + +A serialization boundery occures whenever you cross a lexical scope of a function that is converted into lazy loadable form. It is always denoted by `$(...)` (or `____$(...)`) See example: + +```tsx +import {component$} from "@builder.io/qwik"; + +export const topLevel = Promise.resolve('nonserializable data'); + +export const Greeter = component$(() => { + // BEGIN component serialization boundery + + // Referring to top level symbols that are exported is always allowed. + console.log(topLevel); // OK + + const captureSerializable = 'serializable data'; + const captureNonSerializable = Promise.resolve('nonserializable data'); + return ( + + ); + // BEGIN component serialization boundery +}); + +``` \ No newline at end of file diff --git a/packages/docs/src/pages/docs/components/anatomy.mdx b/packages/docs/src/pages/docs/components/anatomy.mdx index eb41a2a972b..07a32180b87 100644 --- a/packages/docs/src/pages/docs/components/anatomy.mdx +++ b/packages/docs/src/pages/docs/components/anatomy.mdx @@ -265,8 +265,8 @@ const Parent = component$(() => { In the above example the Optimizer transforms the above to: ```tsx -const Child = component$(qrl('./chunk-a', 'Child_onMount')); -const Parent = component$(qrl('./chunk-b', 'Parent_onMount')); +const Child = componentQrl(qrl('./chunk-a', 'Child_onMount')); +const Parent = componentQrl(qrl('./chunk-b', 'Parent_onMount')); const Parent_onMount = () => qrl('./chunk-c', 'Parent_onRender'); const Parent_onRender = () => (
diff --git a/packages/docs/src/pages/docs/components/events.mdx b/packages/docs/src/pages/docs/components/events.mdx index d8b9edff26e..2db7de65ec7 100644 --- a/packages/docs/src/pages/docs/components/events.mdx +++ b/packages/docs/src/pages/docs/components/events.mdx @@ -67,7 +67,7 @@ The main point here is that while the syntax of the events is consistent between ## Prevent default -Because of the async nature of Qwik, event's handler execution might be delayed because the implementation is not downloaded yet. This introduces a problem when the event's handler needs to prevent the default behavior of the event. Tradicional `event.preventDefault()` will not work, instead use the qwik's `preventdefault:{eventName}` attribute: +Because of the async nature of Qwik, event's handler execution might be delayed because the implementation is not downloaded yet. This introduces a problem when the event's handler needs to prevent the default behavior of the event. Traditional `event.preventDefault()` will not work, instead use the qwik's `preventdefault:{eventName}` attribute: ```tsx const Counter = component$(() => { @@ -269,7 +269,7 @@ Notice that `on:click` attribute contains three pieces of information: 2. `Counter_button_onClick`: The symbol which needs to be retrieved from the lazy-loaded chunk. 3. `[0]`: An array of lexically captured variable references (State of the closure). -In our case `() => store.count++` only captures `store`, and hence it contains only a single reference `0`. `0` is an index into the `q:obj` attribute which contains a reference to the actual serialized object referring to `store`. (The exact mechanisms and syntax is an implementation detail that can change at any time.) +In our case `() => store.count++` only captures `store`, and hence it contains only a single reference `0`. `0` is an index into the `q:obj` attribute that contains a reference to the actual serialized object referring to `store`. (The exact mechanisms and syntax is an implementation detail that can change at any time.) ## Comparison to `import()` @@ -277,7 +277,7 @@ JavaScript supports dynamic `import()`. At first glance, it may seem that the sa Dynamic `import()`: -- Is relative to the file which contains it. This works great for `file-a.js` trying to load `file-b.js` as `import('./file-b.js')`. However, when the `./file-a.js` gets serialized into HTML then we lose its relative nature. It is the framework that reads the `./file-b.js` from HTML and performs the `import()`. This means that all imports now become relative to the framework, which is incorrect. +- Is relative to the file that contains it. This works great for `file-a.js` trying to load `file-b.js` as `import('./file-b.js')`. However, when the `./file-a.js` gets serialized into HTML then we lose its relative nature. It is the framework that reads the `./file-b.js` from HTML and performs the `import()`. This means that all imports now become relative to the framework, which is incorrect. - Requires that the developer writes `import('./file-a.js')`, which means the developer is in charge of deciding where the lazy-loaded boundaries are. This limits our ability of the tooling to move code around in an automated way. - Supports import of top-level functions only which don't capture the state. This is the biggest difference. Qwik allows the imported symbol to be a closure that carries all of its state with it. diff --git a/packages/docs/src/pages/docs/getting-started.mdx b/packages/docs/src/pages/docs/getting-started.mdx index dbb1ad3acc2..04df0320d4e 100644 --- a/packages/docs/src/pages/docs/getting-started.mdx +++ b/packages/docs/src/pages/docs/getting-started.mdx @@ -50,7 +50,7 @@ After your new app is created, you will see an output like the following in your npm start ``` -At this point, you will have `qwik-todo` directory, which contains the starter app. +At this point, you will have `qwik-todo` directory, that contains the starter app. ## Running in development diff --git a/packages/docs/src/pages/docs/overview.mdx b/packages/docs/src/pages/docs/overview.mdx index cb7448c282a..fd9f9155527 100644 --- a/packages/docs/src/pages/docs/overview.mdx +++ b/packages/docs/src/pages/docs/overview.mdx @@ -5,16 +5,16 @@ fetch: https://hackmd.io/@mhevery/Sy52N2Ax9 # Overview -Qwik is a new kind of web framework that can deliver instant loading web applications at any size or complexity. Your sites and apps can boot with less than 1kb of JS (_including_ your code, regardless of complexity), and achieve unheard of performance at scale. +Qwik is a new kind of web framework that can deliver instantly load web applications at any size or complexity. Your sites and apps can boot with about 1kb of JS (regardless of application complexity), and achieve consistent performance at scale. ## Qwik is: - **General-purpose**: Qwik is familiar for React developers and can be used to build any type of web site or application. -- **Resumable**: Qwik is [resumable](./concepts/resumable.mdx) which means Qwik applications require **0 hydration**. This allows Qwik apps to have instant-on interactivity. +- **Resumable**: Qwik is [resumable](./concepts/resumable.mdx) which means Qwik applications require **no hydration**. This allows Qwik apps to have instant-on interactivity. - **Progressive**: Qwik takes [full responsibility of how to load and download JS](https://www.builder.io/blog/dont-blame-the-developer-for-what-the-frameworks-did). No more manual code splitting. - **Reactive**: Qwik semantics allow for [fully reactive and efficient rendering](./concepts/reactivity.mdx). -- **Fast**: Qwik has unprecedented performance, offering sub-second full page loads even on mobile devices. Qwik achieves this by delivering pure HTML, and incrementally loading JS only as-needed. -- **Scalable**: Qwik application have [O(1) constant scalability](https://www.builder.io/blog/our-current-frameworks-are-on-we-need-o1). It does not matter if your application has 1 million components, boot time is unaffected. +- **Fast**: Qwik has consistantly fast performance no matter the application complexity, offering sub-second full page loads even on mobile devices. Qwik achieves this by delivering pure HTML, and incrementally loading JS only as-needed. +- **Scalable**: Qwik application have [O(1) constant scalability](https://www.builder.io/blog/our-current-frameworks-are-on-we-need-o1). It does not matter the size of your application, boot time is constantly fast. Qwik Diagram { + return __put_something_here__; +}); diff --git a/packages/docs/src/pages/tutorial/component/basic/solution/app.tsx b/packages/docs/src/pages/tutorial/component/basic/solution/app.tsx new file mode 100644 index 00000000000..7dca358de75 --- /dev/null +++ b/packages/docs/src/pages/tutorial/component/basic/solution/app.tsx @@ -0,0 +1,5 @@ +import { component$ } from '@builder.io/qwik'; + +export const App = component$(() => { + return Hello World!; +}); diff --git a/packages/docs/src/pages/tutorial/component/binding/index.mdx b/packages/docs/src/pages/tutorial/component/binding/index.mdx new file mode 100644 index 00000000000..9c6a3400fea --- /dev/null +++ b/packages/docs/src/pages/tutorial/component/binding/index.mdx @@ -0,0 +1,9 @@ +--- +title: Binding Expressions +layout: tutorial +--- + +The purpose of components is to merge data with the template (JSX.) This is done with the help of the `{expression}` syntax and can be placed either as a text node or attribute. + +- Bind `data.name` to the `value` attribute of ``. +- Bind `data.description` to the content of ` + + ); +}); + +export const DESCRIPTION = ` +Qwik is designed for the fastest possible page load time, +by delivering pure HTML with near 0 JavaScript for your +pages to become interactive, regardless of how complex +your site or app is. It achieves this via resumability +of code.`; diff --git a/packages/docs/src/pages/tutorial/component/binding/solution/app.tsx b/packages/docs/src/pages/tutorial/component/binding/solution/app.tsx new file mode 100644 index 00000000000..a426889039c --- /dev/null +++ b/packages/docs/src/pages/tutorial/component/binding/solution/app.tsx @@ -0,0 +1,25 @@ +import { component$ } from '@builder.io/qwik'; + +export const App = component$(() => { + const data = { + name: 'Qwik', + description: DESCRIPTION, + }; + + return ( + <> + +
+ + + ); +}); + +export const DESCRIPTION = ` +Qwik is designed for the fastest possible page load time, +by delivering pure HTML with near 0 JavaScript for your +pages to become interactive, regardless of how complex +your site or app is. It achieves this via resumability +of code.`; diff --git a/packages/docs/src/pages/tutorial/component/composition/index.mdx b/packages/docs/src/pages/tutorial/component/composition/index.mdx new file mode 100644 index 00000000000..babaaa4d2a3 --- /dev/null +++ b/packages/docs/src/pages/tutorial/component/composition/index.mdx @@ -0,0 +1,8 @@ +--- +title: Component Composition +layout: tutorial +--- + +Components can be composed together to build applications. + +In this example we have pre-declared an `` and a `` component. Place the `` component inside the `` component so that the user can see its contents. diff --git a/packages/docs/src/pages/tutorial/component/composition/problem/app.tsx b/packages/docs/src/pages/tutorial/component/composition/problem/app.tsx new file mode 100644 index 00000000000..c763fd4884e --- /dev/null +++ b/packages/docs/src/pages/tutorial/component/composition/problem/app.tsx @@ -0,0 +1,14 @@ +import { component$ } from '@builder.io/qwik'; + +export const App = component$(() => { + return ( +
+ Insert Greeter component here. By composing components together large applications can be + written without putting all of the code into a single file/component. +
+ ); +}); + +export const Greeter = component$(() => { + return
Hello World!
; +}); diff --git a/packages/docs/src/pages/tutorial/component/composition/solution/app.tsx b/packages/docs/src/pages/tutorial/component/composition/solution/app.tsx new file mode 100644 index 00000000000..47c6d7e7cb7 --- /dev/null +++ b/packages/docs/src/pages/tutorial/component/composition/solution/app.tsx @@ -0,0 +1,13 @@ +import { component$ } from '@builder.io/qwik'; + +export const App = component$(() => { + return ( +
+ +
+ ); +}); + +export const Greeter = component$(() => { + return
Hello World!
; +}); diff --git a/packages/docs/src/pages/tutorial/component/inline/index.mdx b/packages/docs/src/pages/tutorial/component/inline/index.mdx new file mode 100644 index 00000000000..fd77934ab3b --- /dev/null +++ b/packages/docs/src/pages/tutorial/component/inline/index.mdx @@ -0,0 +1,10 @@ +--- +title: Light Component +layout: tutorial +--- + +Qwik comports are lazy-loadable. This is achieved through `component$()` method. The presence of `$` crates a lazy-loaded boundary. The `$` will be discussed later. Qwik also supports inline components. Inline components are not by themselves lazy loadable and instead load as part of the parent component where they are used. (Inline components are equivalent to regular components in most other frameworks.) + +In this example we have pre-declared an `` and a `` component. The `` component is currently a Qwik Component because it is declared with `component$()`. Remove the `component$()` to convert `` to inline component. Inline components do not have host elements, (described later.) + +Open the `Symbols` tab and notice that the `` component is no longer an independent export, and instead it is bundled as part of the `` component. diff --git a/packages/docs/src/pages/tutorial/component/inline/problem/app.tsx b/packages/docs/src/pages/tutorial/component/inline/problem/app.tsx new file mode 100644 index 00000000000..47c6d7e7cb7 --- /dev/null +++ b/packages/docs/src/pages/tutorial/component/inline/problem/app.tsx @@ -0,0 +1,13 @@ +import { component$ } from '@builder.io/qwik'; + +export const App = component$(() => { + return ( +
+ +
+ ); +}); + +export const Greeter = component$(() => { + return
Hello World!
; +}); diff --git a/packages/docs/src/pages/tutorial/component/inline/solution/app.tsx b/packages/docs/src/pages/tutorial/component/inline/solution/app.tsx new file mode 100644 index 00000000000..c577ffd6750 --- /dev/null +++ b/packages/docs/src/pages/tutorial/component/inline/solution/app.tsx @@ -0,0 +1,13 @@ +import { component$ } from '@builder.io/qwik'; + +export const App = component$(() => { + return ( +
+ +
+ ); +}); + +export const Greeter = () => { + return
Hello World!
; +}; diff --git a/packages/docs/src/pages/tutorial/composing/dollar/index.mdx b/packages/docs/src/pages/tutorial/composing/dollar/index.mdx new file mode 100644 index 00000000000..502b9cee5dd --- /dev/null +++ b/packages/docs/src/pages/tutorial/composing/dollar/index.mdx @@ -0,0 +1,68 @@ +--- +title: Creating APIs with $ +layout: tutorial +--- + +The powerful, part of Optimizer is that you can create your own APIs with `$` suffix. + +Imagine that we would like to have a delay method that lazy loads its callback. Normally we would have to write something like this: + +```typescript +setTimeout(() => { + // I am eagerly loaded, but it would be better if I was lazy-loaded. + ... +}, timeout); +``` + +The issue with the example above is that the callback has to be downloaded and created eagerly. This may be an issue if the closure is large or if the callback is never executed (or only executed later.) + +A better solution would be to have `delay$` method that can lazy-load the closure associated with the callback. Something like this. + +```typescript +delay$(() => { + // I am lazy-loaded only when I need to be executed. + ... +}, 1000) +``` + +In the above solution, the callback is only downloaded when `delay$` is ready to execute it. + +## Creating your APIs with `$` suffix + +Qwik runtime works with `QRL`s. For this reason we define a method like so: +```typescript +export function delayQrl(fn: QRL<() => T>, delayInMs: number): Promise { + return new Promise((res) => { + setTimeout(() => { + res(fn.invoke()); + }, delayInMs); + }); +} +``` + +This method knows how to take a `QRL` and execute it after a certain delay. The key part here is that the `QRL.invoke()` method is called when the delay is ready and is therefore lazy. + +The next step is to convert the `delayQrl` method to a `delay$` alias. This is done with `implicit$FirstArg` like so: + +```typescript +export const delay$ = implicit$FirstArg(delayQrl); +``` + +Here are the types to make it clearer as to what is going on. + +```typescript +declare delayQrl: (fn: QRL<() => T>, delayInMs: number) => Promise; +declare delay$: (fn: () => T, delayInMs: number) => Promise; +``` + +The above allows us to use `delay$` in an inlined fashion, but the Optimizer converts the `delay$` to `delayQrl` form. + +NOTE the two methods must have the same prefix. So a general form is: +```typescript +export const SOME_NAME_Qrl = ...; +export const SOME_NAME_$ = implicit$FirstArg(SOME_NAME_Qrl); +``` + +## Example + +In our example we are executing `store.count++` and `store.delay++` together. Wrap the `store.delay` in `delay$()` call so that it updates with a one second delay. \ No newline at end of file diff --git a/packages/docs/src/pages/tutorial/composing/dollar/problem/app.tsx b/packages/docs/src/pages/tutorial/composing/dollar/problem/app.tsx new file mode 100644 index 00000000000..d2603129116 --- /dev/null +++ b/packages/docs/src/pages/tutorial/composing/dollar/problem/app.tsx @@ -0,0 +1,30 @@ +import { component$, QRL, implicit$FirstArg, useStore } from '@builder.io/qwik'; + +export function delayQrl(fn: QRL<() => T>, delayInMs: number): Promise { + return new Promise((res) => { + setTimeout(() => { + res(fn.invoke()); + }, delayInMs); + }); +} + +export const delay$ = implicit$FirstArg(delayQrl); + +export const App = component$(() => { + const store = useStore({ count: 0, delay: 0 }); + return ( + <> + Count: {store.count}
+ Delay: {store.delay}
+ + + ); +}); diff --git a/packages/docs/src/pages/tutorial/composing/dollar/solution/app.tsx b/packages/docs/src/pages/tutorial/composing/dollar/solution/app.tsx new file mode 100644 index 00000000000..2fd9ccf8407 --- /dev/null +++ b/packages/docs/src/pages/tutorial/composing/dollar/solution/app.tsx @@ -0,0 +1,29 @@ +import { component$, QRL, implicit$FirstArg, useStore } from '@builder.io/qwik'; + +export function delayQrl(fn: QRL<() => T>, delayInMs: number): Promise { + return new Promise((res) => { + setTimeout(() => { + res(fn.invoke()); + }, delayInMs); + }); +} + +export const delay$ = implicit$FirstArg(delayQrl); + +export const App = component$(() => { + const store = useStore({ count: 0, delay: 0 }); + return ( + <> + Count: {store.count}
+ Delay: {store.delay}
+ + + ); +}); diff --git a/packages/docs/src/pages/tutorial/composing/use/index.mdx b/packages/docs/src/pages/tutorial/composing/use/index.mdx new file mode 100644 index 00000000000..08bbde895e9 --- /dev/null +++ b/packages/docs/src/pages/tutorial/composing/use/index.mdx @@ -0,0 +1,11 @@ +--- +title: Composing use Hooks +layout: tutorial +--- + +Hooks are a way to abstract common logic away from the components that use it. They are a way to share logic between components. While Qwik provides many hooks, there will always be one that is not provided out of the box. This tutorial will show you how to create your own hook. + +In this example, the registering of `mousemove` events is something that could be shared between multiple components. Refactor the code by pulling out the code before JSX into its own `useMousePosition()` function. + +Congratulations, you have successfully created your own hook! You can now use it in any component that needs to listen to the mouse position. + diff --git a/packages/docs/src/pages/tutorial/composing/use/problem/app.tsx b/packages/docs/src/pages/tutorial/composing/use/problem/app.tsx new file mode 100644 index 00000000000..f3c65847ec5 --- /dev/null +++ b/packages/docs/src/pages/tutorial/composing/use/problem/app.tsx @@ -0,0 +1,17 @@ +import { component$, useOnDocument, useStore, $ } from '@builder.io/qwik'; + +export const App = component$(() => { + const mousePosition = useStore({ x: 0, y: 0 }); + useOnDocument( + 'mousemove', + $((event: Event) => { + mousePosition.x = (event as MouseEvent).clientX; + mousePosition.y = (event as MouseEvent).clientY; + }) + ); + return ( +
+ (x: {mousePosition.x}, y: {mousePosition.y}) +
+ ); +}); diff --git a/packages/docs/src/pages/tutorial/composing/use/solution/app.tsx b/packages/docs/src/pages/tutorial/composing/use/solution/app.tsx new file mode 100644 index 00000000000..a772374c3db --- /dev/null +++ b/packages/docs/src/pages/tutorial/composing/use/solution/app.tsx @@ -0,0 +1,22 @@ +import { component$, useOnDocument, useStore, $ } from '@builder.io/qwik'; + +export function useMousePosition() { + const mousePosition = useStore({ x: 0, y: 0 }); + useOnDocument( + 'mousemove', + $((event: Event) => { + mousePosition.x = (event as MouseEvent).clientX; + mousePosition.y = (event as MouseEvent).clientY; + }) + ); + return mousePosition; +} + +export const App = component$(() => { + const mousePosition = useMousePosition(); + return ( +
+ (x: {mousePosition.x}, y: {mousePosition.y}) +
+ ); +}); diff --git a/packages/docs/src/pages/tutorial/context/basic/index.mdx b/packages/docs/src/pages/tutorial/context/basic/index.mdx new file mode 100644 index 00000000000..1a7695d14be --- /dev/null +++ b/packages/docs/src/pages/tutorial/context/basic/index.mdx @@ -0,0 +1,15 @@ +--- +title: Context +layout: tutorial +--- + +Use context to pass data to child components without explicitly passing it through components (known as prop drilling). Context is useful to share data that is needed throughout the application components. For example styling information, application state, or currently logged-in user. + +To use context you need three parts: +- `createContext()`: this creates a serializable ID for the context. Make sure that this id is unique within your application. +- `useContextProvider()`: At a parent component call this method to publish the context value. All children (and grandchildren) that are descendants of this component will be able to retrieve the context. +- `useContext()` to retrieve the context and use it in any component. + +In this example, we would like to pass the `TodosStore` to the `` component. Update the code to use `useContext()` to retrieve the value. + +Contexts typically are stores, and as such, they must be serializable. \ No newline at end of file diff --git a/packages/docs/src/pages/tutorial/context/basic/problem/app.tsx b/packages/docs/src/pages/tutorial/context/basic/problem/app.tsx new file mode 100644 index 00000000000..b3a1593063e --- /dev/null +++ b/packages/docs/src/pages/tutorial/context/basic/problem/app.tsx @@ -0,0 +1,28 @@ +import { component$, createContext, useContextProvider, useStore } from '@builder.io/qwik'; + +interface TodosStore { + items: string[]; +} +export const TodosContext = createContext('Todos'); +export const App = component$(() => { + useContextProvider( + TodosContext, + useStore({ + items: ['Learn QWik', 'Build Qwik app', 'Profit'], + }) + ); + + return ; +}); + +export const Items = component$(() => { + // replace this with context retrieval. + const todos = { items: [] }; + return ( +
    + {todos.items.map((item) => ( +
  • {item}
  • + ))} +
+ ); +}); diff --git a/packages/docs/src/pages/tutorial/context/basic/solution/app.tsx b/packages/docs/src/pages/tutorial/context/basic/solution/app.tsx new file mode 100644 index 00000000000..b6125fba79e --- /dev/null +++ b/packages/docs/src/pages/tutorial/context/basic/solution/app.tsx @@ -0,0 +1,33 @@ +import { + component$, + createContext, + useContextProvider, + useContext, + useStore, +} from '@builder.io/qwik'; + +interface TodosStore { + items: string[]; +} +export const TodosContext = createContext('Todos'); +export const App = component$(() => { + useContextProvider( + TodosContext, + useStore({ + items: ['Learn Qwik', 'Build Qwik app', 'Profit'], + }) + ); + + return ; +}); + +export const Items = component$(() => { + const todos = useContext(TodosContext); + return ( +
    + {todos.items.map((item) => ( +
  • {item}
  • + ))} +
+ ); +}); diff --git a/packages/docs/src/pages/tutorial/events/basic/index.mdx b/packages/docs/src/pages/tutorial/events/basic/index.mdx new file mode 100644 index 00000000000..b899484c08d --- /dev/null +++ b/packages/docs/src/pages/tutorial/events/basic/index.mdx @@ -0,0 +1,45 @@ +--- +title: Event Listeners +layout: tutorial +--- + +So far we have created static applications that have no interactivity. An important reason why we use frameworks to build applications is for easy declaration of behavior. A key part of application behavior is to listen to user events. + +There are many different events browser support that Qwik can listen on. The general syntax is to place an attribute `on$` on an element to signal to the framework that we wish to listen to events here. + +Add a `click` event on the ` + + + +``` + + +1. The APIs contain `$` to ensure that your code contains them. +2. The Optimizer looks for `$` and extracts the function wrapped by `$` into a separate lazy-loadable chunk. (See the documentation to better understand how this is done.) +3. As part of the SSR the server executes the JSX and notices that there is a click listener. The click listener gets serialized into the `; +}); diff --git a/packages/docs/src/pages/tutorial/events/basic/solution/app.tsx b/packages/docs/src/pages/tutorial/events/basic/solution/app.tsx new file mode 100644 index 00000000000..c22978e0593 --- /dev/null +++ b/packages/docs/src/pages/tutorial/events/basic/solution/app.tsx @@ -0,0 +1,5 @@ +import { component$ } from '@builder.io/qwik'; + +export const App = component$(() => { + return ; +}); diff --git a/packages/docs/src/pages/tutorial/events/document/index.mdx b/packages/docs/src/pages/tutorial/events/document/index.mdx new file mode 100644 index 00000000000..66055f133a5 --- /dev/null +++ b/packages/docs/src/pages/tutorial/events/document/index.mdx @@ -0,0 +1,14 @@ +--- +title: Listening to document/window +layout: tutorial +--- + +So far we have shown you how to listen to events emitted from a DOM element such as a ` + {store.show ? : null} + + ); +}); + +export const Greeter = component$(() => { + // Use useCleanup$ here to alert the user when the component is removed. + return Hello World; +}); diff --git a/packages/docs/src/pages/tutorial/hooks/cleanup/solution/app.tsx b/packages/docs/src/pages/tutorial/hooks/cleanup/solution/app.tsx new file mode 100644 index 00000000000..bf82b9eccb1 --- /dev/null +++ b/packages/docs/src/pages/tutorial/hooks/cleanup/solution/app.tsx @@ -0,0 +1,18 @@ +import { component$, useCleanup$, useStore } from '@builder.io/qwik'; + +export const App = component$(() => { + const store = useStore({ show: true }); + return ( +
+ + {store.show ? : null} +
+ ); +}); + +export const Greeter = component$(() => { + useCleanup$(() => { + alert('Greeter component has been removed!'); + }); + return Hello World; +}); diff --git a/packages/docs/src/pages/tutorial/hooks/client-effect/index.mdx b/packages/docs/src/pages/tutorial/hooks/client-effect/index.mdx new file mode 100644 index 00000000000..a95a8533011 --- /dev/null +++ b/packages/docs/src/pages/tutorial/hooks/client-effect/index.mdx @@ -0,0 +1,26 @@ +--- +title: useClientEffect$() +layout: tutorial +--- + +Use `useClientEffect$()` to execute code after the component is resumed. This is useful for things such as setting up timers or streams on the client when the application is resumed. + +## Component Life Cycle and SSR + +Qwik is resumable. Resumability means that the application starts up on the server and then the application is transferred to the client. On the client, the application continues execution from where it left off. A common use case of this is that a component is created on the server, paused, and then resumed on the client. To make the component fully functional it may be necessary to execute code eagerly on the client to set up timers or streams. + +`useClientEffect$()` is a client only method. (There is no equivalent on the server.) NOTE: See `useWatch$()` for behavior that needs to be executed on both client and server. + +## When is `useClientEffect$()` executed? + +Client effect code is executed after the component is resumed. The `useClientEffect$()` method takes an additional argument that controls when the effect is executed. There are two options: +- `visible` (default): Execute the effect when the component becomes visible. This is a preferred option because it delays the execution of the effect until the component is visible rather than eagerly on application startup (We are trying to minimize the amount of code the application runs on startup). +- `load`: Execute the code as soon as possible. This is usually right after `DOMContentLoaded` event. + +## Example + +The example shows a clock component that is rendered below the fold. Use the `useClientEffect$()` to make the clock update the current time every second to make it work on the client. We have provided the utility function `updateClock` to aid your implementation. + +Keep in mind that `useClientEffect$()` should return a cleanup function that releases the `setInterval` timer so that the timer can be properly cleaned up when the component is unmounted/destroyed. + + diff --git a/packages/docs/src/pages/tutorial/hooks/client-effect/problem/app.tsx b/packages/docs/src/pages/tutorial/hooks/client-effect/problem/app.tsx new file mode 100644 index 00000000000..7deee910a03 --- /dev/null +++ b/packages/docs/src/pages/tutorial/hooks/client-effect/problem/app.tsx @@ -0,0 +1,54 @@ +import { component$, useStore, useStyles$, useClientEffect$ } from '@builder.io/qwik'; +import styles from './clock.css'; + +interface ClockStore { + hour: number; + minute: number; + second: number; +} +export const Clock = component$(() => { + useStyles$(styles); + + const store = useStore({ + hour: 0, + minute: 0, + second: 0, + }); + + useClientEffect$(() => { + // Put code here to periodically call updateClock(). + }); + + return ( +
+
+
+
+
+
+
+
+
+ ); +}); + +export function updateClock(store: ClockStore) { + const now = new Date(); + store.second = now.getSeconds() * (360 / 60); + store.minute = now.getMinutes() * (360 / 60); + store.hour = now.getHours() * (360 / 12); +} + +export const App = component$(() => { + return ( +
+

This is an example of Lazy executing code on component when component becomes visible.

+ +

+ ⬇️ Scroll down until the clock is in view. +

+ + +
+ ); +}); diff --git a/packages/docs/src/pages/tutorial/hooks/client-effect/problem/clock.css b/packages/docs/src/pages/tutorial/hooks/client-effect/problem/clock.css new file mode 100644 index 00000000000..d1c742f7e38 --- /dev/null +++ b/packages/docs/src/pages/tutorial/hooks/client-effect/problem/clock.css @@ -0,0 +1,92 @@ +/* Clock inspired by: https://paulund.co.uk/create-a-clock-in-css */ + +.clock { + background: #fff; + border: 10px solid #7a7a7a; + border-radius: 50%; + box-sizing: border-box; + height: 300px; + margin: 0 auto; + position: relative; + width: 300px; +} + +.twelve, +.three, +.six, +.nine { + background: #333; + position: absolute; +} + +.twelve, +.six { + height: 10px; + width: 4px; +} + +.three, +.nine { + height: 4px; + width: 10px; +} + +.twelve { + left: 50%; + top: -1px; +} + +.three { + right: -1px; + top: 50%; +} + +.six { + left: 50%; + bottom: -1px; +} + +.nine { + left: -1px; + top: 50%; +} + +.hour { + height: 60px; + width: 4px; + background: #333; + position: absolute; + left: 50%; + top: 80px; + animation: tick 43200s infinite linear; + -webkit-animation: tick 43200s infinite linear; +} + +.minute { + height: 100px; + width: 4px; + background: #777; + position: absolute; + left: 50%; + top: 40px; + animation: tick 3600s infinite linear; + -webkit-animation: tick 3600s infinite linear; +} + +.second { + height: 120px; + width: 3px; + background: #fc0505; + position: absolute; + left: 50%; + top: 20px; + animation: tick 60s infinite linear; + -webkit-animation: tick 60s infinite linear; +} + +.hour, +.minute, +.second { + transform-origin: 2px 100%; + -webkit-transform-origin: 2px 100%; +} diff --git a/packages/docs/src/pages/tutorial/hooks/client-effect/solution/app.tsx b/packages/docs/src/pages/tutorial/hooks/client-effect/solution/app.tsx new file mode 100644 index 00000000000..0c266f81ab3 --- /dev/null +++ b/packages/docs/src/pages/tutorial/hooks/client-effect/solution/app.tsx @@ -0,0 +1,56 @@ +import { component$, useStore, useStyles$, useClientEffect$ } from '@builder.io/qwik'; +import styles from './clock.css'; + +interface ClockStore { + hour: number; + minute: number; + second: number; +} +export const Clock = component$(() => { + useStyles$(styles); + + const store = useStore({ + hour: 0, + minute: 0, + second: 0, + }); + + useClientEffect$(() => { + updateClock(store); + const tmrId = setInterval(() => updateClock(store), 1000); + return () => clearInterval(tmrId); + }); + + return ( +
+
+
+
+
+
+
+
+
+ ); +}); + +export function updateClock(store: ClockStore) { + const now = new Date(); + store.second = now.getSeconds() * (360 / 60); + store.minute = now.getMinutes() * (360 / 60); + store.hour = now.getHours() * (360 / 12); +} + +export const App = component$(() => { + return ( +
+

This is an example of Lazy executing code on component when component becomes visible.

+ +

+ ⬇️ Scroll down until the clock is in view. +

+ + +
+ ); +}); diff --git a/packages/docs/src/pages/tutorial/hooks/client-effect/solution/clock.css b/packages/docs/src/pages/tutorial/hooks/client-effect/solution/clock.css new file mode 100644 index 00000000000..d1c742f7e38 --- /dev/null +++ b/packages/docs/src/pages/tutorial/hooks/client-effect/solution/clock.css @@ -0,0 +1,92 @@ +/* Clock inspired by: https://paulund.co.uk/create-a-clock-in-css */ + +.clock { + background: #fff; + border: 10px solid #7a7a7a; + border-radius: 50%; + box-sizing: border-box; + height: 300px; + margin: 0 auto; + position: relative; + width: 300px; +} + +.twelve, +.three, +.six, +.nine { + background: #333; + position: absolute; +} + +.twelve, +.six { + height: 10px; + width: 4px; +} + +.three, +.nine { + height: 4px; + width: 10px; +} + +.twelve { + left: 50%; + top: -1px; +} + +.three { + right: -1px; + top: 50%; +} + +.six { + left: 50%; + bottom: -1px; +} + +.nine { + left: -1px; + top: 50%; +} + +.hour { + height: 60px; + width: 4px; + background: #333; + position: absolute; + left: 50%; + top: 80px; + animation: tick 43200s infinite linear; + -webkit-animation: tick 43200s infinite linear; +} + +.minute { + height: 100px; + width: 4px; + background: #777; + position: absolute; + left: 50%; + top: 40px; + animation: tick 3600s infinite linear; + -webkit-animation: tick 3600s infinite linear; +} + +.second { + height: 120px; + width: 3px; + background: #fc0505; + position: absolute; + left: 50%; + top: 20px; + animation: tick 60s infinite linear; + -webkit-animation: tick 60s infinite linear; +} + +.hour, +.minute, +.second { + transform-origin: 2px 100%; + -webkit-transform-origin: 2px 100%; +} diff --git a/packages/docs/src/pages/tutorial/hooks/mount/index.mdx b/packages/docs/src/pages/tutorial/hooks/mount/index.mdx new file mode 100644 index 00000000000..0c14074ffb3 --- /dev/null +++ b/packages/docs/src/pages/tutorial/hooks/mount/index.mdx @@ -0,0 +1,30 @@ +--- +title: useMount$() Hook +layout: tutorial +--- + +Use `useMount$()` to execute code when the component is mounted into the rendering tree. (Another way to think about it is that `useMount$()` executes on component creation.) + +## Component Life Cycle and SSR + +Qwik is resumable. Resumability means that the application starts up on the server and then the application is transferred to the client. On the client, the application continues execution from where it left off. The implication of this is that a component may be created on the server and destroyed on the client. This means that the component's `useMount$()` method may execute on the server and but its `useCleanup$()` method may execute on the client. + +## Usage of `useMount$()` + +`useMount$()` is a hook that executes a callback when the component is mounted into the rendering tree. The `useMount$()` function can be async. `useMount$()` delays the rendering of the component until the `useMount$()` callback is finished executing. Typical usage for `useMount$()` is to fetch data needed for initial rendering. + +## `useMethod$()` variations + +The `useMount$()` hook is subdivided into server and client versions: +- `useServerMount$()`: Hook that executes on the component mount when in a server environment. This is useful because server often has different APIs for retrieving data. +- `useClientMount$()`: Hook that executes on the component mount when in a client environment. This is useful because server often has different APIs for retrieving data. + +Use `useMount$()` if the code that needs to be executed is identical. + +## Server only imports + +Because `useServerMount$()` hooks have `$` in their name, they are subject to lazy loading. Lazy-loading means that the function is moved into a new file by the Optimizer. When the function is moved the Optimizer also moves any imports with it. This means that it is safe to have server-only imports as part of the `useServerMount$()` hook as they will be removed by the Optimizer. + +## Example + +Use `useServerMount()` to fetch data needed for the rendering. \ No newline at end of file diff --git a/packages/docs/src/pages/tutorial/hooks/mount/problem/app.tsx b/packages/docs/src/pages/tutorial/hooks/mount/problem/app.tsx new file mode 100644 index 00000000000..e3dafad2f4b --- /dev/null +++ b/packages/docs/src/pages/tutorial/hooks/mount/problem/app.tsx @@ -0,0 +1,39 @@ +import { component$, useServerMount$, useStore } from '@builder.io/qwik'; + +export const App = component$(() => { + const github = useStore({ + org: 'BuilderIO', + repos: null as string[] | null, + }); + + useServerMount$(async () => { + // Put code here to fetch data from the server. + }); + + return ( +
+ GitHub username: {github.org} +
+ {github.repos ? ( +
    + {github.repos.map((repo) => ( +
  • + {repo} +
  • + ))} +
+ ) : ( + 'loading...' + )} +
+
+ ); +}); + +export async function getRepositories(username: string, controller?: AbortController) { + const resp = await fetch(`https://api.github.com/users/${username}/repos`, { + signal: controller?.signal, + }); + const json = await resp.json(); + return Array.isArray(json) ? json.map((repo: { name: string }) => repo.name) : null; +} diff --git a/packages/docs/src/pages/tutorial/hooks/mount/solution/app.tsx b/packages/docs/src/pages/tutorial/hooks/mount/solution/app.tsx new file mode 100644 index 00000000000..2d78f3c1787 --- /dev/null +++ b/packages/docs/src/pages/tutorial/hooks/mount/solution/app.tsx @@ -0,0 +1,39 @@ +import { component$, useServerMount$, useStore } from '@builder.io/qwik'; + +export const App = component$(() => { + const github = useStore({ + org: 'BuilderIO', + repos: null as string[] | null, + }); + + useServerMount$(async () => { + github.repos = await getRepositories(github.org); + }); + + return ( +
+ GitHub username: {github.org} +
+ {github.repos ? ( +
    + {github.repos.map((repo) => ( +
  • + {repo} +
  • + ))} +
+ ) : ( + 'loading...' + )} +
+
+ ); +}); + +export async function getRepositories(username: string, controller?: AbortController) { + const resp = await fetch(`https://api.github.com/users/${username}/repos`, { + signal: controller?.signal, + }); + const json = await resp.json(); + return Array.isArray(json) ? json.map((repo: { name: string }) => repo.name) : null; +} diff --git a/packages/docs/src/pages/tutorial/host/basic/index.mdx b/packages/docs/src/pages/tutorial/host/basic/index.mdx new file mode 100644 index 00000000000..fc09b1b2b34 --- /dev/null +++ b/packages/docs/src/pages/tutorial/host/basic/index.mdx @@ -0,0 +1,25 @@ +--- +title: Host Element +layout: tutorial +--- + +The host element is used to mark the component boundaries. Without the host element, Qwik would not know where components start and end. This information is needed so that components can render independently and out of order, a key feature of Qwik. + +An alternative to the host element is to recover the component boundaries by re-executing the application as part of hydration. Qwik's explicit goal is to not perform hydration on application startup as it would force eager application download and execution and negatively impact startup performance. + +## Controlling the Host Element Type with `tagName` + +Every component declared with `component$()` has a host element. By default the host element is `
` marked with the `q:host` attribute. At times it may be necessary to change the `
` to another element, in which case the `tagName` option can be used with `component$()` as shown in this example. + +## `` Element + +A component may need to add additional attributes or listeners to the host element. This can be done through the `` element. Any attribute that can be placed on the DOM element can be placed on the ``. + + +## Example + +In this example, we have created `` component. This component needs to be a ` + + ); +}); + +export const Panel = component$(() => { + console.log('Render: '); + return ( +
+ Currently the <Panel> component controls the content here. Replace this text + with <Slot> element to see the content projected from the <App>. +
+ ); +}); diff --git a/packages/docs/src/pages/tutorial/projection/basic/solution/app.tsx b/packages/docs/src/pages/tutorial/projection/basic/solution/app.tsx new file mode 100644 index 00000000000..7d29bfe5470 --- /dev/null +++ b/packages/docs/src/pages/tutorial/projection/basic/solution/app.tsx @@ -0,0 +1,21 @@ +/* eslint-disable no-console */ +import { component$, Slot, useStore } from '@builder.io/qwik'; + +export const App = component$(() => { + const store = useStore({ count: 0 }); + console.log('Render: '); + return ( + + Count: {store.count}. + + ); +}); + +export const Panel = component$(() => { + console.log('Render: '); + return ( +
+ +
+ ); +}); diff --git a/packages/docs/src/pages/tutorial/projection/fallback/index.mdx b/packages/docs/src/pages/tutorial/projection/fallback/index.mdx new file mode 100644 index 00000000000..d065204b2b1 --- /dev/null +++ b/packages/docs/src/pages/tutorial/projection/fallback/index.mdx @@ -0,0 +1,10 @@ +--- +title: Fallback Content +layout: tutorial +--- + +Fallback content allows the child component to display fallback content in case the parent component did not provide content. Fallback content is declared inside the `` element. + +## Example + +In this example, we show three cards, but some of them are not filled with content. Use the `` element to specify the fallback content. \ No newline at end of file diff --git a/packages/docs/src/pages/tutorial/projection/fallback/problem/app.tsx b/packages/docs/src/pages/tutorial/projection/fallback/problem/app.tsx new file mode 100644 index 00000000000..5a7857abae9 --- /dev/null +++ b/packages/docs/src/pages/tutorial/projection/fallback/problem/app.tsx @@ -0,0 +1,61 @@ +import { component$, Host, Slot, useStyles$ } from '@builder.io/qwik'; + +export const Card = component$(() => { + useStyles$(CSS); + return ( + +
+ +
+
+ +
+
+ ); +}); + +export const App = component$(() => { + return ( + <> + + Qwik + Qwik is a resumable framework for building instant web apps. + + + Partytown + + + + Builder.io allows you to visually build on your tech stack Empower your entire team to + visually create and optimize high-speed experiences on your sites and apps. Provide + whole-team autonomy with a platform that is developer approved. + + + + ); +}); + +export const CSS = ` +.card { + border-radius: 5px; + vertical-align: top; + display: inline-block; + border: 1px solid grey; + width: 200px; + margin: .5em; +} + +.title { + background-color: lightgray; + padding: 0.5em; + border-bottom: 1px solid black; +} + +q\\:fallback { + color: gray; +} + +.body { + padding: 0.5em; +} +`; diff --git a/packages/docs/src/pages/tutorial/projection/fallback/solution/app.tsx b/packages/docs/src/pages/tutorial/projection/fallback/solution/app.tsx new file mode 100644 index 00000000000..39663cc8ae3 --- /dev/null +++ b/packages/docs/src/pages/tutorial/projection/fallback/solution/app.tsx @@ -0,0 +1,61 @@ +import { component$, Host, Slot, useStyles$ } from '@builder.io/qwik'; + +export const Card = component$(() => { + useStyles$(CSS); + return ( + +
+ (no title) +
+
+ (no content) +
+
+ ); +}); + +export const App = component$(() => { + return ( + <> + + Qwik + Qwik is a resumable framework for building instant web apps. + + + Partytown + + + + Builder.io allows you to visually build on your tech stack Empower your entire team to + visually create and optimize high-speed experiences on your sites and apps. Provide + whole-team autonomy with a platform that is developer approved. + + + + ); +}); + +export const CSS = ` +.card { + border-radius: 5px; + vertical-align: top; + display: inline-block; + border: 1px solid grey; + width: 200px; + margin: .5em; +} + +.title { + background-color: lightgray; + padding: 0.5em; + border-bottom: 1px solid black; +} + +q\\:fallback { + color: gray; +} + +.body { + padding: 0.5em; +} +`; diff --git a/packages/docs/src/pages/tutorial/projection/slots/index.mdx b/packages/docs/src/pages/tutorial/projection/slots/index.mdx new file mode 100644 index 00000000000..3960406a468 --- /dev/null +++ b/packages/docs/src/pages/tutorial/projection/slots/index.mdx @@ -0,0 +1,14 @@ +--- +title: Named Slots +layout: tutorial +--- + +In simple cases, projection allows content from the parent component to be projected into the child component. In more complex cases there may be more than one content slot that needs to be projected. Having multiple content slots is achieved by naming them. + +## Example + +In this example, we have created `` component that toggles between open and closed states. Currently, when the `` is closed it does not show any content. Add additional `` to project the `q:slot="closed"` content. + +## Unprojected Slots + +We have added console statements to show when individual components re-render. Notice that the `` component never re-renders on the client. Also notice that the `` projects only one content at a time. This means that when `` renders on the server it has to produce both default as well as `closed` content that Qwik must serialize. The benefit is that when `` toggles between open and closed states, it does not need to re-render the `` component to recover the content that was projected into it. \ No newline at end of file diff --git a/packages/docs/src/pages/tutorial/projection/slots/problem/app.tsx b/packages/docs/src/pages/tutorial/projection/slots/problem/app.tsx new file mode 100644 index 00000000000..834e8252b2d --- /dev/null +++ b/packages/docs/src/pages/tutorial/projection/slots/problem/app.tsx @@ -0,0 +1,33 @@ +import { component$, Slot, useStore } from '@builder.io/qwik'; + +export const App = component$(() => { + console.log('Render: '); + return ( + + (collapsed summary) + Content that should be displayed when the collapse component is open. + + ); +}); + +export const Collapsable = component$(() => { + console.log('Render: '); + const store = useStore({ open: true }); + return ( +
(store.open = !store.open)}> + {store.open ? ( +
+ ▼ +
+ +
+
+ ) : ( +
+ ▶︎ + {/* Project content name "closed" here */} +
+ )} +
+ ); +}); diff --git a/packages/docs/src/pages/tutorial/projection/slots/solution/app.tsx b/packages/docs/src/pages/tutorial/projection/slots/solution/app.tsx new file mode 100644 index 00000000000..db6de966a75 --- /dev/null +++ b/packages/docs/src/pages/tutorial/projection/slots/solution/app.tsx @@ -0,0 +1,33 @@ +import { component$, Slot, useStore } from '@builder.io/qwik'; + +export const App = component$(() => { + console.log('Render: '); + return ( + + (collapsed summary) + Content that should be displayed when the collapse component is open. + + ); +}); + +export const Collapsable = component$(() => { + console.log('Render: '); + const store = useStore({ open: true }); + return ( +
(store.open = !store.open)}> + {store.open ? ( +
+ ▼ +
+ +
+
+ ) : ( +
+ ▶︎ + +
+ )} +
+ ); +}); diff --git a/packages/docs/src/pages/tutorial/props/basic/index.mdx b/packages/docs/src/pages/tutorial/props/basic/index.mdx new file mode 100644 index 00000000000..32e245b248c --- /dev/null +++ b/packages/docs/src/pages/tutorial/props/basic/index.mdx @@ -0,0 +1,8 @@ +--- +title: Component Props +layout: tutorial +--- + +Web applications are built up from components in the same way that general applications are built up from functions. Composing functions would not be very useful if functions would not allow us to pass parameters to them. In the same way that functions have parameters components have props. We use props to pass data from parent to child components. + +Modify the code so that the parent `` can pass `salutation="Hello"` and `name="World"` to the ``. Add properties to the `GreeterProps` interface and then add the necessary bindings. \ No newline at end of file diff --git a/packages/docs/src/pages/tutorial/props/basic/problem/app.tsx b/packages/docs/src/pages/tutorial/props/basic/problem/app.tsx new file mode 100644 index 00000000000..0e3223d836c --- /dev/null +++ b/packages/docs/src/pages/tutorial/props/basic/problem/app.tsx @@ -0,0 +1,14 @@ +import { component$ } from '@builder.io/qwik'; + +export const App = component$(() => { + return ( +
+ +
+ ); +}); + +interface GreeterProps {} +export const Greeter = component$((props: GreeterProps) => { + return
Bind props here
; +}); diff --git a/packages/docs/src/pages/tutorial/props/basic/solution/app.tsx b/packages/docs/src/pages/tutorial/props/basic/solution/app.tsx new file mode 100644 index 00000000000..b83a5896fd8 --- /dev/null +++ b/packages/docs/src/pages/tutorial/props/basic/solution/app.tsx @@ -0,0 +1,21 @@ +import { component$ } from '@builder.io/qwik'; + +export const App = component$(() => { + return ( +
+ +
+ ); +}); + +interface GreeterProps { + salutation: string; + name: string; +} +export const Greeter = component$((props: GreeterProps) => { + return ( +
+ {props.salutation} {props.name}! +
+ ); +}); diff --git a/packages/docs/src/pages/tutorial/props/closures/index.mdx b/packages/docs/src/pages/tutorial/props/closures/index.mdx new file mode 100644 index 00000000000..fba4f185f33 --- /dev/null +++ b/packages/docs/src/pages/tutorial/props/closures/index.mdx @@ -0,0 +1,51 @@ +--- +title: Passing Closures +layout: tutorial +--- + +Props must be serializable so that Qwik can resume and render each component independently from other components on the page. This possess a problem if we wish to pass a callback to a child component. Callbacks are functions and functions are not directly serializable, but they are serializable through the `$()` by converting them to QRLs first. + +## QRLs + +Function passing across serializable boundaries must be done through QRLs. QRLs are serialized forms of a function. (See [QRL](/docs/advanced/qrl) in advanced section.) + +Qwik has convenience APIs which end in `$` that are equivalent to calling `$()` directly. These two lines are equivalent: +- inline: ` +``` + +Creating a new callback for ` +``` + +This form allows the ` + +
+ ); +}); diff --git a/packages/docs/src/pages/tutorial/props/closures/solution/app.tsx b/packages/docs/src/pages/tutorial/props/closures/solution/app.tsx new file mode 100644 index 00000000000..e5ec9fe5cd7 --- /dev/null +++ b/packages/docs/src/pages/tutorial/props/closures/solution/app.tsx @@ -0,0 +1,23 @@ +import { component$, $, QRL } from '@builder.io/qwik'; + +export const App = component$(() => { + const goodbyeQrl = $(() => alert('Good Bye!')); + return ( +
+ alert('Hello ' + name)} /> +
+ ); +}); + +interface MyComponentProps { + goodbyeQrl?: QRL<() => void>; + helloQrl?: QRL<(name: string) => void>; +} +export const MyComponent = component$((props: MyComponentProps) => { + return ( +
+ + +
+ ); +}); diff --git a/packages/docs/src/pages/tutorial/props/mutable/index.mdx b/packages/docs/src/pages/tutorial/props/mutable/index.mdx new file mode 100644 index 00000000000..97096984d69 --- /dev/null +++ b/packages/docs/src/pages/tutorial/props/mutable/index.mdx @@ -0,0 +1,28 @@ +--- +title: Mutable Props +layout: tutorial +--- + +Look at the example on the right. It is a simple counter. The unique part about this counter is that the displaying of the count was broken out into a separate component called ``. + +When the user clicks on the `+1` button the `store.count` increments. This causes the `` component to re-render which in turn causes the `` component to re-render. + +But it does not work, because by default Qwik assumes that all property bindings are static. There is no way for Qwik runtime to tell if a property is static or it can change during the application lifetime. Qwik runtime assumes that all properties are static unless they are marked `mutable()`. + +## Why assume that properties are static + +Imagine for a second that the `` is bound to a static value like so: + +```jsx + +``` + +Looking at this code we can tell that `` will never need to be re-render from the outside. But when Qwik is serializing `` it does not know if the properties are static or dynamic (such information is not available at runtime). If it assumes that they are dynamic then it has to serialize the props all the time. If it assumes they are static then it only needs to serialize the props if the child component itself has its own behavior. + +Because Qwik wants to minimize the amount of HTML sent to the client, it is better to assume that the properties are static. But this presents a problem. How do we tell Qwik, that in this case, they are actually dynamic? We do this by wrapping the mutable binding in `mutable()` function call which provides the necessary runtime information to Qwik. + +You can fix the code example by changing `store.count` to `mutable(store.count)`. + +## Its better to not use `mutable()` + +See the next step in the tutorial on how we can avoid using `mutable()`. \ No newline at end of file diff --git a/packages/docs/src/pages/tutorial/props/mutable/problem/app.tsx b/packages/docs/src/pages/tutorial/props/mutable/problem/app.tsx new file mode 100644 index 00000000000..ae90c8e6550 --- /dev/null +++ b/packages/docs/src/pages/tutorial/props/mutable/problem/app.tsx @@ -0,0 +1,22 @@ +import { component$, useStore } from '@builder.io/qwik'; + +interface CountStore { + count: number; +} +export const App = component$(() => { + const store = useStore({ count: 0 }); + + return ( + <> + + + + ); +}); + +interface DisplayProps { + count: number; +} +export const Display = component$((props: DisplayProps) => { + return
The count is: {props.count}
; +}); diff --git a/packages/docs/src/pages/tutorial/props/mutable/solution/app.tsx b/packages/docs/src/pages/tutorial/props/mutable/solution/app.tsx new file mode 100644 index 00000000000..f07eccfbe9a --- /dev/null +++ b/packages/docs/src/pages/tutorial/props/mutable/solution/app.tsx @@ -0,0 +1,22 @@ +import { component$, useStore, mutable } from '@builder.io/qwik'; + +interface CountStore { + count: number; +} +export const App = component$(() => { + const store = useStore({ count: 0 }); + + return ( + <> + + + + ); +}); + +interface GreeterProps { + count: number; +} +export const Display = component$((props: GreeterProps) => { + return
The count is: {props.count}
; +}); diff --git a/packages/docs/src/pages/tutorial/props/store/index.mdx b/packages/docs/src/pages/tutorial/props/store/index.mdx new file mode 100644 index 00000000000..742dc200057 --- /dev/null +++ b/packages/docs/src/pages/tutorial/props/store/index.mdx @@ -0,0 +1,20 @@ +--- +title: Passing Stores +layout: tutorial +--- + +In the previous tutorial, we have shown that Qwik assumes that all props are static for efficiency reasons. The previous example introduced you to `mutable()` which told Qwik that the props are mutable. + +Besides the fact that typing `mutable()` is extra work, the previous example is also inefficient. When the user clicks on the `+1` it causes the `` to be re-rendered so it can update the `` bindings. The re-rendering of `` is needed to update the props of `` but there is no update to what the user sees, so it is a waste of resources. + +It would be more efficient to only re-render `` component. We can do that by passing in the `CountStore` rather than the `count` value. Because the store reference never changes the `` component will not need to re-render. + +Change the code to pass int `store` rather than `store.count`. + +By making the above change we gain two benefits: +- we remove `mutable()` from our component. +- `` does not need to be downloaded or re-rendered. + +## Best Practice + +It is considered best practice in Qwik to pass the store to the child component rather than pass the individual primitives which will require to be wrapped in `mutable()` to make the application work. \ No newline at end of file diff --git a/packages/docs/src/pages/tutorial/props/store/problem/app.tsx b/packages/docs/src/pages/tutorial/props/store/problem/app.tsx new file mode 100644 index 00000000000..40dba76a418 --- /dev/null +++ b/packages/docs/src/pages/tutorial/props/store/problem/app.tsx @@ -0,0 +1,22 @@ +import { component$, useStore, mutable } from '@builder.io/qwik'; + +interface CountStore { + count: number; +} +export const App = component$(() => { + const store = useStore({ count: 0 }); + + return ( + <> + + + + ); +}); + +interface DisplayProps { + count: number; +} +export const Display = component$((props: DisplayProps) => { + return
The count is: {props.count}
; +}); diff --git a/packages/docs/src/pages/tutorial/props/store/solution/app.tsx b/packages/docs/src/pages/tutorial/props/store/solution/app.tsx new file mode 100644 index 00000000000..23be1e88770 --- /dev/null +++ b/packages/docs/src/pages/tutorial/props/store/solution/app.tsx @@ -0,0 +1,22 @@ +import { component$, useStore } from '@builder.io/qwik'; + +interface CountStore { + count: number; +} +export const App = component$(() => { + const store = useStore({ count: 0 }); + + return ( + <> + + + + ); +}); + +interface DisplayProps { + store: CountStore; +} +export const Display = component$((props: DisplayProps) => { + return
The count is: {props.store.count}
; +}); diff --git a/packages/docs/src/pages/tutorial/qrl/closures/index.mdx b/packages/docs/src/pages/tutorial/qrl/closures/index.mdx new file mode 100644 index 00000000000..a3270211e53 --- /dev/null +++ b/packages/docs/src/pages/tutorial/qrl/closures/index.mdx @@ -0,0 +1,14 @@ +--- +title: Lazy Lading Closures +layout: tutorial +--- + +A closure can be converted into a lazy-loaded reference using the `$()` function. This generates a `QRL` type. A QRL is a lazy-loadable reference of the closure. In our case, we have extracted the closure associated with the `onKeyUp` event into the component body. Because it is no longer inlined we need to change how the JSX refers to it from `onKeyUp$` to `onKeyUpQrl`. + +Notice that our closure closes over the `store` that is captured by the Optimizer and then restored as needed. + +## Example + +In this example, we would like to demonstrate how easy it is to lazy-load behavior in Qwik. Let's lazy load the code that is executed when the `Enter` key is pressed. Wrap the code associated with `Enter` with `$()` to mark it for lazy loading. The resulting `QRL<()=>void>` can then be lazy-invoked with `.invoke()` and the result can be awaited with `await` keyword. + +Look at the `Symbols` tab to see how the code was broken up into pieces. \ No newline at end of file diff --git a/packages/docs/src/pages/tutorial/qrl/closures/problem/app.tsx b/packages/docs/src/pages/tutorial/qrl/closures/problem/app.tsx new file mode 100644 index 00000000000..c30f74a5dfd --- /dev/null +++ b/packages/docs/src/pages/tutorial/qrl/closures/problem/app.tsx @@ -0,0 +1,18 @@ +import { component$, useStore, $ } from '@builder.io/qwik'; + +export const App = component$(() => { + const store = useStore({ name: '' }); + const onKeyUp = $(async (event: KeyboardEvent) => { + const input = event.target as HTMLInputElement; + if (event.key === 'Enter') { + alert(store.name); + } else { + store.name = input.value; + } + }); + return ( + <> + Enter your name followed by the enter key: + + ); +}); diff --git a/packages/docs/src/pages/tutorial/qrl/closures/solution/app.tsx b/packages/docs/src/pages/tutorial/qrl/closures/solution/app.tsx new file mode 100644 index 00000000000..039e0546d10 --- /dev/null +++ b/packages/docs/src/pages/tutorial/qrl/closures/solution/app.tsx @@ -0,0 +1,20 @@ +import { component$, useStore, $ } from '@builder.io/qwik'; + +export const App = component$(() => { + const store = useStore({ name: '' }); + const onKeyUp = $(async (event: KeyboardEvent) => { + const input = event.target as HTMLInputElement; + if (event.key === 'Enter') { + await $(() => { + alert(store.name); + }).invoke(); + } else { + store.name = input.value; + } + }); + return ( + <> + Enter your name followed by the enter key: + + ); +}); diff --git a/packages/docs/src/pages/tutorial/qrl/data/index.mdx b/packages/docs/src/pages/tutorial/qrl/data/index.mdx new file mode 100644 index 00000000000..1bc5c1fdf19 --- /dev/null +++ b/packages/docs/src/pages/tutorial/qrl/data/index.mdx @@ -0,0 +1,17 @@ +--- +title: Loading Data +layout: tutorial +--- + +The Optimizer can be used to lazy-load data, not just functions. The benefit of doing that is that lazy loading any part of your application becomes easy. + +Use the stand-alone `$()` function to mark the data that you wish to lazy load. The `$()` function returns a `QRL` that is a reference that can be serialized by Qwik and that can later be resolved into the original value. + +## Example + +Load the data representing the massage lazily as part of the click listener. + +For this exercise imagine that we wish to lazy load the `'Hello World!`' string. Use the `$()` function to mark the string as lazy-loadable. Then use `await to resolve the lazy-loadable value. + + +Go to the `Symbols` tab and examine how the code was broken down into parts. \ No newline at end of file diff --git a/packages/docs/src/pages/tutorial/qrl/data/problem/app.tsx b/packages/docs/src/pages/tutorial/qrl/data/problem/app.tsx new file mode 100644 index 00000000000..4ddffb22cc6 --- /dev/null +++ b/packages/docs/src/pages/tutorial/qrl/data/problem/app.tsx @@ -0,0 +1,9 @@ +import { component$ } from '@builder.io/qwik'; + +export const App = component$(() => { + return ( + <> + + + ); +}); diff --git a/packages/docs/src/pages/tutorial/qrl/data/solution/app.tsx b/packages/docs/src/pages/tutorial/qrl/data/solution/app.tsx new file mode 100644 index 00000000000..58eeb049f3c --- /dev/null +++ b/packages/docs/src/pages/tutorial/qrl/data/solution/app.tsx @@ -0,0 +1,9 @@ +import { component$, $ } from '@builder.io/qwik'; + +export const App = component$(() => { + return ( + <> + + + ); +}); diff --git a/packages/docs/src/pages/tutorial/qrl/optimizer/index.mdx b/packages/docs/src/pages/tutorial/qrl/optimizer/index.mdx new file mode 100644 index 00000000000..ae460fef528 --- /dev/null +++ b/packages/docs/src/pages/tutorial/qrl/optimizer/index.mdx @@ -0,0 +1,22 @@ +--- +title: Optimizer +layout: tutorial +--- + +For the application to be resumable it needs to have lots of entry points. For example, clicking on button `A` is a different entry point than clicking on button `B`. When we implement an application we don't usually think about entry points and so we typically end up with just one entry point or the `main()` function. + +The Optimizer does its job by looking for functions that end in `$` character. For example, the Optimizer will transform a call to `component$()` into an entry point. Notice that the name of the function doesn't matter only that it ends with the `$`. + +Every time you see `$` you should think, there is a lazy-loaded boundary here. The implication is that the lazy-loaded content may require lazy-loading and hence can't be accessed synchronously. + +While the Optimizer can serialize any data that Qwik can serialize, it has special handling for closures. Closures are functions that are created inside of other functions and that may capture variables in the lexical scope. The ability to serialize closures is a key property that makes Qwik resumable. Without closure serialization, it would be difficult to have resumable applications. + +## Example + +In this example notice that we have two lazy-loaded chunks because we have two `$` in our code. + +Open the `Symbols` tab and notice how Optimizer turned the `onClick$` function into an entry point. Specifically, notice that the `onClick$` entry point does not import the `@builder.io/qwik` module. + +Now change the `onClick$` callback to `store.count++`. + +Open the `Symbols` tab again and notice that this time the Optimizer imported `@builder.io/qwik` and inserted `useLexicalScope()` call to restore the captured state of the event handler. Restoring the captured state of the function is what makes Qwik resumable. diff --git a/packages/docs/src/pages/tutorial/qrl/optimizer/problem/app.tsx b/packages/docs/src/pages/tutorial/qrl/optimizer/problem/app.tsx new file mode 100644 index 00000000000..d1dbcf1cb7d --- /dev/null +++ b/packages/docs/src/pages/tutorial/qrl/optimizer/problem/app.tsx @@ -0,0 +1,11 @@ +/* eslint-disable no-console */ +import { component$, useStore } from '@builder.io/qwik'; + +export const App = component$(() => { + const store = useStore({ count: 0 }); + return ( + <> + Count: {store.count} + + ); +}); diff --git a/packages/docs/src/pages/tutorial/qrl/optimizer/solution/app.tsx b/packages/docs/src/pages/tutorial/qrl/optimizer/solution/app.tsx new file mode 100644 index 00000000000..4ae1580d34e --- /dev/null +++ b/packages/docs/src/pages/tutorial/qrl/optimizer/solution/app.tsx @@ -0,0 +1,11 @@ +/* eslint-disable no-console */ +import { component$, useStore } from '@builder.io/qwik'; + +export const App = component$(() => { + const store = useStore({ count: 0 }); + return ( + <> + Count: {store.count} + + ); +}); diff --git a/packages/docs/src/pages/tutorial/reactivity/explicit/index.mdx b/packages/docs/src/pages/tutorial/reactivity/explicit/index.mdx new file mode 100644 index 00000000000..76f4b17efff --- /dev/null +++ b/packages/docs/src/pages/tutorial/reactivity/explicit/index.mdx @@ -0,0 +1,14 @@ +--- +title: Explicit Reactivity +layout: tutorial +--- + +In addition to implicit reactivity created by the templates, Qwik supports explicit execution of code when a property changes. This is achieved through the `useWatch$()` hook. `useWatch$()` hooks execute before the component renders and can be asynchronous. The hook can also have a clean-up function that is invoked on the next hook execution or when the component is removed. + +In this example clicking on `+1` updates `count` immediately. What we would like is to update the `delay count` after a 2-second delay. If `count` is updated before the 2 seconds are up then the timer is restarted. + +Notice that `useWatch$()` callback receives `track` function. Use the `track` function to tell Qwik which properties should trigger this watch. The `track` function creates subscriptions in store. On each invocation of `useWatch$()` the subscriptions are cleared, so it is important to always set up a new set of subscriptions. This is useful if the set of subscriptions changes during the function lifetime. + +The `useWatch$()` callback function can return a cleanup function. The clean-up function is invoked on the next `useWatch$()` callback execution or when the component is removed. In our case, the cleanup function is used for returning code which clears the `setTimeout`. + +The `useWatch$()` callbacks execute before the component is rendered. This allows them to be used to compute values used in rendering. The function runs on both server and client. The server execution sets up subscriptions that are then serialized and available to the client. This saves the client from having to download all of the components and execute them at least once to recover the subscription information for the system. \ No newline at end of file diff --git a/packages/docs/src/pages/tutorial/reactivity/explicit/problem/app.tsx b/packages/docs/src/pages/tutorial/reactivity/explicit/problem/app.tsx new file mode 100644 index 00000000000..eef4f53ef8b --- /dev/null +++ b/packages/docs/src/pages/tutorial/reactivity/explicit/problem/app.tsx @@ -0,0 +1,38 @@ +/* eslint-disable no-console */ +import { component$, useStore, useWatch$ } from '@builder.io/qwik'; + +interface AppStore { + count: number; + delayCount: number; +} +export const App = component$(() => { + const store = useStore({ + count: 0, + delayCount: 0, + }); + console.log('Render: '); + useWatch$((track) => { + // tracking `store.count` + // setup a timer to copy `count => delayCount` after 2 seconds. + return () => { + // cleanup code + }; + }); + return ( + <> + + + + + ); +}); + +export const DisplayCount = component$((props: { store: AppStore }) => { + console.log('Render: '); + return <>{props.store.count}; +}); + +export const DisplayDelayCount = component$((props: { store: AppStore }) => { + console.log('Render: '); + return <>{props.store.delayCount}; +}); diff --git a/packages/docs/src/pages/tutorial/reactivity/explicit/solution/app.tsx b/packages/docs/src/pages/tutorial/reactivity/explicit/solution/app.tsx new file mode 100644 index 00000000000..9d540d15e23 --- /dev/null +++ b/packages/docs/src/pages/tutorial/reactivity/explicit/solution/app.tsx @@ -0,0 +1,36 @@ +/* eslint-disable no-console */ +import { component$, useStore, useWatch$ } from '@builder.io/qwik'; + +interface AppStore { + count: number; + delayCount: number; +} +export const App = component$(() => { + const store = useStore({ + count: 0, + delayCount: 0, + }); + console.log('Render: '); + useWatch$((track) => { + track(store, 'count'); + const id = setTimeout(() => (store.delayCount = store.count), 2000); + return () => clearTimeout(id); + }); + return ( + <> + + + + + ); +}); + +export const DisplayCount = component$((props: { store: AppStore }) => { + console.log('Render: '); + return <>{props.store.count}; +}); + +export const DisplayDelayCount = component$((props: { store: AppStore }) => { + console.log('Render: '); + return <>{props.store.delayCount}; +}); diff --git a/packages/docs/src/pages/tutorial/reactivity/template/index.mdx b/packages/docs/src/pages/tutorial/reactivity/template/index.mdx new file mode 100644 index 00000000000..e9c9419e6ed --- /dev/null +++ b/packages/docs/src/pages/tutorial/reactivity/template/index.mdx @@ -0,0 +1,16 @@ +--- +title: Implicit Template Updates +layout: tutorial +--- + +This example demonstrates how mutating stores automatically update the templates. + +During SSR rendering the server needs to render all of the components in the application. As it is rendering the components the bindings in those components perform reads on store properties. For example, when `` reads the `countA` property from the store, Qwik records that as a subscription. Qwik now knows that if `countA` changes then `` needs to be re-rendered. Rendering templates will automatically set up subscriptions on the store. Each time the template re-renders the old subscriptions are thrown away and new subscriptions are created. This means that the template can change the set of things it is listening to during its lifecycle. + +Currently, the buttons don't do anything. Implement the buttons to increment the respective store properties. + +Once you make the buttons work, notice that even though all state is stored in a single store, the updates are very focused. `a++` button will only cause the re-rendering of `` and `b++` button will only cause re-rendering of ``. The fine-grained re-rendering is an important property of Qwik. It is what allows Qwik applications to stay lean and not download too much code unnecessarily. + +Template subscriptions are automatically created and released when the component is removed. There is no need to keep track of them or release them manually. + +Qwik is a reactive system. All reactive systems require a single full execution of the application to create subscriptions. Qwik applications also require full execution to set up all subscriptions. However, Qwik applications perform the full execution on the server and transfer the subscription information to the client. In this way, the client knows which component needs to be re-rendered when without being forced to do one full rendering of the whole application. Doing so would force all components to be eagerly downloaded, and Qwik wants to avoid that. \ No newline at end of file diff --git a/packages/docs/src/pages/tutorial/reactivity/template/problem/app.tsx b/packages/docs/src/pages/tutorial/reactivity/template/problem/app.tsx new file mode 100644 index 00000000000..df7e6ae95e4 --- /dev/null +++ b/packages/docs/src/pages/tutorial/reactivity/template/problem/app.tsx @@ -0,0 +1,33 @@ +/* eslint-disable no-console */ +import { component$, useStore } from '@builder.io/qwik'; + +interface AppStore { + countA: number; + countB: number; +} +export const App = component$(() => { + const store = useStore({ + countA: 0, + countB: 0, + }); + console.log('Render: '); + return ( + <> + + +
+ + + + ); +}); + +export const DisplayA = component$((props: { store: AppStore }) => { + console.log('Render: '); + return <>{props.store.countA}; +}); + +export const DisplayB = component$((props: { store: AppStore }) => { + console.log('Render: '); + return <>{props.store.countB}; +}); diff --git a/packages/docs/src/pages/tutorial/reactivity/template/solution/app.tsx b/packages/docs/src/pages/tutorial/reactivity/template/solution/app.tsx new file mode 100644 index 00000000000..129ba8c096b --- /dev/null +++ b/packages/docs/src/pages/tutorial/reactivity/template/solution/app.tsx @@ -0,0 +1,33 @@ +/* eslint-disable no-console */ +import { component$, useStore } from '@builder.io/qwik'; + +interface AppStore { + countA: number; + countB: number; +} +export const App = component$(() => { + const store = useStore({ + countA: 0, + countB: 0, + }); + console.log('Render: '); + return ( + <> + + +
+ + + + ); +}); + +export const DisplayA = component$((props: { store: AppStore }) => { + console.log('Render: '); + return <>{props.store.countA}; +}); + +export const DisplayB = component$((props: { store: AppStore }) => { + console.log('Render: '); + return <>{props.store.countB}; +}); diff --git a/packages/docs/src/pages/tutorial/store/basic/index.mdx b/packages/docs/src/pages/tutorial/store/basic/index.mdx new file mode 100644 index 00000000000..a46a4a6e28b --- /dev/null +++ b/packages/docs/src/pages/tutorial/store/basic/index.mdx @@ -0,0 +1,22 @@ +--- +title: Storing State +layout: tutorial +--- + +Applications need to store state to be useful. (Otherwise, they are just static pages.) + +Qwik needs to track the application state for two reasons: +1. Application state needs to be serialized on application pause (and deserialize on application resume.) +2. Qwik needs to create subscriptions on stores so that it can know which components need to be re-rendered (if Qwik would not track subscription information then it would have to re-render the whole application which would defeat the lazy-loading.) + +The component on the right looks like it should work, but it does not because the `counter` is just a regular objects, which does not create subscriptions. As a result, Qwik does not know when `counter.count` changes and it does not know that it has to re-render the ``. + +Wrap the `counter` in `useStore()` to enable dependency tracking and automatic re-rendering. + +## Serialization + +Open the HTML tab to see what information was serialized by the server. find `