react-google-maps icon indicating copy to clipboard operation
react-google-maps copied to clipboard

[Feat] Add SSR support or safe usage of AdvancedMarker and DOM-dependent components

Open shani-ti opened this issue 5 months ago • 5 comments

Target Use Case

We are using @vis.gl/react-google-maps in a project that renders pages using server-side rendering, and then hydrates them on the client.

However, the current package assumes a browser environment by default — for example, components like AdvancedMarker use ReactDOM.createPortal and interact with the DOM, which causes SSR crashes when the page is pre-rendered on the server.

We are currently forced to wrap these components in client-only guards like:

if (typeof window !== 'undefined') {
  return <AdvancedMarker ... />;
}
const [isClient, setIsClient] = useState(false);

useEffect(() => {
  setIsClient(true);
}, []);

return isClient ? <AdvancedMarker ... /> : null;

It would be helpful if the library could offer a built-in way to make components like AdvancedMarker SSR-safe or optionally lazy-loadable so that SSR apps don’t crash or require workarounds.

Adding SSR support (or safe lazy loading) would benefit anyone using React frameworks like Next.js, Remix, or custom SSR setups that render Google Maps on the server and hydrate them client-side.

This is especially important for:

  • SEO-sensitive apps
  • Performance-focused web apps
  • Sites with server-rendered landing pages that contain maps

It would also reduce the need for wrapping AdvancedMarker and other DOM-dependent components in custom client-only guards, simplifying code and avoiding errors during hydration.

Proposal

  1. Provide a ssr: false or clientOnly: true prop on components like AdvancedMarker or APIProvider that tells them to skip rendering during SSR.
  2. Internally guard browser-only operations in DOM-based components like AdvancedMarker by checking typeof window !== 'undefined' before calling ReactDOM.createPortal.
  3. Document a recommended pattern or wrapper component for SSR users, such as a ClientOnly wrapper like:
function ClientOnly({ children }) {
  const [isClient, setIsClient] = useState(false);
  useEffect(() => setIsClient(true), []);
  return isClient ? children : null;
}
  1. Optionally provide a utility from the package itself:
import { ClientOnly } from '@vis.gl/react-google-maps';
// or
<AdvancedMarker ssr={false} ... />

Even a small guard inside AdvancedMarker (as well as MapControl) would prevent runtime crashes and improve compatibility across frameworks.

shani-ti avatar Aug 07 '25 09:08 shani-ti

We definitely want to make sure people using SSR frameworks are having a good experience using this library.

The main problem with this is that there is no standard way to make a client-side component "just work" together with all libraries and frameworks. For example, both next.js and remix/react-router have very different approaches to this. See here if you haven't found that already: https://visgl.github.io/react-google-maps/docs/guides/ssr-and-frameworks.

So, for these components to work properly and be bundled into the correct places, it is currently necessary to implement your own wrappers for the framework you're using. And I think it's actually a good thing to make it very obvious that this library doesn't work in an SSR environment.

It would be interesting to see how other 'browser only' libraries in the React ecosystem handle this, please let me know if you find something comparable.

One thing we might want to do in the future is to provide separate exports for the most popular frameworks (e.g. import {Map} from '@vis.gl/react-google-maps/nextjs) that include some of those framework-specific features. I would very much like to find someone who knows enough about next.js/react-router/... to help maintain that.

usefulthink avatar Aug 07 '25 10:08 usefulthink

Basically, the SSR issue is related to the use of the createPortal method inside AdvancedMarker.

It’s quite strange that APIProvider brings code related to AdvancedMarker into the build output — including AdvancedMarkerContext and the createPortal usage — even when AdvancedMarker is not used anywhere in the source code.

For example, I built a very basic app that includes just APIProvider and Map, but in the final build output got also AdvancedMarkerContext and import & usage of createPortal.

source: https://gitlab.com/NatalyIvanova/maps/-/blob/main/src/Map.js output: https://gitlab.com/NatalyIvanova/maps/-/blob/main/dist_to_save/withNoMarker.js?ref_type=heads#L11610

nataliai-picasso avatar Aug 07 '25 12:08 nataliai-picasso

That the code for the markers is there isn't that weird if you ask me. Your bundler is pulling in the file ./dist/index.modern.mjs from our package, and that includes all of our components and hooks. That in itself shouldn't cause any issues or does it?

usefulthink avatar Aug 07 '25 13:08 usefulthink

It won't solve the SSR issue, because createContext also only works on the client side. From a library architecture perspective, it's better if the consumer receives only the components they explicitly imported, even though all components are exposed.

nataliai-picasso avatar Aug 07 '25 19:08 nataliai-picasso

I believe it is common practice for libraries to provide functionality in a bundled form. At least I can't name any project at the moment that handles this differently. Also, as long as React.createContext isn't being called, it shouldn't affect you in any way. Bundlers will usually remove unused code from the bundle in the minification stage.

But I'm always open to suggestions for how we can improve our library...

usefulthink avatar Aug 08 '25 17:08 usefulthink