Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

refactor: merge useMount and useWatch into useTask #2379

Merged
merged 7 commits into from
Dec 10, 2022
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
🍦
  • Loading branch information
manucorporat committed Dec 6, 2022
commit c7fc1e22fe35c69e22c3d8f47677d2ccbf890f9d
180 changes: 83 additions & 97 deletions packages/docs/src/routes/docs/components/lifecycle/index.mdx
Original file line number Diff line number Diff line change
@@ -11,116 +11,62 @@ import diagram from './diagram2.svg';

# Lifecycles

[Resumability](/docs/concepts/resumable/index.mdx) plays a key role to understand how lifecycle hooks work in Qwik.
Thanks to [Resumability](/docs/concepts/resumable/index.mdx), components life and its lifecycle extend across server and browser. Sometimes the component will be first rendered in the server, sometimes it could be in the browser, however, in both cases the lifecycle will be the same, only its execution happens in different environments.

Usually **the life of a component starts on the server** (during SSR or SSG), in that case the hooks will run like this:
Usually **the life of a component starts on the server** (during SSR or SSG), in that case, the hooks will run like this:

```
useMount$ -> useTask$ -> useResource$ -> RENDER -> useClientEffect$
|
|--------------------- SERVER ---------------------|----- BROWSER -----|
|
pause|resume
useTask$ --> RENDER --> useClientDOM$
|
| ------ SERVER ------ | --- BROWSER --- |
|
pause|resume
```

> **Notice** that because the component was mounted in the server, **only useClientEffect$() runs in the browser**. This is because the browser continues the same lifecycle, that was paused in the server and resumed in the browser.
> **Notice** that because the component was mounted in the server, **only useClientDOM$() runs in the browser**. This is because the browser continues the same lifecycle, that was paused in the server right after the render and resumed in the browser.

What if a **component is first rendered/mounted in the browser**? In that case the hooks will run like this:
Sometimes a component will be first mounted/rendered in the browser, for example when the user SPA navigates to a new page, or a "modal" component first appears in the page. In that case, the lifecycle will run like this:

```
useMount$ -> useTask$ -> useResource$ -> RENDER -> useClientEffect$
useTask$ --> RENDER --> useClientDOM$

|------------------------------- BROWSER ------------------------------|
| -------------- BROWSER -------------- |
```

> **Notice** that the lifecycle looks exactly the same, but this time all the hooks run in the browser, and non in the server.
> **Notice** that the lifecycle is exactly the same, but this time all the hooks run in the browser, and non in the server.

For now, we know that mounting happens exactly once across all platforms, this is a core difference between Qwik and other frameworks, were the lifecycle is repeated, and components are mounted during SSR and during hydration.

## `useMount$()`
## `useTask$()`

- **When:** BEFORE component's first render
- **Times:** exactly once
- **When:** BEFORE component's first render, and when tracked state changes
- **Times:** at least once
- **Platform:** server and browser

`useMount$()` registers a hook to be executed upon component creation, while `useMount$()` can execute on either the server or on the client, it runs exactly once. (Either on the server or on the client, depending on where the component got first rendered).

`useMount$()` will block the rendering of the component until after the async callback resolves. (This is useful for fetching asynchronous data and delaying rendering until data is received, ensuring that the rendered component contains the data)
`useTask$()` registers a hook to be executed upon component creation, it will run at least one either in the server or in the browser, depending where the component is initially rendered.

### Example
Additionally, this task can be reactive and re-execute when some **tracked** [signal or store](/docs/components/state/index.mdx) changes, like this:

```tsx
export const Cmp = component$(() => {
const store = useStore({
users: [],
});
useMount$(async () => {
// This code will run on component creation to fetch the data.
store.users = await db.requestUsers();
});
return (
<>
{store.users.map((user) => (
<User user={user} />
))}
</>
);
});

interface User {
name: string;
}

export function User(props: { user: User }) {
return <div>Name: {props.user.name}</div>;
}
useTask$(({track}) => {
track(() => store.count));
// will run at mount and every time "store.count" changes
})
```

### Server/Client only mount

Qwik does not provide a specific hook to run code only on the server or only on the client during mount. However, you can use `@builder.io/qwik/build` conditionals to achieve this:

```tsx
import { isServer, isBrowser } from '@builder.io/qwik/build';
**Notice that any subsequent execution will always happen in the browser**, because reactivity is a client-only thing.

export const Cmp = component$(() => {
const store = useStore({
users: [],
});
useMount$(async () => {
if (isServer) {
// If the component is mounted on the server, call DB directly.
store.users = await db.requestUsers();
}
if (isBrowser) {
// If the component is mounted on the browser, fetch users through API.
store.users = await fetchGetUser();
}
});

return (
<>
{store.users.map((user) => (
<User user={user} />
))}
</>
);
});
```
useTask$(track) -> RENDER -> CLICK (state changes) -> useTask$(track)
▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ | ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
| --------- SERVER --------- | --------------- BROWSER --------------- |
|
pause|resume
```

### When to use `useMount$()`

You wanna run some code ONLY when the component mounts, once.

## `useTask$()`

- **When:** BEFORE component's first render and when tracked state changes
- **Times:** at least once
- **Platform:** server and browser
> If a `useTask$()` does not track any state, it will run **exactly once**, either in the server or in the browser, depending where the component is initially rendered. Effectively behaving like a on mount hook.

Just like `useMount$()`, the hook is called the first time when the component mounts, however it can be called multiple times if (and only if) it tracks a [signal or store](/docs/components/state/index.mdx) for changes.
`useTask$()` will block the rendering of the component until after the async callback resolves. (This is useful for fetching asynchronous data and delaying rendering until data is received, ensuring that the rendered component contains the data)

> `useTask$()` is different from React's useEffect in that, `useWatch` also runs during SSR and before rendering.

### Example

@@ -135,6 +81,12 @@ export const Cmp = component$(() => {
doubleCount: 0,
});

// this task will be called exactly once, either on the server or on the browser
useTask$(() => {
console.log('component mounted');
});

// this task will be called at mount and every time `store.count` changes
useTask$(({ track }) => {
const count = track(() => store.count);
store.doubleCount = 2 * count;
@@ -152,26 +104,59 @@ The example above uses the `track` function to watch changes in `() => store.cou

> Notice that `useTask$()` runs **BEFORE** the actual rendering and in the server, thus manual DOM manipulation must be done with caution.

> See also the `useClientEffect$()` hook that shares similar semantics but only runs on the client after rendering.
> See also the `useClientDOM$()` hook that shares similar semantics but only runs on the client after rendering.

### When to use `useTask$()`

You wanna mutate some state, or perform some action when some state changes. i.e, you need to have some side effects.
- Run async tasks before rendering
- Run code only once before component is first rendered
- Compute derived state from tracked state
- Programatically run code when state changes

> Note, if you wanna load some data (for example a fetch()), to later use that in your component, look at [`useResource$()`](/docs/components/resource/index.mdx). This API will be even more efficiently leveraging SSR streaming and parallelisim.

## `useClientEffect$()`
### Server/Client only task

Sometimes is desired to only run code either in the server or in the client. This can be achieved by using the `isServer` and `isBrowser` conditionals exported in `@builder.io/qwik/build`.

```tsx
import { isServer, isBrowser } from '@builder.io/qwik/build';

export const Cmp = component$(() => {
const store = useStore({
users: [],
});
useTalk$(async () => {
if (isServer) {
// If the component is mounted on the server, call DB directly.
store.users = await db.requestUsers();
}
if (isBrowser) {
// If the component is mounted on the browser, fetch users through API.
store.users = await fetchGetUser();
}
});

return (
<>
{store.users.map((user) => (
<User user={user} />
))}
</>
);
});
```

## `useClientDOM$()`

- **When:** AFTER component's first render and on tracked state changes
- **Times:** at least once
- **Platform:** browser only

Similarly to `useTask$()` or `useMount$()` this hook will also run at least once, but it will never RUN in the server, that is, it will run only in the browser.

For all components that got mounted during SSR (in the server), the `useClientEffect$()` will run eagarly, that means, without user interaction, but the eagerness can be configured!
For all components that got mounted during SSR (in the server), the `useClientDOM$()` will run eagarly, that means, without user interaction, but the eagerness can be configured!

```tsx
useClientEffect$(() => console.log('runs in the browser'), {
useClientDOM$(() => console.log('runs in the browser'), {
eagerness: 'visible', // 'load' | 'visible' | 'idle'
});
```
@@ -191,7 +176,7 @@ export const Timer = component$(() => {
const store = useStore({
count: 0,
});
useClientEffect$(() => {
useClientDOM$(() => {
// Only runs in the client
const timer = setInterval(() => {
store.count++;
@@ -204,18 +189,19 @@ export const Timer = component$(() => {
});
```

> **NOTE:** Don't abuse `useClientEffect$()` when the same logic can be achieved using `useTask$()` or other means. Ask to yourself: Does this code really need to run at the beginning in the browser? If the answer is no, `useClientEffect$()` is probably not the right answer.
> **NOTE:** Don't abuse `useClientDOM$()` when the same logic can be achieved using `useTask$()` or other means. Ask to yourself: Does this code really need to run at the beginning in the browser? If the answer is no, `useClientDOM$()` is probably not the right answer.

> How does it compare with React's `useEffect()`? Both APIs share a lot of semantics, but while both run AFTER rendering,`useClientEffect$()` can run also independently from rendering.
> How does it compare with React's `useEffect()`? Both APIs share a lot of semantics, but while both run AFTER rendering,`useClientDOM$()` can run also independently from rendering.

### When to use `useClientEffect$()`
### When to use `useClientDOM$()`

You need to run JS right at loading time of the page, even if the user never interacts with the page.

- Read the DOM after rendering
- Initialize some animations
- WebGL logic
- Read some `localStorage`
- Runs some code without user interaction
- Use DOM APIs like `localStorage`
- Runs some code without user interaction, like a setInterval()

## Diagram flow

4 changes: 2 additions & 2 deletions packages/qwik-city/runtime/src/qwik-city-component.tsx
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ import {
useEnvData,
getLocale,
useStore,
useTask$,
useWatch$,
} from '@builder.io/qwik';
import { loadRoute } from './routing';
import type {
@@ -94,7 +94,7 @@ export const QwikCityProvider = component$<QwikCityProps>(() => {
useContextProvider(RouteLocationContext, routeLocation);
useContextProvider(RouteNavigateContext, routeNavigate);

useTask$(async ({ track }) => {
useWatch$(async ({ track }) => {
const locale = getLocale('');
const { routes, menus, cacheModules, trailingSlash } = await import('@qwik-city-plan');
const path = track(() => routeNavigate.path);