# Design `onWatch` v1
Design API which can be used to watch for changes in the inputs and then cause an effect.
## Constraints
- Qwik does not execute the inputs on each rendering, instead Qwik relies on subscriptions.
- Execution of the update function needs to be lazy loaded
## Proposal
```typescript=
/**
* Provides ability to re-run a function when watched inputs change.
*
* `onWatch` allows for execution of a function which has a side effect.
* The function is re-executed every time one of the guarded inputs
* change. (See `WatchGuard`.) It is expected that the execution of
* the `effectFn` has a side-effect.
*
* The `efectFn` is stored as QRL and therefore is lazy loaded.
*
* Use the `WatchGuard` to create subscriptions on data which are
* considered as inputs to the `effectFn`. Whenever these properties
* change the `effectFn` is re-run.
*
* @param effectFn: QRL pointing to a function which will re-execute.
*/
export function onWatch(watcher: QRL<(obs: Observer) => void>);
export function onWatch$(watcher: (obs: Observer) => void);
/**
* Wraps an object in a read proxy which creates a subscription on
* that property.
*
* Any subsequent changes to the object will trigger the `useEffect`
* function to rerun.
*/
export type WatchGuard<T extends {}> = ();
```
```typescript=
import {component$, $} from '@builder.io/qwik';
const MyComponent = component$(() => {
const store = useStore(); // assume id: STORE123
onWatch$((obs) => {
// This will create a subscription on `store.city` property
// any time the property changes this function will re-run.
const city = obs(store).city;
store.temp = await fetch(`http://weather/${city}`);
}));
return onRender(() => <span>{store.city} / {store.temp}</span>);
});
```
The above will encode like so:
```htmlembedded=
<div on:q-render="..."
on:q-watch="chunk@effectSymbol|STORE123.city[*STORE123]">
</div>
```
## Open Questions
- When should the API run
- Before / after `on:qRender`?
- In React `useEffect` runs after `render` so that `useEffect` can use `createRef`
- Should we somehow coalesce events?
- When a set of subscriptions change, how long before the `onWatch` runs?
- currently `onRender` runs after `rAF`.
- `onWatch` can return `Promise` which means it can delay other things?
*Debate*:
- `onWatch` should happen after rendering so that it can read layout information.
- do we want `useWatch` which runs before and after `onRender`?
- Should `onWatch` be synchronous? This makes sense. If you want to execute async operations and then cancel it.
-
*Insights*:
- if `onWatch` runs after `onRender` this means that it also runs through `rAF`
NOTE:
- If it is important that UI updates are applied atomically then we have to render to JSX buffer first and then apply to UI
*Conclusions*:
- `onWatch` must:
- return synchronously.
- Framework makes best effort to run in front of render, but not guaranteed.
- (`onWatch` function may need to be loaded in which case `rAF` of `onRender` will get in front of it.)
- May return cancel fn `()=>void`, which will run before next invocation.
- Runs on next microtask after detecting input change.
- this implies before onRender because `onRender` runs `rAF`
- We may need `onLayout()` (to run after `onRender` which allows reading DOM)
---
```typescript=
class {
componentWillRender() {
// Funcionality A
// Funcionality B
}
componentDidRender() {
// Funcionality A
// Funcionality B
// Funcionality C
}
}
const useMyThing = () {
onBeforeRender(()=>{
// do some stuff
}
}
```
```typescript=
function FetchTemp() {
return {
cancel() {
...
},
fetch(city: string, callback: Function) {
...
}
};
}
cost MyComp = component$(() => {
const store = useStore({city: '', temp: 0});
const ref = useRef();
onWatch$((obs) => {
const city = obs(store).city;
const {res, cancel} = fetchWithCancel('http...city');
});
onWatch$((obs) => {
if (obs(ref).current) {
console.log('runs after render?')
}
})
onDidRender$(()=> {
console.log('read layout here.');
});
return onRender$(() => {
return <span>{store.count}</span>
});
});
```
---
# Manu
```typescript=
interface MyAppProps {
city: string;
}
interface State {
state: 'initial' | 'loading' | 'done';
name?: string;
weather?: string;
temp?: string;
}
export const MyApp = qComponent((props: MyAppProps) => {
const store = useStore<State>({state: 'initial'});
onWatch$(async (obs) => {
store.state = 'loading';
const res = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${obs(props).city}&appid=0d590602bfaa37f01d8bec9322b4df72&units=metric`,
{
method: 'GET',
headers: {},
}
);
const json = await res.json();
Object.assign(store, {
state: 'done',
...json,
});
}));
return onRender(() => {
const ready = markReady(store.state === 'done');
return (
<div>
{ready ? (
<>
<h1>City: {store.name}</h1>
<h2>Weather: {store.weather}</h2>
<h2>Temp: {store.temp}C</h2>
</>
) : (
<span>LOADING</span>
)}
</div>
);
});
});
```
```typescript=
export const MyApp = qComponent(async (props: MyAppProps) => {
console.log('Fetching weather for', props.city);
const res = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${props.city}&appid=0d590602bfaa37f01d8bec9322b4df72&units=metric`,
{
method: 'GET',
headers: {},
}
);
const json = await res.json();
const state = {
name: json.name,
weather: json.weather[0].description,
temp: json.main.temp,
};
return onRender(() => {
return (
<div>
<h1>Prop: {props.city}</h1>
<h1>City: {state.name}</h1>
<h2>Weather: {state.weather}</h2>
<h2>Temp: {state.temp}C</h2>
</div>
);
});
});
```
# FAQ
- Why can't we just supply args to watch?
```typescript=
export const MyComp = component$(() => {
const store = useStore({a: 0, b:0});
const a = store.a;
const x = onWatch$(() => a + store.b, [a, store.b]); // This is re-executed
return onRender$(() => <span>{x}</span>)
})
```
- Why do we need to have `WatchGuard`?
```typescript=
import {$} from '@builder.io/qwik';
export const MyComp = component$(() => {
const store = useStore({a: 0, b:0});
const a = store.a;
const x = useEffect$(() => a + store.b); // This is re-executed
useMyStuff$(() => console.log(''))
return onRender$(() => <button on$:click={() => console.log('click')}>{x}</button>)
})
function useMyStuff$(fn:QRL<() =>void>) {
useEffectFromQrl(fn)
}
export const MyComp = componentFromQrl($(() => {
const store = useStore({a: 0, b:0});
const a = store.a;
const x = useEffectFromQRl($(() => a + store.b)); // This is re-executed
return onRenderFromQrl($(() => <button on:click={$(() => console.log('click'))}>{x}</button>))
}))
onWatch -----> onRender
onRender --/--> onWatch
const a = onWatch(watcher => {
return 12;
})
function useEffect$(fn) {
onWatch(fn);
}
useWatch(w => {
})
useObserve(observer => {
})
useObserve(obs => {
})
```
```typescript=
export const MyComp = component$(() => {
const store = useStore({a: 0, b:0});
const a = store.a;
onWatch$((_) => _(a) + _(store).b); // This is re-executed
return onRender$(() => <span>{x}</span>)
})
```
```typescript=
export const MyComp = component$(({a}) => {
const store = useStore({a: 0, b:0});
//const a = store.a;
const x = useEffect$((obs) => {
const {c} = obs(store);
return store.a + store.b + c;
}); // This is re-executed
return onRender$(() => <span>{x}</span>)
})
```
- How to handle async loading?
---
```typescript=
export default function useDebounce(obj, key, delay) {
// State and setters for debounced value
const [debouncedValue, setDebouncedValue] = useState(value);
obj[key]
useEffect(() => {
// Set debouncedValue to value (passed in) after the specified delay
const handler = setInterval(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
```
1. Call: useEffect() + unreg()
2. Call: unreg(), useEffect() + unreg()
const [code, setCode] = useState();
const debounced = useDebounce(code, 500);
useEffect(()=>{
// Compilation
compile(debounced);
}, [debounced])
```htmlembedded=
<div q:base="/build">
<button on:click="foo.js">/build/foo.js</button>
<button on:click="/foo.js">/foo.js</button>
</div>
<div q:base="http://server/build">
<button on:click="foo.js">/build/foo.js</button>
<button on:click="/foo.js">http://server/foo.js</button>
</div>
```