# Subscriptions from scratch Say you are building a video player. You want different controls when the video goes fullscreen. The browser fires a [`fullscreenchange`](https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API/Guide) event, but did it just enter or leave fullscreen? The event does not say. You have to read `document.fullscreenElement`. You also have to remove the listener when the app no longer needs fullscreen updates. off_topic helps you manage this lifecycle. It asks your `subscriptions` function which listeners should be active, starts new ones, and cleans up those you stop returning, all while sending their messages through `update` as usual. The built-in `window_size` subscription works this way too: it reads the current value, listens for changes, and cleans up afterward. Let's write a fullscreen subscription with `from`. ## Start with the browser API We need one JavaScript function that reports the current state, attaches a listener, and returns a function to remove it. Gleam declares that function as an external: ```gleam // src/my_app/browser.gleam @external(javascript, "./browser.mjs", "onFullscreenChange") pub fn on_fullscreen_change(report: fn(Bool) -> Nil) -> fn() -> Nil ``` Here is the JavaScript next to it: ```javascript // src/my_app/browser.mjs export function onFullscreenChange(report) { const send = () => report(document.fullscreenElement !== null); document.addEventListener("fullscreenchange", send); send(); return () => document.removeEventListener("fullscreenchange", send); } ``` The call to `send()` is easy to overlook. If this subscription starts while an element is already fullscreen, waiting for the next event would leave the model with the wrong value. Many useful subscriptions follow this pattern: send the current value first, then keep it up to date. ## Let off_topic manage its lifetime `from` takes a list of dependencies and a setup callback, which then can register any listeners and must return a cleanup function. Our JavaScript function already has exactly that shape: ```gleam // src/my_app/subscriptions.gleam import my_app/browser import off_topic.{type Subscription} pub fn fullscreen(send: fn(Bool) -> message) -> Subscription(message) { use dispatch <- off_topic.from(watching: [off_topic.dep("my-app/fullscreen")]) browser.on_fullscreen_change(fn(active) { dispatch(send(active)) }) } ``` Return `fullscreen(FullscreenChanged)` from your app's `subscriptions` callback to then subscribe to the current fullscreen state! When off_topic starts it, the callback calls the `on_fullscreen_change` FFI function, and the function it returns becomes the cleanup for the subscription. ## When does it restart? After an update, off_topic compares the new subscriptions with the ones already running. By default, it matches them structurally, comparing them by position in their batch, before comparing their dependencies. If they all turn out equal, the dependency keeps running, while different ones cause off_topic to run cleanup and setup again. We pass `"my-app/fullscreen"` as an identifier dependency in this subscription. That matters because off_topic does not compare the setup functions themselves, since they might close over other data as well. If a different custom subscription takes this position with the same dependency list, off_topic would still the old listener, so having a dependency that is for sure different solves this problem. Typically, you add a dependency for any input that should reopen the listener. A subscription to a user-specific event stream, for example, needs the user ID: ```gleam watching: [ off_topic.dep("my-app/user-events"), off_topic.dep(user_id), ] ``` A changed user ID then closes the old stream and opens the new one. The `watching` helper adds dependencies to an existing subscription when you need the same behavior with a built-in. ## When the root element matters `from` is enough for browser APIs on `document` or `window`. If setup needs the Lustre root element, use `element`. Its callback receives both `dispatch` and the root as a `Dynamic` value. It runs before paint, when the root is in the DOM. The same cleanup rule applies: a `ResizeObserver`, for example, should return a function that calls `disconnect()`. The built-in `element_size` subscription shows this pattern. The fullscreen example runs in a browser app. Server components need a browser-side handler registered with `remote`; see [Server components](./server-components.html).