Work

/

About
Play
Blog
Contact
Home/Blog/Engineering
Engineering
•
Updated Oct 9, 2026
•
4 min read

Custom React Hooks That Survive SSR and Cleanup

Custom React hooks diagram showing reusable subscriptions and cleanup
MS
Muhammad Saad

Shopify engineer. I build storefronts, two published Shopify apps, and the infrastructure behind a 70,000+ product store, and I write here about what that work teaches me.

Follow on LinkedInSee my workBook a call
Found this useful? Share it.

Keep reading

Building an Embeddable React Widget That Works on Any Site
Engineering
•
4 min read

Building an Embeddable React Widget That Works on Any Site

Build an embeddable React widget with a script-tag loader, isolated styles, lazy-loaded UI and explicit host-page communication and cleanup.

Jun 24, 2026
Diagram of chat state, a streamed response and a separate action boundary
Engineering
•
4 min read

React AI Chat: Streaming State, Cancellation and Safe Actions

Build a React chat response state machine with request IDs, UTF-8 stream decoding, cancellation and a separate authorization boundary for store actions.

Jun 20, 2026
React performance workflow: reproduce, measure and verify a catalog interaction
Engineering
•
5 min read

React Performance: Diagnose Slow Catalog Interactions

Find whether a slow React catalog interaction comes from requests, rendering or layout, then choose a focused fix and verify it with a repeatable trace.

Jan 10, 2026
Back to the blog

© 2026 Muhammad Saad • Colophon

Connect with me on LinkedIn

Elsewhere

  • Github
  • Testimonials
  • CV
  • LinkedIn

Contact

  • Contact details
  • Book a call
  • Email
  • Privacy policy
  • Editorial policy
On this page
  1. 1. Decide whether the component needs a hook
  2. 2. Separate the server snapshot from browser state
  3. 3. Test changes after the first render
  4. 4. Give observers the same ownership discipline
  5. 5. Review the contract before reusing it

A custom React hook can work in a browser demo and still fail when the page is rendered on a server. Media queries make that mistake easy: reading window in a state initializer assumes the browser exists before React starts rendering. I want the hook's environment and cleanup rules to be part of its API.

Quick answer

Treat browser subscriptions as external state, provide a consistent server snapshot, and remove every listener when its subscription ends. Use CSS for visual breakpoints where possible. A hook should own a specific behavior, including changes to its inputs, instead of merely hiding an effect inside another function.

1. Decide whether the component needs a hook

I split SwiftSearch into a 4.5 KB gzipped core and an 18 KB interface loaded on search intent. The SwiftSearch case study documents that boundary; it does not document the hook below as production code. The lesson I carry into React examples is to give each piece of behavior a clear reason to exist.

A media-query hook makes sense when behavior changes, such as whether an optional animation should run. It is usually unnecessary when the only requirement is a narrower grid or smaller margin. I would implement those differences in CSS, keeping the same useful content available before JavaScript starts.

Write down the hook's consumer before its implementation. What value does the component need, how can that value change, and who owns the event source? If the answer is a calculation from existing props, an ordinary function may be enough. Adding a subscription would create another moving part without solving a state problem.

2. Separate the server snapshot from browser state

React's useSyncExternalStore reference defines three responsibilities: subscribe to changes, read the current snapshot, and provide the server snapshot when rendering on the server. That last value must also match the initial hydration snapshot. It is not a claim that the server knows the visitor's viewport.

The following module targets React 18 or later and modern browsers. It returns false on the server, then reflects the browser query after hydration. Call it at the top level of a component or another hook; do not call it conditionally inside a click handler.

useMediaQuery.js — browser subscription with an SSR fallback
import { useCallback, useSyncExternalStore } from 'react';

const getServerSnapshot = () => false;

export function useMediaQuery(query) {
  const subscribe = useCallback((notify) => {
    const media = window.matchMedia(query);
    media.addEventListener('change', notify);
    return () => media.removeEventListener('change', notify);
  }, [query]);

  const getSnapshot = useCallback(
    () => window.matchMedia(query).matches,
    [query],
  );

  return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}

The snapshot is a boolean, so unchanged reads do not manufacture a new object identity. The subscription function stays stable while the query stays stable. When the query changes, its dependency changes and React can replace the old subscription with the new one.

Choose the fallback deliberately. If false means “hide the only navigation,” the component design needs correction. A safe initial value should preserve a usable page, and essential layout should not depend on discovering the viewport through JavaScript.

3. Test changes after the first render

MDN's matchMedia documentation describes the initial match value and the change event. A useful test exercises both. Mounting a component once and checking its first label does not show whether resizing or a new query works.

For this hook, I would test a narrow viewport, resize across the breakpoint, change the query prop, and unmount. I would also server-render it with no browser globals available. Those checks address the actual failure modes rather than asserting that a particular React function was called.

Hook behavior to verify
SituationExpected resultFailure to investigate
Server renderConsistent fallback without browser accessReading window during rendering
First hydrated renderCompatible initial markupDifferent server and client assumptions
Breakpoint crossedConsumer reflects the new matchMissing change subscription
Query prop replacedNew query controls the resultStale callback dependencies
Component unmountedOld listener is removedCleanup bound to a different listener

Keep these checks attached to a real consumer. For example, a reduced-motion preference should change the optional animation behavior while preserving the product information. The hook returning a correct boolean is only part of that outcome.

4. Give observers the same ownership discipline

An intersection observer introduces another input: the actual DOM node being observed. A ref object can stay the same while its current node changes. If an element appears only after loading, an effect that ran before the element existed needs a way to observe the new node.

I would make that node change explicit with a callback ref or a component boundary that mounts with the element. Then the effect can disconnect the previous observer and attach to the current element. React's effect lifecycle guidance explains why setup and cleanup must remain symmetrical as dependencies change.

Avoid bundling visibility, analytics and network loading into a hook named only useInView. A consumer may want one of those behaviors without the others. Return the visibility information, then make the decision to load or record an event at a boundary where its purpose is clear.

The embeddable widget guide covers the wider installation boundary. The search widget loading walkthrough deals with preserving typed input while an optional interface arrives. Neither problem is solved merely by introducing a reusable hook.

5. Review the contract before reusing it

Before copying a hook into another project, I would write down its assumptions: supported browser APIs, server fallback, input identity, listener lifetime and behavior after unmount. That short contract is more useful than calling it universally reusable.

For this example, start by integrating useMediaQuery into one optional behavior and running the test sequence above. Keep a CSS-only version in mind as the simpler alternative when the requirement is purely visual. For slow catalog interactions rather than subscriptions, continue with the React performance diagnosis guide.