# 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> ```