skip to content

Several dashboard panels each call useWindowSize(); what does wrapping it in VueUse's createSharedComposable change on the client, and what happens during SSR?

level: seniorimportance: nice to knowfreq 16%

answer

  1. one instance, many callers
  2. a detached effect scope
  3. counting subscribers
  4. first caller's arguments win
  5. no sharing on the server

basics

~20 s

On the client, the first call runs useWindowSize in a detached scope and later calls reuse its refs, so one set of listeners serves all panels until the last caller is disposed. During SSR every call gets a fresh, unshared instance.

solid answer

~40 s

`createSharedComposable(useWindowSize)` returns a wrapper. On the client the first call creates a detached `effectScope(true)`, runs `useWindowSize` inside it and caches the result; every later call returns the same `width` and `height` refs and increments a subscriber count. Each caller registers a disposal hook on its own scope, and when the count reaches zero the shared scope is stopped — removing the `resize` and orientation listeners — and the cache is cleared, so the next caller starts fresh. Arguments are used only on the first call; later callers' options are ignored. During server rendering the wrapper is the original composable, so each call creates independent state and nothing leaks between requests. Contrast `createGlobalState`, which is never disposed and does not fall back on the server.

code

ts · 12 lines
ts
// composables/useSharedWindowSize.ts
import { createSharedComposable, useWindowSize } from '@vueuse/core'

// one resize listener for every panel on the client;
// a fresh, unshared instance per call during SSR
export const useSharedWindowSize = createSharedComposable(() =>
  useWindowSize({ initialWidth: 1280, initialHeight: 800 }),
)

// in each panel's <script setup>:
// const { width } = useSharedWindowSize()
// const compact = computed(() => width.value < 900)

go deeper

for a junior

Recall that createSharedComposable makes many components share one instance of a composable instead of each creating its own listeners.

for a middle

Explain the lifecycle: a detached scope created on first use, a subscriber count, and disposal when the last caller's scope is disposed.

for a senior

Show you know the edges: first-call arguments win, the server gets unshared instances, and createGlobalState never disposes.

for a principal

Decide what deserves sharing: global browser listeners do, per-component state does not, and durable app state belongs in a store.

## The problem: many panels, many listeners In the analytics dashboard, a chart panel, a table panel and a sidebar each need the window size to choose a layout. Each calls `useWindowSize()`, and each call registers its own `resize` listener, its own orientation media query and its own pair of refs. That works, but a dashboard with a dozen panels runs a dozen identical handlers on every resize. `createSharedComposable` turns a composable into a **shared, reference-counted** instance. ## How createSharedComposable works `const useSharedWindowSize = createSharedComposable(useWindowSize)` returns a wrapper function. On the client: 1. The **first** call creates a detached effect scope with `effectScope(true)`, runs `useWindowSize(...args)` inside it and caches the returned object. 2. **Every** call, including the first, increments a subscriber counter and registers a dispose hook on the **caller's** own effect scope, typically the calling component. 3. **Later** calls skip creation and return the cached object, so all panels read the same `width` and `height` refs, backed by one set of listeners. 4. When a caller's scope is disposed — the panel unmounts — the counter is decremented. 5. When it reaches **zero**, the shared scope is stopped, which removes the listeners, and the cache is cleared. The next caller builds a fresh instance. Because the scope is **detached**, the shared state does not die with the first component that happened to create it; it lives exactly as long as someone is using it. ## Arguments: the first caller wins The wrapper passes arguments only on the first call. If the chart panel calls `useSharedWindowSize({ type: 'outer' })` first and the table panel later calls `useSharedWindowSize({ includeScrollbar: false })`, the table silently gets the chart's configuration. Share composables that take **no arguments**, or create one shared wrapper per configuration. ## Server rendering On the server, `createSharedComposable` returns the **original** composable: every call creates its own state. That is deliberate — a module-level shared instance on a server would be shared between all requests, leaking one user's state into another's response. Two consequences for the dashboard: - nothing is shared during server rendering, which is harmless because there are no listeners to save; - `useWindowSize` itself reports `Infinity` for width and height on the server unless `initialWidth` and `initialHeight` are set, so layout decisions made from it on the server should use those options or wait for the client. ## createSharedComposable vs createGlobalState | | `createSharedComposable` | `createGlobalState` | |---|---|---| | Created | on first call | on first call | | Disposed | when the last caller's scope is disposed | never | | Server behaviour | falls back to a fresh instance per call | one instance for the whole process | | Arguments | first caller's used | first caller's used | | Fits | shared browser listeners and sensors | app-wide state that must survive unmounts | State that must outlive every component, or be shared safely across a server-rendered app, belongs in a store such as Pinia rather than either helper. ## When to use it - **Use it** for composables that attach global listeners or observers and return read-only views of browser state: window size, mouse position, network status, media queries. - **Avoid it** for composables whose state is meant to be per component, such as a form's local values or a per-panel fetch. - **Check for side effects in arguments**: if different callers need different options, sharing changes behaviour silently. ## Debugging a shared composable - **A panel sees options it never passed.** Another caller reached the wrapper first; its arguments configured the shared instance. - **Listeners survive after every panel is gone.** A caller ran outside any effect scope — for example at module level or in a plain callback — so no dispose hook was registered and the subscriber count never returns to zero. - **State resets unexpectedly.** Every subscriber unmounted at once, for instance during a route change, so the instance was disposed and the next caller built a new one; that is by design. - **The server-rendered layout is wrong.** `useWindowSize` reports its initial sizes on the server, `Infinity` by default, and the client then measures the real window; supply realistic `initialWidth` and `initialHeight` or defer size-dependent layout until mount.

  • Why does VueUse's createSharedComposable run the composable in a detached effect scope?
    If it ran inside the first caller's scope, the shared listeners would be torn down when that component unmounted, even while other components still used the result. A detached scope ties the lifetime to the subscriber count instead.
  • Why does createSharedComposable fall back to the plain composable on the server?
    Module-level shared state on a server lives for the whole process and is seen by every request. Returning the original composable gives each call fresh state, so one user's data cannot appear in another user's rendered page.

A shared composable is like the one weather station on an office roof: the first person who needs it switches it on, everyone reads the same display, and the last person to leave switches it off. Nobody installs their own station, and someone arriving later cannot reconfigure it.

saying these in an interview costs you the question

  • Each caller of a shared composable gets its own listeners and refs.
  • Later callers can pass different options to a shared composable instance.
  • The shared state is destroyed as soon as the first caller unmounts.
  • createSharedComposable shares one instance between server requests too.
  • createSharedComposable and createGlobalState both dispose when unused.