diff --git a/package.json b/package.json index 2bded13fcd7..a1d5231b6ac 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "qwik-monorepo", - "version": "0.0.20-8", + "version": "0.0.20", "scripts": { "build": "yarn node scripts --tsc --build --api --platform-binding-wasm-copy", "build.full": "yarn node scripts --tsc --build --api --eslint --platform-binding --wasm", diff --git a/packages/create-qwik/package.json b/packages/create-qwik/package.json index ba8246e9a6f..dcfb69b70b6 100644 --- a/packages/create-qwik/package.json +++ b/packages/create-qwik/package.json @@ -1,6 +1,6 @@ { "name": "create-qwik", - "version": "0.0.20-8", + "version": "0.0.20", "description": "Interactive CLI and API for generating Qwik projects.", "bin": "create-qwik", "main": "index.js", diff --git a/packages/docs/src/components/repl/worker/request-handler.ts b/packages/docs/src/components/repl/worker/request-handler.ts index d942d5d1bb4..9e338b24c12 100644 --- a/packages/docs/src/components/repl/worker/request-handler.ts +++ b/packages/docs/src/components/repl/worker/request-handler.ts @@ -56,7 +56,7 @@ export const requestHandler = (ev: FetchEvent) => { headers: { 'Content-Type': 'application/javascript; charset=utf-8', 'Cache-Control': 'no-store', - 'X-Qwik-REPL-App': 'client-module', + 'X-Qwik-REPL-App': 'ssr-result', 'X-Qwik-Client-Id': clientId, }, }) diff --git a/packages/eslint-plugin-qwik/package.json b/packages/eslint-plugin-qwik/package.json index 301cb31c817..ac957c82dee 100644 --- a/packages/eslint-plugin-qwik/package.json +++ b/packages/eslint-plugin-qwik/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-qwik", - "version": "0.0.20-8", + "version": "0.0.20", "description": "An Open-Source sub-framework designed with a focus on server-side-rendering, lazy-loading, and styling/animation.", "main": "index.js", "author": "Builder Team", diff --git a/packages/qwik/package.json b/packages/qwik/package.json index d2fadc9b3d4..af6b3741a44 100644 --- a/packages/qwik/package.json +++ b/packages/qwik/package.json @@ -1,6 +1,6 @@ { "name": "@builder.io/qwik", - "version": "0.0.20-8", + "version": "0.0.20", "description": "An Open-Source sub-framework designed with a focus on server-side-rendering, lazy-loading, and styling/animation.", "main": "./dist/core.cjs", "module": "./dist/core.mjs", diff --git a/packages/qwik/src/core/api.md b/packages/qwik/src/core/api.md index 88173ed5544..dddf9e0d290 100644 --- a/packages/qwik/src/core/api.md +++ b/packages/qwik/src/core/api.md @@ -362,7 +362,7 @@ export type On$Props = { }; // @public (undocumented) -export type OnRenderFn = (props: PROPS) => ValueOrPromise | null>; +export type OnRenderFn = (props: PROPS) => ValueOrPromise | null | (() => JSXNode)>; // @alpha export function pauseContainer(elmOrDoc: Element | Document): SnapshotResult; @@ -576,6 +576,16 @@ export const useClientEffect$: (first: WatchFn, opts?: UseEffectOptions | undefi // @public export function useClientEffectQrl(qrl: QRL, opts?: UseEffectOptions): void; +// Warning: (ae-incompatible-release-tags) The symbol "useClientMount$" is marked as @public, but its signature references "ServerFn" which is marked as @alpha +// +// @public +export const useClientMount$: (first: ServerFn) => void; + +// Warning: (ae-incompatible-release-tags) The symbol "useClientMountQrl" is marked as @public, but its signature references "ServerFn" which is marked as @alpha +// +// @public +export function useClientMountQrl(mountQrl: QRL): void; + // @alpha (undocumented) export function useContext(context: Context): STATE; @@ -600,6 +610,16 @@ export function useHostElement(): Element; // @public export function useLexicalScope(): VARS; +// Warning: (ae-incompatible-release-tags) The symbol "useMount$" is marked as @public, but its signature references "ServerFn" which is marked as @alpha +// +// @public +export const useMount$: (first: ServerFn) => void; + +// Warning: (ae-incompatible-release-tags) The symbol "useMountQrl" is marked as @public, but its signature references "ServerFn" which is marked as @alpha +// +// @public +export function useMountQrl(mountQrl: QRL): void; + // @alpha export function useOn(event: string, eventFn: QRL<() => void>): void; @@ -634,7 +654,7 @@ export const useServerMount$: (first: ServerFn) => void; // Warning: (ae-incompatible-release-tags) The symbol "useServerMountQrl" is marked as @public, but its signature references "ServerFn" which is marked as @alpha // // @public -export function useServerMountQrl(watchQrl: QRL): void; +export function useServerMountQrl(mountQrl: QRL): void; // @public export function useStore(initialState: STATE | (() => STATE)): STATE; diff --git a/packages/qwik/src/core/component/component-ctx.ts b/packages/qwik/src/core/component/component-ctx.ts index aeba297a248..22d9e062f00 100644 --- a/packages/qwik/src/core/component/component-ctx.ts +++ b/packages/qwik/src/core/component/component-ctx.ts @@ -61,10 +61,14 @@ export const renderComponent = (rctx: RenderContext, ctx: QContext): ValueOrProm appendStyle(rctx, hostElement, task); } }); - if (ctx.dirty) { + if (typeof jsxNode === 'function') { + ctx.dirty = false; + jsxNode = jsxNode(); + } else if (ctx.dirty) { logDebug('Dropping render. State changed during render.'); return renderComponent(rctx, ctx); } + let componentCtx = ctx.component; if (!componentCtx) { componentCtx = ctx.component = { diff --git a/packages/qwik/src/core/component/component.public.ts b/packages/qwik/src/core/component/component.public.ts index 2709f69bbe4..f949914b6ce 100644 --- a/packages/qwik/src/core/component/component.public.ts +++ b/packages/qwik/src/core/component/component.public.ts @@ -559,7 +559,9 @@ export function component$( /** * @public */ -export type OnRenderFn = (props: PROPS) => ValueOrPromise | null>; +export type OnRenderFn = ( + props: PROPS +) => ValueOrPromise | null | (() => JSXNode)>; export interface RenderFactoryOutput { renderQRL: QRL>; diff --git a/packages/qwik/src/core/examples.tsx b/packages/qwik/src/core/examples.tsx index 364b050412c..c969a3eca4d 100644 --- a/packages/qwik/src/core/examples.tsx +++ b/packages/qwik/src/core/examples.tsx @@ -18,7 +18,13 @@ import { } from './component/component.public'; import { Host } from './render/jsx/host.public'; import { $, implicit$FirstArg, QRL } from './import/qrl.public'; -import { useClientEffect$, useServerMount$, useWatch$ } from './watch/watch.public'; +import { + useClientEffect$, + useServerMount$, + useClientMount$, + useMount$, + useWatch$, +} from './watch/watch.public'; import { useHostElement } from './use/use-host-element.public'; import { useRef } from './use/use-store.public'; @@ -249,7 +255,6 @@ export const CmpInline = component$(() => { users: [], }); - // Double count watch useServerMount$(async () => { // This code will ONLY run once in the server, when the component is mounted store.users = await db.requestUsers(); @@ -275,34 +280,47 @@ export const CmpInline = component$(() => { }; () => { - let db: any; - // + // const Cmp = component$(() => { const store = useStore({ - users: [], + hash: '', }); - // Double count watch - useServerMount$(async () => { - // This code will ONLY run once in the server, when the component is mounted - store.users = await db.requestUsers(); + useClientMount$(async () => { + // This code will ONLY run once in the client, when the component is mounted + store.hash = document.location.hash; }); return ( - {store.users.map((user) => ( - - ))} +

The url hash is: ${store.hash}

); }); + //
+ return Cmp; +}; - interface User { - name: string; - } - function User(props: { user: User }) { - return
Name: {props.user.name}
; - } +() => { + // + const Cmp = component$(() => { + const store = useStore({ + temp: 0, + }); + + useMount$(async () => { + // This code will run once whenever a component is mounted in the server, or in the client + const res = await fetch('weather-api.example'); + const json = (await res.json()) as any; + store.temp = json.temp; + }); + + return ( + +

The temperature is: ${store.temp}

+
+ ); + }); //
return Cmp; }; diff --git a/packages/qwik/src/core/import/qrl-class.ts b/packages/qwik/src/core/import/qrl-class.ts index 55d4b6412a7..36e4666dd02 100644 --- a/packages/qwik/src/core/import/qrl-class.ts +++ b/packages/qwik/src/core/import/qrl-class.ts @@ -1,5 +1,4 @@ import { InvokeContext, newInvokeContext, useInvoke } from '../use/use-core'; -import { emitEvent } from '../util/event'; import { then } from '../util/promises'; import type { ValueOrPromise } from '../util/types'; import { qrlImport, QRLSerializeOptions, stringifyQRL } from './qrl'; @@ -90,11 +89,10 @@ export const getCanonicalSymbol = (symbolName: string) => { }; export const isSameQRL = (a: QRL, b: QRL): boolean => { - return getCanonicalSymbol(a.symbol) === getCanonicalSymbol(b.symbol); + const symA = a.refSymbol ?? a.symbol; + const symB = b.refSymbol ?? b.symbol; + return getCanonicalSymbol(symA) === getCanonicalSymbol(symB); }; export type QRLInternal = QRL; export const QRLInternal: typeof QRL = QRL; - -// https://regexr.com/6enjv -const FIND_EXT = /\?[\w=&]+$/; diff --git a/packages/qwik/src/core/index.ts b/packages/qwik/src/core/index.ts index d6551922e17..03c5bd32a84 100644 --- a/packages/qwik/src/core/index.ts +++ b/packages/qwik/src/core/index.ts @@ -60,6 +60,9 @@ export type { export { useWatch$, useWatchQrl } from './watch/watch.public'; export { useClientEffect$, useClientEffectQrl } from './watch/watch.public'; export { useServerMount$, useServerMountQrl } from './watch/watch.public'; +export { useClientMount$, useClientMountQrl } from './watch/watch.public'; +export { useMount$, useMountQrl } from './watch/watch.public'; + export { handleWatch } from './watch/watch.public'; ////////////////////////////////////////////////////////////////////////////////////////// diff --git a/packages/qwik/src/core/readme.md b/packages/qwik/src/core/readme.md index c0b07e254f7..b3dc997d607 100644 --- a/packages/qwik/src/core/readme.md +++ b/packages/qwik/src/core/readme.md @@ -97,20 +97,44 @@ The `obs` passed into the `watchFn` is used to mark `state.count` as a property @public -# `useServerMount` +# `useClientEffect` + + + +@public -Register's a server mount hook, that runs only in server when the component is first mounted. `useWatch` will run once in the server, and N-times in the client, only when the **tracked** state changes. +# `useMount` + +Register's a mount hook, that runs both in the server and the client when the component is first mounted. ## Example - + +@see `useServerMount` `useClientMount` @public -# `useClientEffect` +# `useClientMount` - +Register's a client mount hook, that runs only in client when the component is first mounted. + +## Example + + + +@see `useServerMount` `useMount` + +@public + +# `useServerMount` + +Register's a server mount hook, that runs only in server when the component is first mounted. + +## Example + + +@see `useClientMount` `useMount` @public # `useStyles` diff --git a/packages/qwik/src/core/watch/watch.public.ts b/packages/qwik/src/core/watch/watch.public.ts index df386eede98..a2384186c50 100644 --- a/packages/qwik/src/core/watch/watch.public.ts +++ b/packages/qwik/src/core/watch/watch.public.ts @@ -300,8 +300,6 @@ export const useClientEffect$ = implicit$FirstArg(useClientEffectQrl); // (edit ../readme.md#useServerMount instead) /** * Register's a server mount hook, that runs only in server when the component is first mounted. - * `useWatch` will run once in the server, and N-times in the client, only when the **tracked** - * state changes. * * ## Example * @@ -311,7 +309,6 @@ export const useClientEffect$ = implicit$FirstArg(useClientEffectQrl); * users: [], * }); * - * // Double count watch * useServerMount$(async () => { * // This code will ONLY run once in the server, when the component is mounted * store.users = await db.requestUsers(); @@ -332,12 +329,37 @@ export const useClientEffect$ = implicit$FirstArg(useClientEffectQrl); * function User(props: { user: User }) { * return
Name: {props.user.name}
; * } + * ``` + * + * @see `useClientMount` `useMount` + * @public + */ +//
+export function useServerMountQrl(mountQrl: QRL): void { + const [watch, setWatch] = useSequentialScope(); + if (!watch) { + setWatch(true); + const isServer = getPlatform(useDocument()).isServer; + if (isServer) { + useWaitOn(mountQrl.invoke()); + } + } +} + +// +// !!DO NOT EDIT THIS COMMENT DIRECTLY!!! +// (edit ../readme.md#useServerMount instead) +/** + * Register's a server mount hook, that runs only in server when the component is first mounted. + * + * ## Example + * + * ```tsx * const Cmp = component$(() => { * const store = useStore({ * users: [], * }); * - * // Double count watch * useServerMount$(async () => { * // This code will ONLY run once in the server, when the component is mounted * store.users = await db.requestUsers(); @@ -360,89 +382,166 @@ export const useClientEffect$ = implicit$FirstArg(useClientEffectQrl); * } * ``` * + * @see `useClientMount` `useMount` * @public */ // -export function useServerMountQrl(watchQrl: QRL): void { +export const useServerMount$ = implicit$FirstArg(useServerMountQrl); + +// +// !!DO NOT EDIT THIS COMMENT DIRECTLY!!! +// (edit ../readme.md#useClientMount instead) +/** + * Register's a client mount hook, that runs only in client when the component is first mounted. + * + * ## Example + * + * ```tsx + * const Cmp = component$(() => { + * const store = useStore({ + * hash: '' + * }); + * + * useClientMount$(async () => { + * // This code will ONLY run once in the client, when the component is mounted + * store.hash = document.location.hash + * }); + * + * return ( + * + *

The url hash is: ${store.hash}

+ *
+ * ); + * }); + * ``` + * + * @see `useServerMount` `useMount` + * + * @public + */ +//
+export function useClientMountQrl(mountQrl: QRL): void { const [watch, setWatch] = useSequentialScope(); if (!watch) { setWatch(true); const isServer = getPlatform(useDocument()).isServer; - if (isServer) { - useWaitOn(watchQrl.invoke()); + if (!isServer) { + useWaitOn(mountQrl.invoke()); } } } -// +// // !!DO NOT EDIT THIS COMMENT DIRECTLY!!! -// (edit ../readme.md#useServerMount instead) +// (edit ../readme.md#useClientMount instead) /** - * Register's a server mount hook, that runs only in server when the component is first mounted. - * `useWatch` will run once in the server, and N-times in the client, only when the **tracked** - * state changes. + * Register's a client mount hook, that runs only in client when the component is first mounted. * * ## Example * * ```tsx * const Cmp = component$(() => { * const store = useStore({ - * users: [], + * hash: '' * }); * - * // Double count watch - * useServerMount$(async () => { - * // This code will ONLY run once in the server, when the component is mounted - * store.users = await db.requestUsers(); + * useClientMount$(async () => { + * // This code will ONLY run once in the client, when the component is mounted + * store.hash = document.location.hash * }); * * return ( * - * {store.users.map((user) => ( - * - * ))} + *

The url hash is: ${store.hash}

*
* ); * }); + * ``` * - * interface User { - * name: string; - * } - * function User(props: { user: User }) { - * return
Name: {props.user.name}
; - * } + * @see `useServerMount` `useMount` + * + * @public + */ +//
+export const useClientMount$ = implicit$FirstArg(useClientMountQrl); + +// +// !!DO NOT EDIT THIS COMMENT DIRECTLY!!! +// (edit ../readme.md#useMount instead) +/** + * Register's a mount hook, that runs both in the server and the client when the component is + * first mounted. + * + * ## Example + * + * ```tsx * const Cmp = component$(() => { * const store = useStore({ - * users: [], + * temp: 0, * }); * - * // Double count watch - * useServerMount$(async () => { - * // This code will ONLY run once in the server, when the component is mounted - * store.users = await db.requestUsers(); + * useMount$(async () => { + * // This code will run once whenever a component is mounted in the server, or in the client + * const res = await fetch('weather-api.example'); + * const json = await res.json() as any; + * store.temp = json.temp; * }); * * return ( * - * {store.users.map((user) => ( - * - * ))} + *

The temperature is: ${store.temp}

*
* ); * }); + * ``` * - * interface User { - * name: string; - * } - * function User(props: { user: User }) { - * return
Name: {props.user.name}
; - * } + * @see `useServerMount` `useClientMount` + * @public + */ +//
+export function useMountQrl(mountQrl: QRL): void { + const [watch, setWatch] = useSequentialScope(); + if (!watch) { + setWatch(true); + useWaitOn(mountQrl.invoke()); + } +} + +// +// !!DO NOT EDIT THIS COMMENT DIRECTLY!!! +// (edit ../readme.md#useMount instead) +/** + * Register's a mount hook, that runs both in the server and the client when the component is + * first mounted. + * + * ## Example + * + * ```tsx + * const Cmp = component$(() => { + * const store = useStore({ + * temp: 0, + * }); + * + * useMount$(async () => { + * // This code will run once whenever a component is mounted in the server, or in the client + * const res = await fetch('weather-api.example'); + * const json = await res.json() as any; + * store.temp = json.temp; + * }); + * + * return ( + * + *

The temperature is: ${store.temp}

+ *
+ * ); + * }); * ``` * + * @see `useServerMount` `useClientMount` * @public */ //
-export const useServerMount$ = implicit$FirstArg(useServerMountQrl); +export const useMount$ = implicit$FirstArg(useMountQrl); export function runWatch(watch: WatchDescriptor): Promise { if (!(watch.f & WatchFlags.IsDirty)) { diff --git a/starters/apps/e2e/src/components/effect-client/effect-client.tsx b/starters/apps/e2e/src/components/effect-client/effect-client.tsx index d6ee96c893e..20d7c2da688 100644 --- a/starters/apps/e2e/src/components/effect-client/effect-client.tsx +++ b/starters/apps/e2e/src/components/effect-client/effect-client.tsx @@ -1,5 +1,5 @@ /* eslint-disable */ -import { component$, useClientEffect$, useStore, useStyles$ } from '@builder.io/qwik'; +import { component$, Host, useClientEffect$, useStore, useStyles$ } from '@builder.io/qwik'; export const EffectClient = component$(() => { useStyles$(`.box { @@ -32,6 +32,12 @@ export const Timer = component$(() => { const state = useStore({ count: 0, + msg: 'empty', + }); + + // Double count watch + useClientEffect$(() => { + state.msg = 'run'; }); // Double count watch @@ -45,5 +51,10 @@ export const Timer = component$(() => { }; }); - return
{state.count}
; + return ( + +
{state.count}
+
{state.msg}
+
+ ); }); diff --git a/starters/e2e/e2e.spec.ts b/starters/e2e/e2e.spec.ts index eb5f811c6fd..5d63226b21a 100644 --- a/starters/e2e/e2e.spec.ts +++ b/starters/e2e/e2e.spec.ts @@ -401,4 +401,29 @@ test.describe('e2e', () => { ]); }); }); + + test.describe('effect-client', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/e2e/effect-client'); + page.on('pageerror', (err) => expect(err).toEqual(undefined)); + }); + + test('should load', async ({ page }) => { + const counter = await page.locator('#counter'); + const msg = await page.locator('#msg'); + + await expect(counter).toHaveText('0'); + await expect(msg).toHaveText('empty'); + + await counter.scrollIntoViewIfNeeded(); + await page.waitForTimeout(100); + + await expect(counter).toHaveText('10'); + await expect(msg).toHaveText('run'); + + await page.waitForTimeout(500); + await expect(counter).toHaveText('11'); + await expect(msg).toHaveText('run'); + }); + }); }); diff --git a/yarn.lock b/yarn.lock index 27228c490d9..843324618df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -450,7 +450,14 @@ __metadata: languageName: node linkType: hard -"@builder.io/qwik@0.0.20-8, @builder.io/qwik@workspace:packages/qwik": +"@builder.io/qwik@npm:0.0.20-8": + version: 0.0.20-8 + resolution: "@builder.io/qwik@npm:0.0.20-8" + checksum: 2a67a7f484a3f4bd3a7ddf74608422b3a8fcd4df3e5e9e608b51f10095e785de557bcaf3fd4df3e595bf58e3195ce56223828019a6ebc5178a92ccc901c60ea7 + languageName: node + linkType: hard + +"@builder.io/qwik@workspace:packages/qwik": version: 0.0.0-use.local resolution: "@builder.io/qwik@workspace:packages/qwik" languageName: unknown