diff --git a/.prettierignore b/.prettierignore
index 826c3bed..c78d4e5b 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -2,3 +2,5 @@ node_modules
dist
.npmignore
CHANGELOG.md
+examples/nextjs/.next
+examples/nextjs/next.lock
diff --git a/docs/guides/ssr-and-frameworks.md b/docs/guides/ssr-and-frameworks.md
new file mode 100644
index 00000000..8305307b
--- /dev/null
+++ b/docs/guides/ssr-and-frameworks.md
@@ -0,0 +1,73 @@
+# Frameworks and SSR
+
+The main thing to consider when using the library with Server-side Rendering (SSR) or a fullstack framework like Next.js or Remix is to make sure that
+the map is excluded from Server-side Rendering since that is not supported by the Google Maps API. We are currently evaluating a solution that would provide basic SSR capabilities via the Static Maps API.
+
+## Next.js
+
+This is how a component in a Next.js (app router) application looks like. Checkout the example [code](https://github.com/visgl/react-google-maps/tree/main/examples/nextjs) on Github or play around with the [demo](https://codesandbox.io/s/github/visgl/react-google-maps/tree/main/examples/nextjs) on Codesandbox.
+
+:::note
+
+The `use client;` statement at the top tells Next.js that
+this component should only be rendered on the client.
+
+:::
+
+```tsx
+'use client';
+
+import {APIProvider, Map} from '@vis.gl/react-google-maps';
+
+export default function MyMap() {
+ return (
+
+ );
+}
+```
+
+## Remix
+
+Here is the best approach we found to use a map component in a Remix application. Checkout the example [code](https://github.com/visgl/react-google-maps/tree/main/examples/remix) on Github or play around with the [demo](https://codesandbox.io/s/github/visgl/react-google-maps/tree/main/examples/remix) on Codesandbox.
+
+Wrap the map in a `` component from the [`remix-utils`](https://github.com/sergiodxa/remix-utils) package for it to be rendered only on the client.
+
+:::note
+
+If you use a fallback and you know the dimensions of your final map, make sure that
+the fallback has the same size to prevent layout shifts when the map component loads.
+
+:::
+
+```tsx
+import {APIProvider, Map} from '@vis.gl/react-google-maps';
+import {ClientOnly} from 'remix-utils/client-only';
+
+export default function MyMap() {
+ return (
+ }>
+ {() => (
+
+
+
+ )}
+
+ );
+}
+```
diff --git a/docs/table-of-contents.json b/docs/table-of-contents.json
index 6c2cd4b9..dcd020eb 100644
--- a/docs/table-of-contents.json
+++ b/docs/table-of-contents.json
@@ -16,6 +16,7 @@
"items": [
"guides/interacting-with-google-maps-api",
"guides/deckgl-integration",
+ "guides/ssr-and-frameworks",
"guides/writing-examples"
]
},
diff --git a/examples/nextjs/.eslintrc.json b/examples/nextjs/.eslintrc.json
new file mode 100644
index 00000000..37224185
--- /dev/null
+++ b/examples/nextjs/.eslintrc.json
@@ -0,0 +1,3 @@
+{
+ "extends": ["next/core-web-vitals", "next/typescript"]
+}
diff --git a/examples/nextjs/.example.env b/examples/nextjs/.example.env
new file mode 100644
index 00000000..57df6474
--- /dev/null
+++ b/examples/nextjs/.example.env
@@ -0,0 +1,2 @@
+# Duplicate this file and rename it to ".env" and enter your Google Maps API key here
+NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=
diff --git a/examples/nextjs/.gitignore b/examples/nextjs/.gitignore
new file mode 100644
index 00000000..28be9ded
--- /dev/null
+++ b/examples/nextjs/.gitignore
@@ -0,0 +1,51 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/versions
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# env files (can opt-in for committing if needed)
+.env*
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
+
+# Ignoring tsconfig.json here since NextJS does not provide a way yet to specify a custom tsconfig.json path
+# and rather hard codes the tsconfig.json path. We do want to use different tsconfig files
+# for this example to be able to use the local files in dev mode and the installed package
+# when called in Codesandbox.
+tsconfig.json
+
+# Ignoring the lock folder here since this is not intended
+# for a production build This should not be done on a real project.
+# https://nextjs.org/docs/app/api-reference/next-config-js/urlImports#lockfile
+next.lock
diff --git a/examples/nextjs/README.md b/examples/nextjs/README.md
new file mode 100644
index 00000000..d831f7c2
--- /dev/null
+++ b/examples/nextjs/README.md
@@ -0,0 +1,41 @@
+# Next.js Example
+
+This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). It shows a basic map setup with two routes.
+
+## Demo
+
+Checkout the [demo](https://codesandbox.io/s/github/visgl/react-google-maps/tree/main/examples/nextjs) on Codesandbox.
+
+## Google Maps Platform API Key
+
+This example does not come with an API key. Running the examples locally requires a valid API key for the Google Maps Platform.
+See [the official documentation][get-api-key] on how to create and configure your own key.
+
+The API key has to be provided via an environment variable `NEXT_PUBLIC_GOOGLE_MAPS_API_KEY`. This can be done by creating a
+file named `.env` in the example directory with the following content:
+
+```shell title=".env"
+NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=""
+```
+
+If you are on the CodeSandbox playground you can also choose to [provide the API key like this](https://codesandbox.io/docs/learn/environment/secrets)
+
+## Development
+
+First, run the development server:
+
+For the local server that uses the local library files:
+
+```bash
+npm run start-local
+```
+
+For the regular dev server that uses the installe files for `@vis.gl/react-google-maps`:
+
+```bash
+npm run start
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
+[get-api-key]: https://developers.google.com/maps/documentation/javascript/get-api-key
diff --git a/examples/nextjs/next.config.ts b/examples/nextjs/next.config.ts
new file mode 100644
index 00000000..1dc18dc7
--- /dev/null
+++ b/examples/nextjs/next.config.ts
@@ -0,0 +1,7 @@
+import type {NextConfig} from 'next';
+
+const nextConfig: NextConfig = {
+ /* config options here */
+};
+
+export default nextConfig;
diff --git a/examples/nextjs/package.json b/examples/nextjs/package.json
new file mode 100644
index 00000000..6cc6a8e2
--- /dev/null
+++ b/examples/nextjs/package.json
@@ -0,0 +1,23 @@
+{
+ "scripts": {
+ "start": "cp tsconfig.sandbox.json tsconfig.json && next dev",
+ "start-local": "cp tsconfig.local.json tsconfig.json && next dev",
+ "build": "cp tsconfig.sandbox.json tsconfig.json && next build",
+ "serve": "next start",
+ "lint": "next lint"
+ },
+ "dependencies": {
+ "@vis.gl/react-google-maps": "latest",
+ "react": "19.0.0-rc-66855b96-20241106",
+ "react-dom": "19.0.0-rc-66855b96-20241106",
+ "next": "15.0.3"
+ },
+ "devDependencies": {
+ "typescript": "^5",
+ "@types/node": "^20",
+ "@types/react": "^18",
+ "@types/react-dom": "^18",
+ "eslint": "^8",
+ "eslint-config-next": "15.0.3"
+ }
+}
diff --git a/examples/nextjs/src/app/about/page.module.css b/examples/nextjs/src/app/about/page.module.css
new file mode 100644
index 00000000..cdffddd5
--- /dev/null
+++ b/examples/nextjs/src/app/about/page.module.css
@@ -0,0 +1,8 @@
+.page {
+ width: 100%;
+ height: 100%;
+ background: aliceblue;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
diff --git a/examples/nextjs/src/app/about/page.tsx b/examples/nextjs/src/app/about/page.tsx
new file mode 100644
index 00000000..02b50f87
--- /dev/null
+++ b/examples/nextjs/src/app/about/page.tsx
@@ -0,0 +1,5 @@
+import styles from './page.module.css';
+
+export default function About() {
+ return About
;
+}
diff --git a/examples/nextjs/src/app/components/header.module.css b/examples/nextjs/src/app/components/header.module.css
new file mode 100644
index 00000000..fdd0c627
--- /dev/null
+++ b/examples/nextjs/src/app/components/header.module.css
@@ -0,0 +1,18 @@
+.container {
+ width: 100%;
+ height: 100%;
+}
+
+.header {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.nav ul {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 16px;
+ list-style: none;
+}
diff --git a/examples/nextjs/src/app/components/header.tsx b/examples/nextjs/src/app/components/header.tsx
new file mode 100644
index 00000000..c3c3fd6b
--- /dev/null
+++ b/examples/nextjs/src/app/components/header.tsx
@@ -0,0 +1,20 @@
+import Link from 'next/link';
+
+import styles from './header.module.css';
+
+export default function Header() {
+ return (
+
+ );
+}
diff --git a/examples/nextjs/src/app/globals.css b/examples/nextjs/src/app/globals.css
new file mode 100644
index 00000000..bca01b24
--- /dev/null
+++ b/examples/nextjs/src/app/globals.css
@@ -0,0 +1,11 @@
+html,
+body {
+ max-width: 100vw;
+ overflow-x: hidden;
+}
+
+* {
+ box-sizing: border-box;
+ padding: 0;
+ margin: 0;
+}
diff --git a/examples/nextjs/src/app/layout.module.css b/examples/nextjs/src/app/layout.module.css
new file mode 100644
index 00000000..2f20a005
--- /dev/null
+++ b/examples/nextjs/src/app/layout.module.css
@@ -0,0 +1,7 @@
+.container {
+ width: 100vw;
+ height: 100vh;
+ display: grid;
+ grid-template-columns: 1fr;
+ grid-template-rows: 60px 1fr;
+}
diff --git a/examples/nextjs/src/app/layout.tsx b/examples/nextjs/src/app/layout.tsx
new file mode 100644
index 00000000..bc381f59
--- /dev/null
+++ b/examples/nextjs/src/app/layout.tsx
@@ -0,0 +1,33 @@
+import type {Metadata} from 'next';
+import Script from 'next/script';
+
+import Header from './components/header';
+
+import styles from './layout.module.css';
+import './globals.css';
+
+export const metadata: Metadata = {
+ title: 'React Google Maps - NextJS Example'
+};
+
+export default function RootLayout({
+ children
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return (
+
+
+
+
+ {children}
+
+
+
+
+
+ );
+}
diff --git a/examples/nextjs/src/app/page.module.css b/examples/nextjs/src/app/page.module.css
new file mode 100644
index 00000000..cbe226cb
--- /dev/null
+++ b/examples/nextjs/src/app/page.module.css
@@ -0,0 +1,4 @@
+.container {
+ width: 100%;
+ height: 100%;
+}
diff --git a/examples/nextjs/src/app/page.tsx b/examples/nextjs/src/app/page.tsx
new file mode 100644
index 00000000..d2de69d8
--- /dev/null
+++ b/examples/nextjs/src/app/page.tsx
@@ -0,0 +1,25 @@
+'use client';
+
+import {APIProvider, Map} from '@vis.gl/react-google-maps';
+
+import styles from './page.module.css';
+
+export default function Home() {
+ const API_KEY =
+ (process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY as string) ??
+ globalThis.GOOGLE_MAPS_API_KEY;
+
+ return (
+
+ );
+}
diff --git a/examples/nextjs/src/global.d.ts b/examples/nextjs/src/global.d.ts
new file mode 100644
index 00000000..3315d985
--- /dev/null
+++ b/examples/nextjs/src/global.d.ts
@@ -0,0 +1,4 @@
+// global.d.ts
+declare module globalThis {
+ var GOOGLE_MAPS_API_KEY: string;
+}
diff --git a/examples/nextjs/tsconfig.local.json b/examples/nextjs/tsconfig.local.json
new file mode 100644
index 00000000..5a3426bb
--- /dev/null
+++ b/examples/nextjs/tsconfig.local.json
@@ -0,0 +1,35 @@
+{
+ "compilerOptions": {
+ "target": "ES2017",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./src/*"],
+ // Use local files when starting with `npm run start-local`
+ "@vis.gl/react-google-maps": ["../../src/index"]
+ }
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts",
+ "src/global.d.ts"
+ ],
+ "exclude": ["node_modules"]
+}
diff --git a/examples/nextjs/tsconfig.sandbox.json b/examples/nextjs/tsconfig.sandbox.json
new file mode 100644
index 00000000..c6b9fcc3
--- /dev/null
+++ b/examples/nextjs/tsconfig.sandbox.json
@@ -0,0 +1,33 @@
+{
+ "compilerOptions": {
+ "target": "ES2017",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts",
+ "src/global.d.ts"
+ ],
+ "exclude": ["node_modules"]
+}
diff --git a/examples/remix/.eslintrc.cjs b/examples/remix/.eslintrc.cjs
new file mode 100644
index 00000000..17d37325
--- /dev/null
+++ b/examples/remix/.eslintrc.cjs
@@ -0,0 +1,84 @@
+/**
+ * This is intended to be a basic starting point for linting in your app.
+ * It relies on recommended configs out of the box for simplicity, but you can
+ * and should modify this configuration to best suit your team's needs.
+ */
+
+/** @type {import('eslint').Linter.Config} */
+module.exports = {
+ root: true,
+ parserOptions: {
+ ecmaVersion: 'latest',
+ sourceType: 'module',
+ ecmaFeatures: {
+ jsx: true
+ }
+ },
+ env: {
+ browser: true,
+ commonjs: true,
+ es6: true
+ },
+ ignorePatterns: ['!**/.server', '!**/.client'],
+
+ // Base config
+ extends: ['eslint:recommended'],
+
+ overrides: [
+ // React
+ {
+ files: ['**/*.{js,jsx,ts,tsx}'],
+ plugins: ['react', 'jsx-a11y'],
+ extends: [
+ 'plugin:react/recommended',
+ 'plugin:react/jsx-runtime',
+ 'plugin:react-hooks/recommended',
+ 'plugin:jsx-a11y/recommended'
+ ],
+ settings: {
+ react: {
+ version: 'detect'
+ },
+ formComponents: ['Form'],
+ linkComponents: [
+ {name: 'Link', linkAttribute: 'to'},
+ {name: 'NavLink', linkAttribute: 'to'}
+ ],
+ 'import/resolver': {
+ typescript: {}
+ }
+ }
+ },
+
+ // Typescript
+ {
+ files: ['**/*.{ts,tsx}'],
+ plugins: ['@typescript-eslint', 'import'],
+ parser: '@typescript-eslint/parser',
+ settings: {
+ 'import/internal-regex': '^~/',
+ 'import/resolver': {
+ node: {
+ extensions: ['.ts', '.tsx']
+ },
+ typescript: {
+ alwaysTryTypes: true
+ }
+ }
+ },
+ extends: [
+ 'plugin:@typescript-eslint/recommended',
+ 'plugin:import/recommended',
+ 'plugin:import/typescript'
+ ]
+ },
+
+ // Node
+ {
+ files: ['.eslintrc.cjs'],
+ env: {
+ node: true
+ }
+ }
+ ]
+};
diff --git a/examples/remix/.gitignore b/examples/remix/.gitignore
new file mode 100644
index 00000000..80ec311f
--- /dev/null
+++ b/examples/remix/.gitignore
@@ -0,0 +1,5 @@
+node_modules
+
+/.cache
+/build
+.env
diff --git a/examples/remix/README.md b/examples/remix/README.md
new file mode 100644
index 00000000..100cdc26
--- /dev/null
+++ b/examples/remix/README.md
@@ -0,0 +1,41 @@
+# Remix Example
+
+This is a [Remix](https://remix.run/docs) example project. It shows a basic map setup with two routes.
+
+## Demo
+
+Checkout the [demo](https://codesandbox.io/s/github/visgl/react-google-maps/tree/main/examples/remix) on Codesandbox.
+
+## Google Maps Platform API Key
+
+This example does not come with an API key. Running the examples locally requires a valid API key for the Google Maps Platform.
+See [the official documentation][get-api-key] on how to create and configure your own key.
+
+The API key has to be provided via an environment variable `GOOGLE_MAPS_API_KEY`. This can be done by creating a
+file named `.env` in the example directory with the following content:
+
+```shell title=".env"
+GOOGLE_MAPS_API_KEY=""
+```
+
+If you are on the CodeSandbox playground you can also choose to [provide the API key like this](https://codesandbox.io/docs/learn/environment/secrets)
+
+## Development
+
+First, run the development server:
+
+For the local server that uses the local library files:
+
+```bash
+npm run start-local
+```
+
+For the regular dev server that uses the installed version files for `@vis.gl/react-google-maps`:
+
+```bash
+npm run start
+```
+
+Open [http://localhost:5371](http://localhost:5371) with your browser to see the result.
+
+[get-api-key]: https://developers.google.com/maps/documentation/javascript/get-api-key
diff --git a/examples/remix/app/components/header/header.module.css b/examples/remix/app/components/header/header.module.css
new file mode 100644
index 00000000..fdd0c627
--- /dev/null
+++ b/examples/remix/app/components/header/header.module.css
@@ -0,0 +1,18 @@
+.container {
+ width: 100%;
+ height: 100%;
+}
+
+.header {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.nav ul {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 16px;
+ list-style: none;
+}
diff --git a/examples/remix/app/components/header/header.tsx b/examples/remix/app/components/header/header.tsx
new file mode 100644
index 00000000..0e0f7555
--- /dev/null
+++ b/examples/remix/app/components/header/header.tsx
@@ -0,0 +1,20 @@
+import {Link} from '@remix-run/react';
+
+import styles from './header.module.css';
+
+export default function Header() {
+ return (
+
+ );
+}
diff --git a/examples/remix/app/components/map/map-fallback.module.css b/examples/remix/app/components/map/map-fallback.module.css
new file mode 100644
index 00000000..fadb213e
--- /dev/null
+++ b/examples/remix/app/components/map/map-fallback.module.css
@@ -0,0 +1,7 @@
+.container {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
diff --git a/examples/remix/app/components/map/map-fallback.tsx b/examples/remix/app/components/map/map-fallback.tsx
new file mode 100644
index 00000000..666cbb4d
--- /dev/null
+++ b/examples/remix/app/components/map/map-fallback.tsx
@@ -0,0 +1,5 @@
+import styles from './map-fallback.module.css';
+
+export default function MyMapFallback() {
+ return Map loading...
;
+}
diff --git a/examples/remix/app/components/map/map.client.tsx b/examples/remix/app/components/map/map.client.tsx
new file mode 100644
index 00000000..137f5756
--- /dev/null
+++ b/examples/remix/app/components/map/map.client.tsx
@@ -0,0 +1,23 @@
+import {APIProvider, Map} from '@vis.gl/react-google-maps';
+
+import styles from './map.module.css';
+
+export default function MyMap() {
+ const API_KEY =
+ (process.env.GOOGLE_MAPS_API_KEY as string) ??
+ globalThis.GOOGLE_MAPS_API_KEY;
+
+ return (
+
+ );
+}
diff --git a/examples/remix/app/components/map/map.module.css b/examples/remix/app/components/map/map.module.css
new file mode 100644
index 00000000..cbe226cb
--- /dev/null
+++ b/examples/remix/app/components/map/map.module.css
@@ -0,0 +1,4 @@
+.container {
+ width: 100%;
+ height: 100%;
+}
diff --git a/examples/remix/app/entry.client.tsx b/examples/remix/app/entry.client.tsx
new file mode 100644
index 00000000..1f8a807a
--- /dev/null
+++ b/examples/remix/app/entry.client.tsx
@@ -0,0 +1,18 @@
+/**
+ * By default, Remix will handle hydrating your app on the client for you.
+ * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
+ * For more information, see https://remix.run/file-conventions/entry.client
+ */
+
+import {RemixBrowser} from '@remix-run/react';
+import {startTransition, StrictMode} from 'react';
+import {hydrateRoot} from 'react-dom/client';
+
+startTransition(() => {
+ hydrateRoot(
+ document,
+
+
+
+ );
+});
diff --git a/examples/remix/app/entry.server.tsx b/examples/remix/app/entry.server.tsx
new file mode 100644
index 00000000..05454419
--- /dev/null
+++ b/examples/remix/app/entry.server.tsx
@@ -0,0 +1,140 @@
+/**
+ * By default, Remix will handle generating the HTTP Response for you.
+ * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
+ * For more information, see https://remix.run/file-conventions/entry.server
+ */
+
+import {PassThrough} from 'node:stream';
+
+import type {AppLoadContext, EntryContext} from '@remix-run/node';
+import {createReadableStreamFromReadable} from '@remix-run/node';
+import {RemixServer} from '@remix-run/react';
+import {isbot} from 'isbot';
+import {renderToPipeableStream} from 'react-dom/server';
+
+const ABORT_DELAY = 5_000;
+
+export default function handleRequest(
+ request: Request,
+ responseStatusCode: number,
+ responseHeaders: Headers,
+ remixContext: EntryContext,
+ // This is ignored so we can keep it in the template for visibility. Feel
+ // free to delete this parameter in your app if you're not using it!
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ loadContext: AppLoadContext
+) {
+ return isbot(request.headers.get('user-agent') || '')
+ ? handleBotRequest(
+ request,
+ responseStatusCode,
+ responseHeaders,
+ remixContext
+ )
+ : handleBrowserRequest(
+ request,
+ responseStatusCode,
+ responseHeaders,
+ remixContext
+ );
+}
+
+function handleBotRequest(
+ request: Request,
+ responseStatusCode: number,
+ responseHeaders: Headers,
+ remixContext: EntryContext
+) {
+ return new Promise((resolve, reject) => {
+ let shellRendered = false;
+ const {pipe, abort} = renderToPipeableStream(
+ ,
+ {
+ onAllReady() {
+ shellRendered = true;
+ const body = new PassThrough();
+ const stream = createReadableStreamFromReadable(body);
+
+ responseHeaders.set('Content-Type', 'text/html');
+
+ resolve(
+ new Response(stream, {
+ headers: responseHeaders,
+ status: responseStatusCode
+ })
+ );
+
+ pipe(body);
+ },
+ onShellError(error: unknown) {
+ reject(error);
+ },
+ onError(error: unknown) {
+ responseStatusCode = 500;
+ // Log streaming rendering errors from inside the shell. Don't log
+ // errors encountered during initial shell rendering since they'll
+ // reject and get logged in handleDocumentRequest.
+ if (shellRendered) {
+ console.error(error);
+ }
+ }
+ }
+ );
+
+ setTimeout(abort, ABORT_DELAY);
+ });
+}
+
+function handleBrowserRequest(
+ request: Request,
+ responseStatusCode: number,
+ responseHeaders: Headers,
+ remixContext: EntryContext
+) {
+ return new Promise((resolve, reject) => {
+ let shellRendered = false;
+ const {pipe, abort} = renderToPipeableStream(
+ ,
+ {
+ onShellReady() {
+ shellRendered = true;
+ const body = new PassThrough();
+ const stream = createReadableStreamFromReadable(body);
+
+ responseHeaders.set('Content-Type', 'text/html');
+
+ resolve(
+ new Response(stream, {
+ headers: responseHeaders,
+ status: responseStatusCode
+ })
+ );
+
+ pipe(body);
+ },
+ onShellError(error: unknown) {
+ reject(error);
+ },
+ onError(error: unknown) {
+ responseStatusCode = 500;
+ // Log streaming rendering errors from inside the shell. Don't log
+ // errors encountered during initial shell rendering since they'll
+ // reject and get logged in handleDocumentRequest.
+ if (shellRendered) {
+ console.error(error);
+ }
+ }
+ }
+ );
+
+ setTimeout(abort, ABORT_DELAY);
+ });
+}
diff --git a/examples/remix/app/global.d.ts b/examples/remix/app/global.d.ts
new file mode 100644
index 00000000..decb9c0e
--- /dev/null
+++ b/examples/remix/app/global.d.ts
@@ -0,0 +1,4 @@
+// global.d.ts
+export declare namespace globalThis {
+ const GOOGLE_MAPS_API_KEY: string;
+}
diff --git a/examples/remix/app/index.css b/examples/remix/app/index.css
new file mode 100644
index 00000000..2862ac14
--- /dev/null
+++ b/examples/remix/app/index.css
@@ -0,0 +1,21 @@
+*,
+*::after,
+*::before {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+html,
+body {
+ max-width: 100vw;
+ overflow-x: hidden;
+}
+
+.container {
+ width: 100vw;
+ height: 100vh;
+ display: grid;
+ grid-template-columns: 1fr;
+ grid-template-rows: 60px 1fr;
+}
diff --git a/examples/remix/app/public/favicon.ico b/examples/remix/app/public/favicon.ico
new file mode 100644
index 00000000..8830cf68
Binary files /dev/null and b/examples/remix/app/public/favicon.ico differ
diff --git a/examples/remix/app/root.tsx b/examples/remix/app/root.tsx
new file mode 100644
index 00000000..94e19715
--- /dev/null
+++ b/examples/remix/app/root.tsx
@@ -0,0 +1,35 @@
+import {
+ Links,
+ Meta,
+ Outlet,
+ Scripts,
+ ScrollRestoration
+} from '@remix-run/react';
+
+import './index.css';
+import Header from './components/header/header';
+
+export function Layout({children}: {children: React.ReactNode}) {
+ return (
+
+
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+ );
+}
+
+export default function App() {
+ return ;
+}
diff --git a/examples/remix/app/routes/_index.tsx b/examples/remix/app/routes/_index.tsx
new file mode 100644
index 00000000..80382f65
--- /dev/null
+++ b/examples/remix/app/routes/_index.tsx
@@ -0,0 +1,10 @@
+import {ClientOnly} from 'remix-utils/client-only';
+
+import MyMap from '../components/map/map.client';
+import MyMapFallback from '../components/map/map-fallback';
+
+export default function Index() {
+ return (
+ }>{() => }
+ );
+}
diff --git a/examples/remix/app/routes/about.tsx b/examples/remix/app/routes/about.tsx
new file mode 100644
index 00000000..fe03ee0e
--- /dev/null
+++ b/examples/remix/app/routes/about.tsx
@@ -0,0 +1,5 @@
+import styles from '../styles/about.module.css';
+
+export default function About() {
+ return About
;
+}
diff --git a/examples/remix/app/styles/about.module.css b/examples/remix/app/styles/about.module.css
new file mode 100644
index 00000000..64b2b110
--- /dev/null
+++ b/examples/remix/app/styles/about.module.css
@@ -0,0 +1,8 @@
+.container {
+ width: 100%;
+ height: 100%;
+ background: aliceblue;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
diff --git a/examples/remix/package.json b/examples/remix/package.json
new file mode 100644
index 00000000..07dd4014
--- /dev/null
+++ b/examples/remix/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "remix",
+ "private": true,
+ "sideEffects": false,
+ "type": "module",
+ "scripts": {
+ "build": "remix vite:build",
+ "start": "remix vite:dev",
+ "start-local": "remix vite:dev --config ./vite.config.local.js",
+ "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
+ "serve": "remix-serve ./build/server/index.js",
+ "typecheck": "tsc"
+ },
+ "dependencies": {
+ "@remix-run/node": "^2.14.0",
+ "@remix-run/react": "^2.14.0",
+ "@remix-run/serve": "^2.14.0",
+ "isbot": "^4.1.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "remix-utils": "^7.7.0"
+ },
+ "devDependencies": {
+ "@remix-run/dev": "^2.14.0",
+ "@types/react": "^18.2.20",
+ "@types/react-dom": "^18.2.7",
+ "@typescript-eslint/eslint-plugin": "^6.7.4",
+ "@typescript-eslint/parser": "^6.7.4",
+ "@vis.gl/react-google-maps": "latest",
+ "autoprefixer": "^10.4.19",
+ "eslint": "^8.38.0",
+ "eslint-import-resolver-typescript": "^3.6.1",
+ "eslint-plugin-import": "^2.28.1",
+ "eslint-plugin-jsx-a11y": "^6.7.1",
+ "eslint-plugin-react": "^7.33.2",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "postcss": "^8.4.38",
+ "typescript": "^5.1.6",
+ "vite": "^5.1.0",
+ "vite-tsconfig-paths": "^4.2.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+}
diff --git a/examples/remix/postcss.config.js b/examples/remix/postcss.config.js
new file mode 100644
index 00000000..8c589bbe
--- /dev/null
+++ b/examples/remix/postcss.config.js
@@ -0,0 +1,5 @@
+export default {
+ plugins: {
+ autoprefixer: {}
+ }
+};
diff --git a/examples/remix/tsconfig.json b/examples/remix/tsconfig.json
new file mode 100644
index 00000000..9d87dd37
--- /dev/null
+++ b/examples/remix/tsconfig.json
@@ -0,0 +1,32 @@
+{
+ "include": [
+ "**/*.ts",
+ "**/*.tsx",
+ "**/.server/**/*.ts",
+ "**/.server/**/*.tsx",
+ "**/.client/**/*.ts",
+ "**/.client/**/*.tsx"
+ ],
+ "compilerOptions": {
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
+ "types": ["@remix-run/node", "vite/client"],
+ "isolatedModules": true,
+ "esModuleInterop": true,
+ "jsx": "react-jsx",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "resolveJsonModule": true,
+ "target": "ES2022",
+ "strict": true,
+ "allowJs": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "baseUrl": ".",
+ "paths": {
+ "~/*": ["./app/*"]
+ },
+
+ // Vite takes care of building everything, not tsc.
+ "noEmit": true
+ }
+}
diff --git a/examples/remix/vite.config.local.ts b/examples/remix/vite.config.local.ts
new file mode 100644
index 00000000..c0259d8f
--- /dev/null
+++ b/examples/remix/vite.config.local.ts
@@ -0,0 +1,37 @@
+import {vitePlugin as remix} from '@remix-run/dev';
+import {defineConfig, loadEnv} from 'vite';
+import tsconfigPaths from 'vite-tsconfig-paths';
+import {resolve} from 'node:path';
+
+declare module '@remix-run/node' {
+ interface Future {
+ v3_singleFetch: true;
+ }
+}
+
+export default defineConfig(({mode}) => {
+ const {GOOGLE_MAPS_API_KEY = ''} = loadEnv(mode, process.cwd(), '');
+
+ return {
+ plugins: [
+ remix({
+ future: {
+ v3_fetcherPersist: true,
+ v3_relativeSplatPath: true,
+ v3_throwAbortReason: true,
+ v3_singleFetch: true,
+ v3_lazyRouteDiscovery: true
+ }
+ }),
+ tsconfigPaths()
+ ],
+ define: {
+ 'process.env.GOOGLE_MAPS_API_KEY': JSON.stringify(GOOGLE_MAPS_API_KEY)
+ },
+ resolve: {
+ alias: {
+ '@vis.gl/react-google-maps': resolve('../../src/index.ts')
+ }
+ }
+ };
+});
diff --git a/examples/remix/vite.config.ts b/examples/remix/vite.config.ts
new file mode 100644
index 00000000..45b55198
--- /dev/null
+++ b/examples/remix/vite.config.ts
@@ -0,0 +1,24 @@
+import {vitePlugin as remix} from '@remix-run/dev';
+import {defineConfig} from 'vite';
+import tsconfigPaths from 'vite-tsconfig-paths';
+
+declare module '@remix-run/node' {
+ interface Future {
+ v3_singleFetch: true;
+ }
+}
+
+export default defineConfig({
+ plugins: [
+ remix({
+ future: {
+ v3_fetcherPersist: true,
+ v3_relativeSplatPath: true,
+ v3_throwAbortReason: true,
+ v3_singleFetch: true,
+ v3_lazyRouteDiscovery: true
+ }
+ }),
+ tsconfigPaths()
+ ]
+});