@jwiedeman/gtm-kit-react 1.1.6 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -6
- package/dist/index.cjs +234 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +283 -2
- package/dist/index.d.ts +283 -2
- package/dist/index.js +224 -4
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -124,16 +124,65 @@ const client = useGtmClient();
|
|
|
124
124
|
|
|
125
125
|
### `useGtmReady()`
|
|
126
126
|
|
|
127
|
-
Get a function that resolves when GTM is fully loaded.
|
|
127
|
+
Get a function that resolves when GTM is fully loaded. Best for awaiting in event handlers.
|
|
128
128
|
|
|
129
129
|
```tsx
|
|
130
130
|
const whenReady = useGtmReady();
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
whenReady()
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
132
|
+
const handleClick = async () => {
|
|
133
|
+
await whenReady();
|
|
134
|
+
console.log('GTM is ready!');
|
|
135
|
+
};
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### `useIsGtmReady()`
|
|
139
|
+
|
|
140
|
+
Get a function for synchronous ready checks. Best for conditionals in callbacks.
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
const isReady = useIsGtmReady();
|
|
144
|
+
|
|
145
|
+
const handleClick = () => {
|
|
146
|
+
if (isReady()) {
|
|
147
|
+
// GTM is loaded, safe to use advanced features
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### `useGtmInitialized()`
|
|
153
|
+
|
|
154
|
+
Get a reactive boolean that updates when GTM initializes. Best for conditional rendering.
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
const isInitialized = useGtmInitialized();
|
|
158
|
+
|
|
159
|
+
return isInitialized ? <Analytics /> : <Skeleton />;
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### `useGtmError()`
|
|
163
|
+
|
|
164
|
+
Get error state from the GTM provider.
|
|
165
|
+
|
|
166
|
+
```tsx
|
|
167
|
+
const { hasError, errorMessage } = useGtmError();
|
|
168
|
+
|
|
169
|
+
if (hasError) {
|
|
170
|
+
console.error('GTM failed:', errorMessage);
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### `useIsGtmProviderPresent()`
|
|
175
|
+
|
|
176
|
+
Check if GtmProvider exists without throwing. Useful for optional GTM integration.
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
const hasProvider = useIsGtmProviderPresent();
|
|
180
|
+
|
|
181
|
+
// Safe to call - won't throw if provider is missing
|
|
182
|
+
if (hasProvider) {
|
|
183
|
+
const push = useGtmPush();
|
|
184
|
+
push({ event: 'optional_tracking' });
|
|
185
|
+
}
|
|
137
186
|
```
|
|
138
187
|
|
|
139
188
|
---
|
package/dist/index.cjs
CHANGED
|
@@ -4,13 +4,240 @@ var react = require('react');
|
|
|
4
4
|
var gtmKit = require('@jwiedeman/gtm-kit');
|
|
5
5
|
var jsxRuntime = require('react/jsx-runtime');
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
// src/provider.tsx
|
|
8
|
+
var isSsr = () => typeof window === "undefined";
|
|
9
|
+
var useHydrated = () => {
|
|
10
|
+
return react.useSyncExternalStore(
|
|
11
|
+
// Subscribe function (no-op, state never changes after hydration)
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
13
|
+
() => () => {
|
|
14
|
+
},
|
|
15
|
+
// getSnapshot (client): always true after first render
|
|
16
|
+
() => true,
|
|
17
|
+
// getServerSnapshot (server): always false
|
|
18
|
+
() => false
|
|
19
|
+
);
|
|
20
|
+
};
|
|
21
|
+
var GtmContext = react.createContext(null);
|
|
22
|
+
var warnOnNestedProvider = () => {
|
|
23
|
+
if (process.env.NODE_ENV !== "production") {
|
|
24
|
+
console.warn(
|
|
25
|
+
"[gtm-kit/react] Nested GtmProvider detected. You should only have one GtmProvider at the root of your app. The nested provider will be ignored."
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
var warnOnConfigChange = (initialConfig, nextConfig) => {
|
|
30
|
+
if (process.env.NODE_ENV !== "production" && initialConfig !== nextConfig) {
|
|
31
|
+
console.warn(
|
|
32
|
+
"[gtm-kit/react] GtmProvider received new configuration; reconfiguration after mount is not supported. The initial configuration will continue to be used."
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
var extractContainerIds = (config) => {
|
|
37
|
+
const containers = config.containers;
|
|
38
|
+
const containerArray = Array.isArray(containers) ? containers : [containers];
|
|
39
|
+
return containerArray.map((container) => {
|
|
40
|
+
if (typeof container === "string") {
|
|
41
|
+
return container;
|
|
42
|
+
}
|
|
43
|
+
return container.id;
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
var warnOnOrphanedSsrScripts = (configuredContainers) => {
|
|
47
|
+
if (process.env.NODE_ENV !== "production" && typeof document !== "undefined") {
|
|
48
|
+
const gtmScripts = document.querySelectorAll('script[src*="googletagmanager.com/gtm.js"]');
|
|
49
|
+
gtmScripts.forEach((script) => {
|
|
50
|
+
const src = script.src;
|
|
51
|
+
const idMatch = src.match(/[?&]id=([^&]+)/);
|
|
52
|
+
const scriptContainerId = idMatch == null ? void 0 : idMatch[1];
|
|
53
|
+
if (scriptContainerId && !configuredContainers.includes(scriptContainerId)) {
|
|
54
|
+
console.warn(
|
|
55
|
+
`[gtm-kit/react] Found pre-rendered GTM script for container "${scriptContainerId}" that is not configured in GtmProvider. This may indicate a hydration mismatch between SSR and client-side rendering. Configure GtmProvider with containers: "${scriptContainerId}" to properly hydrate.`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
const gtmNoscripts = document.querySelectorAll('noscript iframe[src*="googletagmanager.com/ns.html"]');
|
|
60
|
+
gtmNoscripts.forEach((iframe) => {
|
|
61
|
+
const src = iframe.src;
|
|
62
|
+
const idMatch = src.match(/[?&]id=([^&]+)/);
|
|
63
|
+
const iframeContainerId = idMatch == null ? void 0 : idMatch[1];
|
|
64
|
+
if (iframeContainerId && !configuredContainers.includes(iframeContainerId)) {
|
|
65
|
+
console.warn(
|
|
66
|
+
`[gtm-kit/react] Found pre-rendered GTM noscript iframe for container "${iframeContainerId}" that is not configured in GtmProvider. If you pre-render noscript fallbacks on the server, ensure your GtmProvider has the same container ID.`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
var useStableClient = (config) => {
|
|
73
|
+
const clientRef = react.useRef();
|
|
74
|
+
const configRef = react.useRef();
|
|
75
|
+
if (!clientRef.current) {
|
|
76
|
+
clientRef.current = gtmKit.createGtmClient(config);
|
|
77
|
+
configRef.current = config;
|
|
78
|
+
} else if (configRef.current) {
|
|
79
|
+
warnOnConfigChange(configRef.current, config);
|
|
80
|
+
}
|
|
81
|
+
return clientRef.current;
|
|
82
|
+
};
|
|
83
|
+
var GtmProvider = ({ config, children }) => {
|
|
84
|
+
const existingContext = react.useContext(GtmContext);
|
|
85
|
+
react.useEffect(() => {
|
|
86
|
+
if (existingContext) {
|
|
87
|
+
warnOnNestedProvider();
|
|
88
|
+
}
|
|
89
|
+
}, [existingContext]);
|
|
90
|
+
if (existingContext) {
|
|
91
|
+
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
|
|
92
|
+
}
|
|
93
|
+
return /* @__PURE__ */ jsxRuntime.jsx(GtmProviderInner, { config, children });
|
|
94
|
+
};
|
|
95
|
+
var GtmProviderInner = ({ config, children }) => {
|
|
96
|
+
const client = useStableClient(config);
|
|
97
|
+
react.useEffect(() => {
|
|
98
|
+
warnOnOrphanedSsrScripts(extractContainerIds(config));
|
|
99
|
+
client.init();
|
|
100
|
+
return () => {
|
|
101
|
+
client.teardown();
|
|
102
|
+
};
|
|
103
|
+
}, [client, config]);
|
|
104
|
+
const value = react.useMemo(
|
|
105
|
+
() => ({
|
|
106
|
+
client,
|
|
107
|
+
push: (value2) => client.push(value2),
|
|
108
|
+
setConsentDefaults: (state, options) => client.setConsentDefaults(state, options),
|
|
109
|
+
updateConsent: (state, options) => client.updateConsent(state, options),
|
|
110
|
+
isReady: () => client.isReady(),
|
|
111
|
+
whenReady: () => client.whenReady(),
|
|
112
|
+
onReady: (callback) => client.onReady(callback)
|
|
113
|
+
}),
|
|
114
|
+
[client]
|
|
115
|
+
);
|
|
116
|
+
return /* @__PURE__ */ jsxRuntime.jsx(GtmContext.Provider, { value, children });
|
|
117
|
+
};
|
|
118
|
+
var useGtmContext = () => {
|
|
119
|
+
const context = react.useContext(GtmContext);
|
|
120
|
+
if (!context) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
'[gtm-kit/react] useGtm() was called outside of a GtmProvider. Make sure to wrap your app with <GtmProvider config={{ containers: "GTM-XXXXXX" }}>.'
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return context;
|
|
126
|
+
};
|
|
127
|
+
var useGtm = useGtmContext;
|
|
128
|
+
var useIsGtmProviderPresent = () => {
|
|
129
|
+
const context = react.useContext(GtmContext);
|
|
130
|
+
return context !== null;
|
|
131
|
+
};
|
|
132
|
+
var useGtmClient = () => {
|
|
133
|
+
return useGtmContext().client;
|
|
134
|
+
};
|
|
135
|
+
var useGtmPush = () => {
|
|
136
|
+
return useGtmContext().push;
|
|
137
|
+
};
|
|
138
|
+
var useGtmConsent = () => {
|
|
139
|
+
const { setConsentDefaults, updateConsent } = useGtmContext();
|
|
140
|
+
return react.useMemo(() => ({ setConsentDefaults, updateConsent }), [setConsentDefaults, updateConsent]);
|
|
141
|
+
};
|
|
142
|
+
var useGtmReady = () => {
|
|
143
|
+
const { whenReady } = useGtmContext();
|
|
144
|
+
return whenReady;
|
|
145
|
+
};
|
|
146
|
+
var useIsGtmReady = () => {
|
|
147
|
+
const { isReady } = useGtmContext();
|
|
148
|
+
return isReady;
|
|
149
|
+
};
|
|
150
|
+
var useGtmInitialized = () => {
|
|
151
|
+
const { isReady, onReady } = useGtmContext();
|
|
152
|
+
const [initialized, setInitialized] = react.useState(() => isReady());
|
|
153
|
+
react.useEffect(() => {
|
|
154
|
+
if (isReady()) {
|
|
155
|
+
setInitialized(true);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const unsubscribe = onReady(() => {
|
|
159
|
+
setInitialized(true);
|
|
160
|
+
});
|
|
161
|
+
return unsubscribe;
|
|
162
|
+
}, [isReady, onReady]);
|
|
163
|
+
return initialized;
|
|
164
|
+
};
|
|
165
|
+
var useGtmError = () => {
|
|
166
|
+
const { onReady } = useGtmContext();
|
|
167
|
+
const [errorState, setErrorState] = react.useState({
|
|
168
|
+
hasError: false,
|
|
169
|
+
failedScripts: [],
|
|
170
|
+
errorMessage: null
|
|
171
|
+
});
|
|
172
|
+
react.useEffect(() => {
|
|
173
|
+
const unsubscribe = onReady((states) => {
|
|
174
|
+
var _a, _b;
|
|
175
|
+
const failedScripts = states.filter((s) => s.status === "failed" || s.status === "partial");
|
|
176
|
+
if (failedScripts.length > 0) {
|
|
177
|
+
const firstError = (_b = (_a = failedScripts.find((s) => s.error)) == null ? void 0 : _a.error) != null ? _b : null;
|
|
178
|
+
setErrorState({
|
|
179
|
+
hasError: true,
|
|
180
|
+
failedScripts,
|
|
181
|
+
errorMessage: firstError
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
return unsubscribe;
|
|
186
|
+
}, [onReady]);
|
|
187
|
+
return errorState;
|
|
188
|
+
};
|
|
189
|
+
var GtmErrorBoundary = class extends react.Component {
|
|
190
|
+
constructor(props) {
|
|
191
|
+
super(props);
|
|
192
|
+
this.reset = () => {
|
|
193
|
+
this.setState({ hasError: false, error: null });
|
|
194
|
+
};
|
|
195
|
+
this.state = { hasError: false, error: null };
|
|
196
|
+
}
|
|
197
|
+
static getDerivedStateFromError(error) {
|
|
198
|
+
return { hasError: true, error };
|
|
199
|
+
}
|
|
200
|
+
componentDidCatch(error, errorInfo) {
|
|
201
|
+
const { onError, logErrors = process.env.NODE_ENV !== "production" } = this.props;
|
|
202
|
+
if (logErrors) {
|
|
203
|
+
console.error("[gtm-kit/react] Error caught by GtmErrorBoundary:", error);
|
|
204
|
+
console.error("[gtm-kit/react] Component stack:", errorInfo.componentStack);
|
|
205
|
+
}
|
|
206
|
+
if (onError) {
|
|
207
|
+
try {
|
|
208
|
+
onError(error, errorInfo);
|
|
209
|
+
} catch (e) {
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
render() {
|
|
214
|
+
const { hasError, error } = this.state;
|
|
215
|
+
const { children, fallback } = this.props;
|
|
216
|
+
if (hasError && error) {
|
|
217
|
+
if (fallback === void 0) {
|
|
218
|
+
return children;
|
|
219
|
+
}
|
|
220
|
+
if (typeof fallback === "function") {
|
|
221
|
+
return fallback(error, this.reset);
|
|
222
|
+
}
|
|
223
|
+
return fallback;
|
|
224
|
+
}
|
|
225
|
+
return children;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
8
228
|
|
|
9
|
-
exports.
|
|
10
|
-
exports.
|
|
11
|
-
exports.
|
|
12
|
-
exports.
|
|
13
|
-
exports.
|
|
14
|
-
exports.
|
|
229
|
+
exports.GtmErrorBoundary = GtmErrorBoundary;
|
|
230
|
+
exports.GtmProvider = GtmProvider;
|
|
231
|
+
exports.isSsr = isSsr;
|
|
232
|
+
exports.useGtm = useGtm;
|
|
233
|
+
exports.useGtmClient = useGtmClient;
|
|
234
|
+
exports.useGtmConsent = useGtmConsent;
|
|
235
|
+
exports.useGtmError = useGtmError;
|
|
236
|
+
exports.useGtmInitialized = useGtmInitialized;
|
|
237
|
+
exports.useGtmPush = useGtmPush;
|
|
238
|
+
exports.useGtmReady = useGtmReady;
|
|
239
|
+
exports.useHydrated = useHydrated;
|
|
240
|
+
exports.useIsGtmProviderPresent = useIsGtmProviderPresent;
|
|
241
|
+
exports.useIsGtmReady = useIsGtmReady;
|
|
15
242
|
//# sourceMappingURL=out.js.map
|
|
16
243
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/provider.tsx"],"names":["createContext","useContext","useEffect","useMemo","useRef","createGtmClient","jsx","GtmContext","warnOnConfigChange","initialConfig","nextConfig","useStableClient","config","clientRef","configRef","GtmProvider","children","client","value","state","options","callback","useGtmContext","context","useGtm","useGtmClient","useGtmPush","useGtmConsent","setConsentDefaults","updateConsent","useGtmReady","whenReady"],"mappings":"AAAA,OAAS,iBAAAA,EAAe,cAAAC,EAAY,aAAAC,EAAW,WAAAC,EAAS,UAAAC,MAAgD,QACxG,OACE,mBAAAC,MAOK,qBAmEE,cAAAC,MAAA,oBA/CT,IAAMC,EAAaP,EAAsC,IAAI,EAEvDQ,EAAqB,CAACC,EAAuCC,IAA6C,CAC1G,QAAQ,IAAI,WAAa,cAAgBD,IAAkBC,GAC7D,QAAQ,KACN,2JAEF,CAEJ,EAEMC,EAAmBC,GAA8C,CACrE,IAAMC,EAAYT,EAAkB,EAC9BU,EAAYV,EAA+B,EAEjD,OAAKS,EAAU,QAGJC,EAAU,SACnBN,EAAmBM,EAAU,QAASF,CAAM,GAH5CC,EAAU,QAAUR,EAAgBO,CAAM,EAC1CE,EAAU,QAAUF,GAKfC,EAAU,OACnB,EAEaE,EAAc,CAAC,CAAE,OAAAH,EAAQ,SAAAI,CAAS,IAAqC,CAClF,IAAMC,EAASN,EAAgBC,CAAM,EAErCV,EAAU,KACRe,EAAO,KAAK,EACL,IAAM,CACXA,EAAO,SAAS,CAClB,GACC,CAACA,CAAM,CAAC,EAEX,IAAMC,EAAQf,EACZ,KAAO,CACL,OAAAc,EACA,KAAOC,GAAUD,EAAO,KAAKC,CAAK,EAClC,mBAAoB,CAACC,EAAOC,IAAYH,EAAO,mBAAmBE,EAAOC,CAAO,EAChF,cAAe,CAACD,EAAOC,IAAYH,EAAO,cAAcE,EAAOC,CAAO,EACtE,UAAW,IAAMH,EAAO,UAAU,EAClC,QAAUI,GAAaJ,EAAO,QAAQI,CAAQ,CAChD,GACA,CAACJ,CAAM,CACT,EAEA,OAAOX,EAACC,EAAW,SAAX,CAAoB,MAAOW,EAAQ,SAAAF,EAAS,CACtD,EAEMM,EAAgB,IAAuB,CAC3C,IAAMC,EAAUtB,EAAWM,CAAU,EACrC,GAAI,CAACgB,EACH,MAAM,IAAI,MAAM,yDAAyD,EAE3E,OAAOA,CACT,EAEaC,EAASF,EAETG,EAAe,IACnBH,EAAc,EAAE,OAGZI,EAAa,IACjBJ,EAAc,EAAE,KAGZK,EAAgB,IAAqB,CAChD,GAAM,CAAE,mBAAAC,EAAoB,cAAAC,CAAc,EAAIP,EAAc,EAC5D,OAAOnB,EAAQ,KAAO,CAAE,mBAAAyB,EAAoB,cAAAC,CAAc,GAAI,CAACD,EAAoBC,CAAa,CAAC,CACnG,EAEaC,EAAc,IAA0C,CACnE,GAAM,CAAE,UAAAC,CAAU,EAAIT,EAAc,EACpC,OAAOS,CACT","sourcesContent":["import { createContext, useContext, useEffect, useMemo, useRef, type PropsWithChildren, type JSX } from 'react';\nimport {\n createGtmClient,\n type ConsentRegionOptions,\n type ConsentState,\n type CreateGtmClientOptions,\n type DataLayerValue,\n type GtmClient,\n type ScriptLoadState\n} from '@jwiedeman/gtm-kit';\n\nexport interface GtmProviderProps extends PropsWithChildren {\n config: CreateGtmClientOptions;\n}\n\nexport interface GtmContextValue {\n client: GtmClient;\n push: (value: DataLayerValue) => void;\n setConsentDefaults: (state: ConsentState, options?: ConsentRegionOptions) => void;\n updateConsent: (state: ConsentState, options?: ConsentRegionOptions) => void;\n whenReady: () => Promise<ScriptLoadState[]>;\n onReady: (callback: (state: ScriptLoadState[]) => void) => () => void;\n}\n\nexport interface GtmConsentApi {\n setConsentDefaults: (state: ConsentState, options?: ConsentRegionOptions) => void;\n updateConsent: (state: ConsentState, options?: ConsentRegionOptions) => void;\n}\n\nconst GtmContext = createContext<GtmContextValue | null>(null);\n\nconst warnOnConfigChange = (initialConfig: CreateGtmClientOptions, nextConfig: CreateGtmClientOptions): void => {\n if (process.env.NODE_ENV !== 'production' && initialConfig !== nextConfig) {\n console.warn(\n '[react-gtm-kit] GtmProvider received new configuration; reconfiguration after mount is not supported. ' +\n 'The initial configuration will continue to be used.'\n );\n }\n};\n\nconst useStableClient = (config: CreateGtmClientOptions): GtmClient => {\n const clientRef = useRef<GtmClient>();\n const configRef = useRef<CreateGtmClientOptions>();\n\n if (!clientRef.current) {\n clientRef.current = createGtmClient(config);\n configRef.current = config;\n } else if (configRef.current) {\n warnOnConfigChange(configRef.current, config);\n }\n\n return clientRef.current!;\n};\n\nexport const GtmProvider = ({ config, children }: GtmProviderProps): JSX.Element => {\n const client = useStableClient(config);\n\n useEffect(() => {\n client.init();\n return () => {\n client.teardown();\n };\n }, [client]);\n\n const value = useMemo<GtmContextValue>(\n () => ({\n client,\n push: (value) => client.push(value),\n setConsentDefaults: (state, options) => client.setConsentDefaults(state, options),\n updateConsent: (state, options) => client.updateConsent(state, options),\n whenReady: () => client.whenReady(),\n onReady: (callback) => client.onReady(callback)\n }),\n [client]\n );\n\n return <GtmContext.Provider value={value}>{children}</GtmContext.Provider>;\n};\n\nconst useGtmContext = (): GtmContextValue => {\n const context = useContext(GtmContext);\n if (!context) {\n throw new Error('useGtm hook must be used within a GtmProvider instance.');\n }\n return context;\n};\n\nexport const useGtm = useGtmContext;\n\nexport const useGtmClient = (): GtmClient => {\n return useGtmContext().client;\n};\n\nexport const useGtmPush = (): ((value: DataLayerValue) => void) => {\n return useGtmContext().push;\n};\n\nexport const useGtmConsent = (): GtmConsentApi => {\n const { setConsentDefaults, updateConsent } = useGtmContext();\n return useMemo(() => ({ setConsentDefaults, updateConsent }), [setConsentDefaults, updateConsent]);\n};\n\nexport const useGtmReady = (): (() => Promise<ScriptLoadState[]>) => {\n const { whenReady } = useGtmContext();\n return whenReady;\n};\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/provider.tsx"],"names":["value"],"mappings":"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","sourcesContent":["import {\n Component,\n createContext,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n useSyncExternalStore,\n type ErrorInfo,\n type PropsWithChildren,\n type ReactNode,\n type JSX\n} from 'react';\nimport {\n createGtmClient,\n type ConsentRegionOptions,\n type ConsentState,\n type CreateGtmClientOptions,\n type DataLayerValue,\n type GtmClient,\n type ScriptLoadState\n} from '@jwiedeman/gtm-kit';\n\n/**\n * Check if code is running in a server-side rendering environment.\n * Returns true if window is undefined (Node.js/SSR), false in browser.\n */\nexport const isSsr = (): boolean => typeof window === 'undefined';\n\n/**\n * Hook that returns false during SSR and initial hydration, then true after hydration completes.\n * Use this to prevent hydration mismatches when rendering GTM-dependent content.\n *\n * @example\n * ```tsx\n * const isHydrated = useHydrated();\n *\n * // Safe: won't cause hydration mismatch\n * return isHydrated ? <DynamicContent /> : <StaticPlaceholder />;\n * ```\n */\nexport const useHydrated = (): boolean => {\n // useSyncExternalStore with getServerSnapshot ensures consistent SSR/hydration\n return useSyncExternalStore(\n // Subscribe function (no-op, state never changes after hydration)\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n () => () => {},\n // getSnapshot (client): always true after first render\n () => true,\n // getServerSnapshot (server): always false\n () => false\n );\n};\n\nexport interface GtmProviderProps extends PropsWithChildren {\n config: CreateGtmClientOptions;\n}\n\nexport interface GtmContextValue {\n client: GtmClient;\n push: (value: DataLayerValue) => void;\n setConsentDefaults: (state: ConsentState, options?: ConsentRegionOptions) => void;\n updateConsent: (state: ConsentState, options?: ConsentRegionOptions) => void;\n /** Synchronously check if all GTM scripts have finished loading */\n isReady: () => boolean;\n whenReady: () => Promise<ScriptLoadState[]>;\n onReady: (callback: (state: ScriptLoadState[]) => void) => () => void;\n}\n\nexport interface GtmConsentApi {\n setConsentDefaults: (state: ConsentState, options?: ConsentRegionOptions) => void;\n updateConsent: (state: ConsentState, options?: ConsentRegionOptions) => void;\n}\n\nconst GtmContext = createContext<GtmContextValue | null>(null);\n\nconst warnOnNestedProvider = (): void => {\n if (process.env.NODE_ENV !== 'production') {\n console.warn(\n '[gtm-kit/react] Nested GtmProvider detected. You should only have one GtmProvider at the root of your app. ' +\n 'The nested provider will be ignored.'\n );\n }\n};\n\nconst warnOnConfigChange = (initialConfig: CreateGtmClientOptions, nextConfig: CreateGtmClientOptions): void => {\n if (process.env.NODE_ENV !== 'production' && initialConfig !== nextConfig) {\n console.warn(\n '[gtm-kit/react] GtmProvider received new configuration; reconfiguration after mount is not supported. ' +\n 'The initial configuration will continue to be used.'\n );\n }\n};\n\n/**\n * Extracts container IDs from the provider config.\n */\nconst extractContainerIds = (config: CreateGtmClientOptions): string[] => {\n const containers = config.containers;\n const containerArray = Array.isArray(containers) ? containers : [containers];\n\n return containerArray.map((container) => {\n if (typeof container === 'string') {\n return container;\n }\n return container.id;\n });\n};\n\n/**\n * Warns in development if there are orphaned SSR-rendered GTM scripts without a matching client.\n * This helps developers identify hydration mismatches where GTM was rendered on the server\n * but the GtmProvider config doesn't match or is missing.\n */\nconst warnOnOrphanedSsrScripts = (configuredContainers: string[]): void => {\n if (process.env.NODE_ENV !== 'production' && typeof document !== 'undefined') {\n // Find all GTM scripts on the page\n const gtmScripts = document.querySelectorAll('script[src*=\"googletagmanager.com/gtm.js\"]');\n\n gtmScripts.forEach((script) => {\n const src = (script as HTMLScriptElement).src;\n const idMatch = src.match(/[?&]id=([^&]+)/);\n const scriptContainerId = idMatch?.[1];\n\n if (scriptContainerId && !configuredContainers.includes(scriptContainerId)) {\n console.warn(\n `[gtm-kit/react] Found pre-rendered GTM script for container \"${scriptContainerId}\" that is not configured in GtmProvider. ` +\n 'This may indicate a hydration mismatch between SSR and client-side rendering. ' +\n `Configure GtmProvider with containers: \"${scriptContainerId}\" to properly hydrate.`\n );\n }\n });\n\n // Also check for noscript iframes\n const gtmNoscripts = document.querySelectorAll('noscript iframe[src*=\"googletagmanager.com/ns.html\"]');\n gtmNoscripts.forEach((iframe) => {\n const src = (iframe as HTMLIFrameElement).src;\n const idMatch = src.match(/[?&]id=([^&]+)/);\n const iframeContainerId = idMatch?.[1];\n\n if (iframeContainerId && !configuredContainers.includes(iframeContainerId)) {\n console.warn(\n `[gtm-kit/react] Found pre-rendered GTM noscript iframe for container \"${iframeContainerId}\" that is not configured in GtmProvider. ` +\n 'If you pre-render noscript fallbacks on the server, ensure your GtmProvider has the same container ID.'\n );\n }\n });\n }\n};\n\nconst useStableClient = (config: CreateGtmClientOptions): GtmClient => {\n const clientRef = useRef<GtmClient>();\n const configRef = useRef<CreateGtmClientOptions>();\n\n if (!clientRef.current) {\n clientRef.current = createGtmClient(config);\n configRef.current = config;\n } else if (configRef.current) {\n warnOnConfigChange(configRef.current, config);\n }\n\n return clientRef.current!;\n};\n\n/**\n * GTM Provider component that initializes Google Tag Manager for your React app.\n *\n * ## SSR/Hydration Behavior\n *\n * This provider is SSR-safe and handles hydration correctly:\n * - **Server**: No GTM initialization occurs (no window/document access)\n * - **Client Hydration**: GTM initializes only after hydration via useEffect\n * - **No Hydration Mismatch**: Provider renders the same on server and client\n *\n * ## Usage with SSR Frameworks\n *\n * ```tsx\n * // Next.js, Remix, etc.\n * export default function App({ children }) {\n * return (\n * <GtmProvider config={{ containers: 'GTM-XXXXXX' }}>\n * {children}\n * </GtmProvider>\n * );\n * }\n * ```\n *\n * ## Preventing Hydration Mismatches\n *\n * If you render different content based on GTM state, use `useHydrated`:\n *\n * ```tsx\n * const isHydrated = useHydrated();\n * const isGtmReady = useGtmInitialized();\n *\n * // Safe: both server and client initially render the placeholder\n * if (!isHydrated) return <Placeholder />;\n * return isGtmReady ? <TrackedContent /> : <LoadingContent />;\n * ```\n */\nexport const GtmProvider = ({ config, children }: GtmProviderProps): JSX.Element => {\n const existingContext = useContext(GtmContext);\n\n // Warn if we're inside another GtmProvider (nested providers)\n useEffect(() => {\n if (existingContext) {\n warnOnNestedProvider();\n }\n }, [existingContext]);\n\n // If nested, just pass through children without creating a new context\n if (existingContext) {\n return <>{children}</>;\n }\n\n return <GtmProviderInner config={config}>{children}</GtmProviderInner>;\n};\n\nconst GtmProviderInner = ({ config, children }: GtmProviderProps): JSX.Element => {\n const client = useStableClient(config);\n\n useEffect(() => {\n // Check for orphaned SSR scripts before initializing\n warnOnOrphanedSsrScripts(extractContainerIds(config));\n\n client.init();\n return () => {\n client.teardown();\n };\n }, [client, config]);\n\n const value = useMemo<GtmContextValue>(\n () => ({\n client,\n push: (value) => client.push(value),\n setConsentDefaults: (state, options) => client.setConsentDefaults(state, options),\n updateConsent: (state, options) => client.updateConsent(state, options),\n isReady: () => client.isReady(),\n whenReady: () => client.whenReady(),\n onReady: (callback) => client.onReady(callback)\n }),\n [client]\n );\n\n return <GtmContext.Provider value={value}>{children}</GtmContext.Provider>;\n};\n\nconst useGtmContext = (): GtmContextValue => {\n const context = useContext(GtmContext);\n if (!context) {\n throw new Error(\n '[gtm-kit/react] useGtm() was called outside of a GtmProvider. ' +\n 'Make sure to wrap your app with <GtmProvider config={{ containers: \"GTM-XXXXXX\" }}>.'\n );\n }\n return context;\n};\n\n/**\n * Hook to access the full GTM context. Throws if used outside GtmProvider.\n *\n * For most use cases, prefer the specific hooks:\n * - `useGtmPush()` - Push events to dataLayer\n * - `useGtmConsent()` - Manage consent state\n * - `useGtmClient()` - Access the raw GTM client\n *\n * @throws Error if called outside of GtmProvider\n *\n * @example\n * ```tsx\n * const { push, client, isReady, whenReady } = useGtm();\n * ```\n */\nexport const useGtm = useGtmContext;\n\n/**\n * Hook to check if GtmProvider is present without throwing.\n *\n * Useful for components that may be rendered before the provider mounts,\n * or for optional GTM integration.\n *\n * @returns `true` if inside GtmProvider, `false` otherwise\n *\n * @example\n * ```tsx\n * const hasProvider = useIsGtmProviderPresent();\n *\n * const handleClick = () => {\n * if (hasProvider) {\n * // Safe to use GTM hooks\n * }\n * };\n * ```\n */\nexport const useIsGtmProviderPresent = (): boolean => {\n const context = useContext(GtmContext);\n return context !== null;\n};\n\n/**\n * Hook to access the raw GTM client instance.\n *\n * @throws Error if called outside of GtmProvider\n * @returns The GTM client instance\n *\n * @example\n * ```tsx\n * const client = useGtmClient();\n *\n * // Access low-level APIs\n * const diagnostics = client.getDiagnostics();\n * ```\n */\nexport const useGtmClient = (): GtmClient => {\n return useGtmContext().client;\n};\n\n/**\n * Hook to get the push function for sending events to the dataLayer.\n *\n * This is the most commonly used hook for tracking events.\n *\n * @throws Error if called outside of GtmProvider\n * @returns Function to push values to the dataLayer\n *\n * @example\n * ```tsx\n * const push = useGtmPush();\n *\n * const handleAddToCart = (product) => {\n * push({\n * event: 'add_to_cart',\n * ecommerce: {\n * items: [{ item_id: product.id, item_name: product.name }]\n * }\n * });\n * };\n * ```\n */\nexport const useGtmPush = (): ((value: DataLayerValue) => void) => {\n return useGtmContext().push;\n};\n\n/**\n * Hook to access consent management functions.\n *\n * @throws Error if called outside of GtmProvider\n * @returns Object with `setConsentDefaults` and `updateConsent` functions\n *\n * @example\n * ```tsx\n * const { updateConsent } = useGtmConsent();\n *\n * const handleAcceptCookies = () => {\n * updateConsent({\n * ad_storage: 'granted',\n * analytics_storage: 'granted'\n * });\n * };\n * ```\n */\nexport const useGtmConsent = (): GtmConsentApi => {\n const { setConsentDefaults, updateConsent } = useGtmContext();\n return useMemo(() => ({ setConsentDefaults, updateConsent }), [setConsentDefaults, updateConsent]);\n};\n\n/**\n * Hook that returns the `whenReady` promise function.\n *\n * **When to use**: When you need to await GTM script loading before taking an action.\n * The returned function returns a Promise that resolves when scripts finish loading.\n *\n * **Comparison of GTM readiness hooks:**\n * | Hook | Returns | Re-renders | Use Case |\n * |------|---------|------------|----------|\n * | `useGtmReady()` | `() => Promise` | No | Await in event handlers |\n * | `useIsGtmReady()` | `() => boolean` | No | Synchronous checks in callbacks |\n * | `useGtmInitialized()` | `boolean` | Yes | Conditional rendering |\n *\n * @returns Function that returns a Promise resolving to script load states\n *\n * @example\n * ```tsx\n * const whenReady = useGtmReady();\n *\n * const handleClick = async () => {\n * const states = await whenReady();\n * if (states.every(s => s.status === 'loaded')) {\n * // Safe to rely on GTM being fully loaded\n * }\n * };\n * ```\n */\nexport const useGtmReady = (): (() => Promise<ScriptLoadState[]>) => {\n const { whenReady } = useGtmContext();\n return whenReady;\n};\n\n/**\n * Hook that returns a function to synchronously check if GTM is ready.\n *\n * **When to use**: When you need to check readiness without triggering re-renders,\n * typically in event handlers or callbacks.\n *\n * **Comparison of GTM readiness hooks:**\n * | Hook | Returns | Re-renders | Use Case |\n * |------|---------|------------|----------|\n * | `useGtmReady()` | `() => Promise` | No | Await in event handlers |\n * | `useIsGtmReady()` | `() => boolean` | No | Synchronous checks in callbacks |\n * | `useGtmInitialized()` | `boolean` | Yes | Conditional rendering |\n *\n * @returns Function that returns `true` if scripts loaded, `false` if still loading\n *\n * @example\n * ```tsx\n * const checkReady = useIsGtmReady();\n *\n * const handleSubmit = () => {\n * if (checkReady()) {\n * // GTM is ready, proceed with tracking\n * push({ event: 'form_submit' });\n * }\n * };\n * ```\n */\nexport const useIsGtmReady = (): (() => boolean) => {\n const { isReady } = useGtmContext();\n return isReady;\n};\n\n/**\n * Reactive hook that returns `true` when GTM scripts have finished loading.\n *\n * **When to use**: When you need to conditionally render UI based on GTM readiness.\n * This hook triggers a re-render when the state changes.\n *\n * **Comparison of GTM readiness hooks:**\n * | Hook | Returns | Re-renders | Use Case |\n * |------|---------|------------|----------|\n * | `useGtmReady()` | `() => Promise` | No | Await in event handlers |\n * | `useIsGtmReady()` | `() => boolean` | No | Synchronous checks in callbacks |\n * | `useGtmInitialized()` | `boolean` | Yes | Conditional rendering |\n *\n * @returns `true` if GTM is initialized, `false` otherwise (reactive)\n *\n * @example\n * ```tsx\n * const isInitialized = useGtmInitialized();\n *\n * if (!isInitialized) {\n * return <LoadingSpinner />;\n * }\n *\n * return <AnalyticsDashboard />;\n * ```\n */\nexport const useGtmInitialized = (): boolean => {\n const { isReady, onReady } = useGtmContext();\n const [initialized, setInitialized] = useState(() => isReady());\n\n useEffect(() => {\n // Already initialized on mount\n if (isReady()) {\n setInitialized(true);\n return;\n }\n\n // Subscribe to ready event\n const unsubscribe = onReady(() => {\n setInitialized(true);\n });\n\n return unsubscribe;\n }, [isReady, onReady]);\n\n return initialized;\n};\n\n/**\n * Result from the useGtmError hook.\n */\nexport interface GtmErrorState {\n /** Whether any scripts failed to load */\n hasError: boolean;\n /** Array of failed script states (status 'failed' or 'partial') */\n failedScripts: ScriptLoadState[];\n /** Convenience getter for the first error message, if any */\n errorMessage: string | null;\n}\n\n/**\n * Hook to capture GTM script load errors.\n * Returns reactive state that updates when scripts fail to load.\n *\n * @example\n * ```tsx\n * const { hasError, failedScripts, errorMessage } = useGtmError();\n *\n * if (hasError) {\n * console.error('GTM failed to load:', errorMessage);\n * // Optionally show fallback UI or retry logic\n * }\n * ```\n */\nexport const useGtmError = (): GtmErrorState => {\n const { onReady } = useGtmContext();\n const [errorState, setErrorState] = useState<GtmErrorState>({\n hasError: false,\n failedScripts: [],\n errorMessage: null\n });\n\n useEffect(() => {\n const unsubscribe = onReady((states) => {\n const failedScripts = states.filter((s) => s.status === 'failed' || s.status === 'partial');\n\n if (failedScripts.length > 0) {\n const firstError = failedScripts.find((s) => s.error)?.error ?? null;\n setErrorState({\n hasError: true,\n failedScripts,\n errorMessage: firstError\n });\n }\n });\n\n return unsubscribe;\n }, [onReady]);\n\n return errorState;\n};\n\n/**\n * Props for GtmErrorBoundary component.\n */\nexport interface GtmErrorBoundaryProps {\n children: ReactNode;\n /** Fallback UI to render when an error occurs */\n fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);\n /** Callback invoked when an error is caught */\n onError?: (error: Error, errorInfo: ErrorInfo) => void;\n /** Whether to log errors to console (default: true in development) */\n logErrors?: boolean;\n}\n\ninterface GtmErrorBoundaryState {\n hasError: boolean;\n error: Error | null;\n}\n\n/**\n * Error boundary component for GTM provider.\n * Catches errors during GTM initialization and renders a fallback UI.\n * Analytics and tracking will be disabled when an error occurs.\n */\nexport class GtmErrorBoundary extends Component<GtmErrorBoundaryProps, GtmErrorBoundaryState> {\n constructor(props: GtmErrorBoundaryProps) {\n super(props);\n this.state = { hasError: false, error: null };\n }\n\n static getDerivedStateFromError(error: Error): GtmErrorBoundaryState {\n return { hasError: true, error };\n }\n\n componentDidCatch(error: Error, errorInfo: ErrorInfo): void {\n const { onError, logErrors = process.env.NODE_ENV !== 'production' } = this.props;\n\n if (logErrors) {\n console.error('[gtm-kit/react] Error caught by GtmErrorBoundary:', error);\n console.error('[gtm-kit/react] Component stack:', errorInfo.componentStack);\n }\n\n if (onError) {\n try {\n onError(error, errorInfo);\n } catch {\n // Ignore callback errors\n }\n }\n }\n\n reset = (): void => {\n this.setState({ hasError: false, error: null });\n };\n\n render(): ReactNode {\n const { hasError, error } = this.state;\n const { children, fallback } = this.props;\n\n if (hasError && error) {\n if (fallback === undefined) {\n // Default: render children without GTM (silent fallback)\n return children;\n }\n\n if (typeof fallback === 'function') {\n return fallback(error, this.reset);\n }\n\n return fallback;\n }\n\n return children;\n }\n}\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
|
-
import { PropsWithChildren, JSX } from 'react';
|
|
1
|
+
import { PropsWithChildren, JSX, ReactNode, ErrorInfo, Component } from 'react';
|
|
2
2
|
import { CreateGtmClientOptions, GtmClient, DataLayerValue, ConsentState, ConsentRegionOptions, ScriptLoadState } from '@jwiedeman/gtm-kit';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Check if code is running in a server-side rendering environment.
|
|
6
|
+
* Returns true if window is undefined (Node.js/SSR), false in browser.
|
|
7
|
+
*/
|
|
8
|
+
declare const isSsr: () => boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Hook that returns false during SSR and initial hydration, then true after hydration completes.
|
|
11
|
+
* Use this to prevent hydration mismatches when rendering GTM-dependent content.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* const isHydrated = useHydrated();
|
|
16
|
+
*
|
|
17
|
+
* // Safe: won't cause hydration mismatch
|
|
18
|
+
* return isHydrated ? <DynamicContent /> : <StaticPlaceholder />;
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
declare const useHydrated: () => boolean;
|
|
4
22
|
interface GtmProviderProps extends PropsWithChildren {
|
|
5
23
|
config: CreateGtmClientOptions;
|
|
6
24
|
}
|
|
@@ -9,6 +27,8 @@ interface GtmContextValue {
|
|
|
9
27
|
push: (value: DataLayerValue) => void;
|
|
10
28
|
setConsentDefaults: (state: ConsentState, options?: ConsentRegionOptions) => void;
|
|
11
29
|
updateConsent: (state: ConsentState, options?: ConsentRegionOptions) => void;
|
|
30
|
+
/** Synchronously check if all GTM scripts have finished loading */
|
|
31
|
+
isReady: () => boolean;
|
|
12
32
|
whenReady: () => Promise<ScriptLoadState[]>;
|
|
13
33
|
onReady: (callback: (state: ScriptLoadState[]) => void) => () => void;
|
|
14
34
|
}
|
|
@@ -16,11 +36,272 @@ interface GtmConsentApi {
|
|
|
16
36
|
setConsentDefaults: (state: ConsentState, options?: ConsentRegionOptions) => void;
|
|
17
37
|
updateConsent: (state: ConsentState, options?: ConsentRegionOptions) => void;
|
|
18
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* GTM Provider component that initializes Google Tag Manager for your React app.
|
|
41
|
+
*
|
|
42
|
+
* ## SSR/Hydration Behavior
|
|
43
|
+
*
|
|
44
|
+
* This provider is SSR-safe and handles hydration correctly:
|
|
45
|
+
* - **Server**: No GTM initialization occurs (no window/document access)
|
|
46
|
+
* - **Client Hydration**: GTM initializes only after hydration via useEffect
|
|
47
|
+
* - **No Hydration Mismatch**: Provider renders the same on server and client
|
|
48
|
+
*
|
|
49
|
+
* ## Usage with SSR Frameworks
|
|
50
|
+
*
|
|
51
|
+
* ```tsx
|
|
52
|
+
* // Next.js, Remix, etc.
|
|
53
|
+
* export default function App({ children }) {
|
|
54
|
+
* return (
|
|
55
|
+
* <GtmProvider config={{ containers: 'GTM-XXXXXX' }}>
|
|
56
|
+
* {children}
|
|
57
|
+
* </GtmProvider>
|
|
58
|
+
* );
|
|
59
|
+
* }
|
|
60
|
+
* ```
|
|
61
|
+
*
|
|
62
|
+
* ## Preventing Hydration Mismatches
|
|
63
|
+
*
|
|
64
|
+
* If you render different content based on GTM state, use `useHydrated`:
|
|
65
|
+
*
|
|
66
|
+
* ```tsx
|
|
67
|
+
* const isHydrated = useHydrated();
|
|
68
|
+
* const isGtmReady = useGtmInitialized();
|
|
69
|
+
*
|
|
70
|
+
* // Safe: both server and client initially render the placeholder
|
|
71
|
+
* if (!isHydrated) return <Placeholder />;
|
|
72
|
+
* return isGtmReady ? <TrackedContent /> : <LoadingContent />;
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
19
75
|
declare const GtmProvider: ({ config, children }: GtmProviderProps) => JSX.Element;
|
|
76
|
+
/**
|
|
77
|
+
* Hook to access the full GTM context. Throws if used outside GtmProvider.
|
|
78
|
+
*
|
|
79
|
+
* For most use cases, prefer the specific hooks:
|
|
80
|
+
* - `useGtmPush()` - Push events to dataLayer
|
|
81
|
+
* - `useGtmConsent()` - Manage consent state
|
|
82
|
+
* - `useGtmClient()` - Access the raw GTM client
|
|
83
|
+
*
|
|
84
|
+
* @throws Error if called outside of GtmProvider
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```tsx
|
|
88
|
+
* const { push, client, isReady, whenReady } = useGtm();
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
20
91
|
declare const useGtm: () => GtmContextValue;
|
|
92
|
+
/**
|
|
93
|
+
* Hook to check if GtmProvider is present without throwing.
|
|
94
|
+
*
|
|
95
|
+
* Useful for components that may be rendered before the provider mounts,
|
|
96
|
+
* or for optional GTM integration.
|
|
97
|
+
*
|
|
98
|
+
* @returns `true` if inside GtmProvider, `false` otherwise
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```tsx
|
|
102
|
+
* const hasProvider = useIsGtmProviderPresent();
|
|
103
|
+
*
|
|
104
|
+
* const handleClick = () => {
|
|
105
|
+
* if (hasProvider) {
|
|
106
|
+
* // Safe to use GTM hooks
|
|
107
|
+
* }
|
|
108
|
+
* };
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
declare const useIsGtmProviderPresent: () => boolean;
|
|
112
|
+
/**
|
|
113
|
+
* Hook to access the raw GTM client instance.
|
|
114
|
+
*
|
|
115
|
+
* @throws Error if called outside of GtmProvider
|
|
116
|
+
* @returns The GTM client instance
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```tsx
|
|
120
|
+
* const client = useGtmClient();
|
|
121
|
+
*
|
|
122
|
+
* // Access low-level APIs
|
|
123
|
+
* const diagnostics = client.getDiagnostics();
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
21
126
|
declare const useGtmClient: () => GtmClient;
|
|
127
|
+
/**
|
|
128
|
+
* Hook to get the push function for sending events to the dataLayer.
|
|
129
|
+
*
|
|
130
|
+
* This is the most commonly used hook for tracking events.
|
|
131
|
+
*
|
|
132
|
+
* @throws Error if called outside of GtmProvider
|
|
133
|
+
* @returns Function to push values to the dataLayer
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```tsx
|
|
137
|
+
* const push = useGtmPush();
|
|
138
|
+
*
|
|
139
|
+
* const handleAddToCart = (product) => {
|
|
140
|
+
* push({
|
|
141
|
+
* event: 'add_to_cart',
|
|
142
|
+
* ecommerce: {
|
|
143
|
+
* items: [{ item_id: product.id, item_name: product.name }]
|
|
144
|
+
* }
|
|
145
|
+
* });
|
|
146
|
+
* };
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
22
149
|
declare const useGtmPush: () => ((value: DataLayerValue) => void);
|
|
150
|
+
/**
|
|
151
|
+
* Hook to access consent management functions.
|
|
152
|
+
*
|
|
153
|
+
* @throws Error if called outside of GtmProvider
|
|
154
|
+
* @returns Object with `setConsentDefaults` and `updateConsent` functions
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```tsx
|
|
158
|
+
* const { updateConsent } = useGtmConsent();
|
|
159
|
+
*
|
|
160
|
+
* const handleAcceptCookies = () => {
|
|
161
|
+
* updateConsent({
|
|
162
|
+
* ad_storage: 'granted',
|
|
163
|
+
* analytics_storage: 'granted'
|
|
164
|
+
* });
|
|
165
|
+
* };
|
|
166
|
+
* ```
|
|
167
|
+
*/
|
|
23
168
|
declare const useGtmConsent: () => GtmConsentApi;
|
|
169
|
+
/**
|
|
170
|
+
* Hook that returns the `whenReady` promise function.
|
|
171
|
+
*
|
|
172
|
+
* **When to use**: When you need to await GTM script loading before taking an action.
|
|
173
|
+
* The returned function returns a Promise that resolves when scripts finish loading.
|
|
174
|
+
*
|
|
175
|
+
* **Comparison of GTM readiness hooks:**
|
|
176
|
+
* | Hook | Returns | Re-renders | Use Case |
|
|
177
|
+
* |------|---------|------------|----------|
|
|
178
|
+
* | `useGtmReady()` | `() => Promise` | No | Await in event handlers |
|
|
179
|
+
* | `useIsGtmReady()` | `() => boolean` | No | Synchronous checks in callbacks |
|
|
180
|
+
* | `useGtmInitialized()` | `boolean` | Yes | Conditional rendering |
|
|
181
|
+
*
|
|
182
|
+
* @returns Function that returns a Promise resolving to script load states
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* ```tsx
|
|
186
|
+
* const whenReady = useGtmReady();
|
|
187
|
+
*
|
|
188
|
+
* const handleClick = async () => {
|
|
189
|
+
* const states = await whenReady();
|
|
190
|
+
* if (states.every(s => s.status === 'loaded')) {
|
|
191
|
+
* // Safe to rely on GTM being fully loaded
|
|
192
|
+
* }
|
|
193
|
+
* };
|
|
194
|
+
* ```
|
|
195
|
+
*/
|
|
24
196
|
declare const useGtmReady: () => (() => Promise<ScriptLoadState[]>);
|
|
197
|
+
/**
|
|
198
|
+
* Hook that returns a function to synchronously check if GTM is ready.
|
|
199
|
+
*
|
|
200
|
+
* **When to use**: When you need to check readiness without triggering re-renders,
|
|
201
|
+
* typically in event handlers or callbacks.
|
|
202
|
+
*
|
|
203
|
+
* **Comparison of GTM readiness hooks:**
|
|
204
|
+
* | Hook | Returns | Re-renders | Use Case |
|
|
205
|
+
* |------|---------|------------|----------|
|
|
206
|
+
* | `useGtmReady()` | `() => Promise` | No | Await in event handlers |
|
|
207
|
+
* | `useIsGtmReady()` | `() => boolean` | No | Synchronous checks in callbacks |
|
|
208
|
+
* | `useGtmInitialized()` | `boolean` | Yes | Conditional rendering |
|
|
209
|
+
*
|
|
210
|
+
* @returns Function that returns `true` if scripts loaded, `false` if still loading
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* ```tsx
|
|
214
|
+
* const checkReady = useIsGtmReady();
|
|
215
|
+
*
|
|
216
|
+
* const handleSubmit = () => {
|
|
217
|
+
* if (checkReady()) {
|
|
218
|
+
* // GTM is ready, proceed with tracking
|
|
219
|
+
* push({ event: 'form_submit' });
|
|
220
|
+
* }
|
|
221
|
+
* };
|
|
222
|
+
* ```
|
|
223
|
+
*/
|
|
224
|
+
declare const useIsGtmReady: () => (() => boolean);
|
|
225
|
+
/**
|
|
226
|
+
* Reactive hook that returns `true` when GTM scripts have finished loading.
|
|
227
|
+
*
|
|
228
|
+
* **When to use**: When you need to conditionally render UI based on GTM readiness.
|
|
229
|
+
* This hook triggers a re-render when the state changes.
|
|
230
|
+
*
|
|
231
|
+
* **Comparison of GTM readiness hooks:**
|
|
232
|
+
* | Hook | Returns | Re-renders | Use Case |
|
|
233
|
+
* |------|---------|------------|----------|
|
|
234
|
+
* | `useGtmReady()` | `() => Promise` | No | Await in event handlers |
|
|
235
|
+
* | `useIsGtmReady()` | `() => boolean` | No | Synchronous checks in callbacks |
|
|
236
|
+
* | `useGtmInitialized()` | `boolean` | Yes | Conditional rendering |
|
|
237
|
+
*
|
|
238
|
+
* @returns `true` if GTM is initialized, `false` otherwise (reactive)
|
|
239
|
+
*
|
|
240
|
+
* @example
|
|
241
|
+
* ```tsx
|
|
242
|
+
* const isInitialized = useGtmInitialized();
|
|
243
|
+
*
|
|
244
|
+
* if (!isInitialized) {
|
|
245
|
+
* return <LoadingSpinner />;
|
|
246
|
+
* }
|
|
247
|
+
*
|
|
248
|
+
* return <AnalyticsDashboard />;
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
251
|
+
declare const useGtmInitialized: () => boolean;
|
|
252
|
+
/**
|
|
253
|
+
* Result from the useGtmError hook.
|
|
254
|
+
*/
|
|
255
|
+
interface GtmErrorState {
|
|
256
|
+
/** Whether any scripts failed to load */
|
|
257
|
+
hasError: boolean;
|
|
258
|
+
/** Array of failed script states (status 'failed' or 'partial') */
|
|
259
|
+
failedScripts: ScriptLoadState[];
|
|
260
|
+
/** Convenience getter for the first error message, if any */
|
|
261
|
+
errorMessage: string | null;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Hook to capture GTM script load errors.
|
|
265
|
+
* Returns reactive state that updates when scripts fail to load.
|
|
266
|
+
*
|
|
267
|
+
* @example
|
|
268
|
+
* ```tsx
|
|
269
|
+
* const { hasError, failedScripts, errorMessage } = useGtmError();
|
|
270
|
+
*
|
|
271
|
+
* if (hasError) {
|
|
272
|
+
* console.error('GTM failed to load:', errorMessage);
|
|
273
|
+
* // Optionally show fallback UI or retry logic
|
|
274
|
+
* }
|
|
275
|
+
* ```
|
|
276
|
+
*/
|
|
277
|
+
declare const useGtmError: () => GtmErrorState;
|
|
278
|
+
/**
|
|
279
|
+
* Props for GtmErrorBoundary component.
|
|
280
|
+
*/
|
|
281
|
+
interface GtmErrorBoundaryProps {
|
|
282
|
+
children: ReactNode;
|
|
283
|
+
/** Fallback UI to render when an error occurs */
|
|
284
|
+
fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);
|
|
285
|
+
/** Callback invoked when an error is caught */
|
|
286
|
+
onError?: (error: Error, errorInfo: ErrorInfo) => void;
|
|
287
|
+
/** Whether to log errors to console (default: true in development) */
|
|
288
|
+
logErrors?: boolean;
|
|
289
|
+
}
|
|
290
|
+
interface GtmErrorBoundaryState {
|
|
291
|
+
hasError: boolean;
|
|
292
|
+
error: Error | null;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Error boundary component for GTM provider.
|
|
296
|
+
* Catches errors during GTM initialization and renders a fallback UI.
|
|
297
|
+
* Analytics and tracking will be disabled when an error occurs.
|
|
298
|
+
*/
|
|
299
|
+
declare class GtmErrorBoundary extends Component<GtmErrorBoundaryProps, GtmErrorBoundaryState> {
|
|
300
|
+
constructor(props: GtmErrorBoundaryProps);
|
|
301
|
+
static getDerivedStateFromError(error: Error): GtmErrorBoundaryState;
|
|
302
|
+
componentDidCatch(error: Error, errorInfo: ErrorInfo): void;
|
|
303
|
+
reset: () => void;
|
|
304
|
+
render(): ReactNode;
|
|
305
|
+
}
|
|
25
306
|
|
|
26
|
-
export { GtmConsentApi, GtmContextValue, GtmProvider, GtmProviderProps, useGtm, useGtmClient, useGtmConsent, useGtmPush, useGtmReady };
|
|
307
|
+
export { GtmConsentApi, GtmContextValue, GtmErrorBoundary, GtmErrorBoundaryProps, GtmErrorState, GtmProvider, GtmProviderProps, isSsr, useGtm, useGtmClient, useGtmConsent, useGtmError, useGtmInitialized, useGtmPush, useGtmReady, useHydrated, useIsGtmProviderPresent, useIsGtmReady };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
|
-
import { PropsWithChildren, JSX } from 'react';
|
|
1
|
+
import { PropsWithChildren, JSX, ReactNode, ErrorInfo, Component } from 'react';
|
|
2
2
|
import { CreateGtmClientOptions, GtmClient, DataLayerValue, ConsentState, ConsentRegionOptions, ScriptLoadState } from '@jwiedeman/gtm-kit';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Check if code is running in a server-side rendering environment.
|
|
6
|
+
* Returns true if window is undefined (Node.js/SSR), false in browser.
|
|
7
|
+
*/
|
|
8
|
+
declare const isSsr: () => boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Hook that returns false during SSR and initial hydration, then true after hydration completes.
|
|
11
|
+
* Use this to prevent hydration mismatches when rendering GTM-dependent content.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* const isHydrated = useHydrated();
|
|
16
|
+
*
|
|
17
|
+
* // Safe: won't cause hydration mismatch
|
|
18
|
+
* return isHydrated ? <DynamicContent /> : <StaticPlaceholder />;
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
declare const useHydrated: () => boolean;
|
|
4
22
|
interface GtmProviderProps extends PropsWithChildren {
|
|
5
23
|
config: CreateGtmClientOptions;
|
|
6
24
|
}
|
|
@@ -9,6 +27,8 @@ interface GtmContextValue {
|
|
|
9
27
|
push: (value: DataLayerValue) => void;
|
|
10
28
|
setConsentDefaults: (state: ConsentState, options?: ConsentRegionOptions) => void;
|
|
11
29
|
updateConsent: (state: ConsentState, options?: ConsentRegionOptions) => void;
|
|
30
|
+
/** Synchronously check if all GTM scripts have finished loading */
|
|
31
|
+
isReady: () => boolean;
|
|
12
32
|
whenReady: () => Promise<ScriptLoadState[]>;
|
|
13
33
|
onReady: (callback: (state: ScriptLoadState[]) => void) => () => void;
|
|
14
34
|
}
|
|
@@ -16,11 +36,272 @@ interface GtmConsentApi {
|
|
|
16
36
|
setConsentDefaults: (state: ConsentState, options?: ConsentRegionOptions) => void;
|
|
17
37
|
updateConsent: (state: ConsentState, options?: ConsentRegionOptions) => void;
|
|
18
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* GTM Provider component that initializes Google Tag Manager for your React app.
|
|
41
|
+
*
|
|
42
|
+
* ## SSR/Hydration Behavior
|
|
43
|
+
*
|
|
44
|
+
* This provider is SSR-safe and handles hydration correctly:
|
|
45
|
+
* - **Server**: No GTM initialization occurs (no window/document access)
|
|
46
|
+
* - **Client Hydration**: GTM initializes only after hydration via useEffect
|
|
47
|
+
* - **No Hydration Mismatch**: Provider renders the same on server and client
|
|
48
|
+
*
|
|
49
|
+
* ## Usage with SSR Frameworks
|
|
50
|
+
*
|
|
51
|
+
* ```tsx
|
|
52
|
+
* // Next.js, Remix, etc.
|
|
53
|
+
* export default function App({ children }) {
|
|
54
|
+
* return (
|
|
55
|
+
* <GtmProvider config={{ containers: 'GTM-XXXXXX' }}>
|
|
56
|
+
* {children}
|
|
57
|
+
* </GtmProvider>
|
|
58
|
+
* );
|
|
59
|
+
* }
|
|
60
|
+
* ```
|
|
61
|
+
*
|
|
62
|
+
* ## Preventing Hydration Mismatches
|
|
63
|
+
*
|
|
64
|
+
* If you render different content based on GTM state, use `useHydrated`:
|
|
65
|
+
*
|
|
66
|
+
* ```tsx
|
|
67
|
+
* const isHydrated = useHydrated();
|
|
68
|
+
* const isGtmReady = useGtmInitialized();
|
|
69
|
+
*
|
|
70
|
+
* // Safe: both server and client initially render the placeholder
|
|
71
|
+
* if (!isHydrated) return <Placeholder />;
|
|
72
|
+
* return isGtmReady ? <TrackedContent /> : <LoadingContent />;
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
19
75
|
declare const GtmProvider: ({ config, children }: GtmProviderProps) => JSX.Element;
|
|
76
|
+
/**
|
|
77
|
+
* Hook to access the full GTM context. Throws if used outside GtmProvider.
|
|
78
|
+
*
|
|
79
|
+
* For most use cases, prefer the specific hooks:
|
|
80
|
+
* - `useGtmPush()` - Push events to dataLayer
|
|
81
|
+
* - `useGtmConsent()` - Manage consent state
|
|
82
|
+
* - `useGtmClient()` - Access the raw GTM client
|
|
83
|
+
*
|
|
84
|
+
* @throws Error if called outside of GtmProvider
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```tsx
|
|
88
|
+
* const { push, client, isReady, whenReady } = useGtm();
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
20
91
|
declare const useGtm: () => GtmContextValue;
|
|
92
|
+
/**
|
|
93
|
+
* Hook to check if GtmProvider is present without throwing.
|
|
94
|
+
*
|
|
95
|
+
* Useful for components that may be rendered before the provider mounts,
|
|
96
|
+
* or for optional GTM integration.
|
|
97
|
+
*
|
|
98
|
+
* @returns `true` if inside GtmProvider, `false` otherwise
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```tsx
|
|
102
|
+
* const hasProvider = useIsGtmProviderPresent();
|
|
103
|
+
*
|
|
104
|
+
* const handleClick = () => {
|
|
105
|
+
* if (hasProvider) {
|
|
106
|
+
* // Safe to use GTM hooks
|
|
107
|
+
* }
|
|
108
|
+
* };
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
declare const useIsGtmProviderPresent: () => boolean;
|
|
112
|
+
/**
|
|
113
|
+
* Hook to access the raw GTM client instance.
|
|
114
|
+
*
|
|
115
|
+
* @throws Error if called outside of GtmProvider
|
|
116
|
+
* @returns The GTM client instance
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```tsx
|
|
120
|
+
* const client = useGtmClient();
|
|
121
|
+
*
|
|
122
|
+
* // Access low-level APIs
|
|
123
|
+
* const diagnostics = client.getDiagnostics();
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
21
126
|
declare const useGtmClient: () => GtmClient;
|
|
127
|
+
/**
|
|
128
|
+
* Hook to get the push function for sending events to the dataLayer.
|
|
129
|
+
*
|
|
130
|
+
* This is the most commonly used hook for tracking events.
|
|
131
|
+
*
|
|
132
|
+
* @throws Error if called outside of GtmProvider
|
|
133
|
+
* @returns Function to push values to the dataLayer
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```tsx
|
|
137
|
+
* const push = useGtmPush();
|
|
138
|
+
*
|
|
139
|
+
* const handleAddToCart = (product) => {
|
|
140
|
+
* push({
|
|
141
|
+
* event: 'add_to_cart',
|
|
142
|
+
* ecommerce: {
|
|
143
|
+
* items: [{ item_id: product.id, item_name: product.name }]
|
|
144
|
+
* }
|
|
145
|
+
* });
|
|
146
|
+
* };
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
22
149
|
declare const useGtmPush: () => ((value: DataLayerValue) => void);
|
|
150
|
+
/**
|
|
151
|
+
* Hook to access consent management functions.
|
|
152
|
+
*
|
|
153
|
+
* @throws Error if called outside of GtmProvider
|
|
154
|
+
* @returns Object with `setConsentDefaults` and `updateConsent` functions
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```tsx
|
|
158
|
+
* const { updateConsent } = useGtmConsent();
|
|
159
|
+
*
|
|
160
|
+
* const handleAcceptCookies = () => {
|
|
161
|
+
* updateConsent({
|
|
162
|
+
* ad_storage: 'granted',
|
|
163
|
+
* analytics_storage: 'granted'
|
|
164
|
+
* });
|
|
165
|
+
* };
|
|
166
|
+
* ```
|
|
167
|
+
*/
|
|
23
168
|
declare const useGtmConsent: () => GtmConsentApi;
|
|
169
|
+
/**
|
|
170
|
+
* Hook that returns the `whenReady` promise function.
|
|
171
|
+
*
|
|
172
|
+
* **When to use**: When you need to await GTM script loading before taking an action.
|
|
173
|
+
* The returned function returns a Promise that resolves when scripts finish loading.
|
|
174
|
+
*
|
|
175
|
+
* **Comparison of GTM readiness hooks:**
|
|
176
|
+
* | Hook | Returns | Re-renders | Use Case |
|
|
177
|
+
* |------|---------|------------|----------|
|
|
178
|
+
* | `useGtmReady()` | `() => Promise` | No | Await in event handlers |
|
|
179
|
+
* | `useIsGtmReady()` | `() => boolean` | No | Synchronous checks in callbacks |
|
|
180
|
+
* | `useGtmInitialized()` | `boolean` | Yes | Conditional rendering |
|
|
181
|
+
*
|
|
182
|
+
* @returns Function that returns a Promise resolving to script load states
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* ```tsx
|
|
186
|
+
* const whenReady = useGtmReady();
|
|
187
|
+
*
|
|
188
|
+
* const handleClick = async () => {
|
|
189
|
+
* const states = await whenReady();
|
|
190
|
+
* if (states.every(s => s.status === 'loaded')) {
|
|
191
|
+
* // Safe to rely on GTM being fully loaded
|
|
192
|
+
* }
|
|
193
|
+
* };
|
|
194
|
+
* ```
|
|
195
|
+
*/
|
|
24
196
|
declare const useGtmReady: () => (() => Promise<ScriptLoadState[]>);
|
|
197
|
+
/**
|
|
198
|
+
* Hook that returns a function to synchronously check if GTM is ready.
|
|
199
|
+
*
|
|
200
|
+
* **When to use**: When you need to check readiness without triggering re-renders,
|
|
201
|
+
* typically in event handlers or callbacks.
|
|
202
|
+
*
|
|
203
|
+
* **Comparison of GTM readiness hooks:**
|
|
204
|
+
* | Hook | Returns | Re-renders | Use Case |
|
|
205
|
+
* |------|---------|------------|----------|
|
|
206
|
+
* | `useGtmReady()` | `() => Promise` | No | Await in event handlers |
|
|
207
|
+
* | `useIsGtmReady()` | `() => boolean` | No | Synchronous checks in callbacks |
|
|
208
|
+
* | `useGtmInitialized()` | `boolean` | Yes | Conditional rendering |
|
|
209
|
+
*
|
|
210
|
+
* @returns Function that returns `true` if scripts loaded, `false` if still loading
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* ```tsx
|
|
214
|
+
* const checkReady = useIsGtmReady();
|
|
215
|
+
*
|
|
216
|
+
* const handleSubmit = () => {
|
|
217
|
+
* if (checkReady()) {
|
|
218
|
+
* // GTM is ready, proceed with tracking
|
|
219
|
+
* push({ event: 'form_submit' });
|
|
220
|
+
* }
|
|
221
|
+
* };
|
|
222
|
+
* ```
|
|
223
|
+
*/
|
|
224
|
+
declare const useIsGtmReady: () => (() => boolean);
|
|
225
|
+
/**
|
|
226
|
+
* Reactive hook that returns `true` when GTM scripts have finished loading.
|
|
227
|
+
*
|
|
228
|
+
* **When to use**: When you need to conditionally render UI based on GTM readiness.
|
|
229
|
+
* This hook triggers a re-render when the state changes.
|
|
230
|
+
*
|
|
231
|
+
* **Comparison of GTM readiness hooks:**
|
|
232
|
+
* | Hook | Returns | Re-renders | Use Case |
|
|
233
|
+
* |------|---------|------------|----------|
|
|
234
|
+
* | `useGtmReady()` | `() => Promise` | No | Await in event handlers |
|
|
235
|
+
* | `useIsGtmReady()` | `() => boolean` | No | Synchronous checks in callbacks |
|
|
236
|
+
* | `useGtmInitialized()` | `boolean` | Yes | Conditional rendering |
|
|
237
|
+
*
|
|
238
|
+
* @returns `true` if GTM is initialized, `false` otherwise (reactive)
|
|
239
|
+
*
|
|
240
|
+
* @example
|
|
241
|
+
* ```tsx
|
|
242
|
+
* const isInitialized = useGtmInitialized();
|
|
243
|
+
*
|
|
244
|
+
* if (!isInitialized) {
|
|
245
|
+
* return <LoadingSpinner />;
|
|
246
|
+
* }
|
|
247
|
+
*
|
|
248
|
+
* return <AnalyticsDashboard />;
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
251
|
+
declare const useGtmInitialized: () => boolean;
|
|
252
|
+
/**
|
|
253
|
+
* Result from the useGtmError hook.
|
|
254
|
+
*/
|
|
255
|
+
interface GtmErrorState {
|
|
256
|
+
/** Whether any scripts failed to load */
|
|
257
|
+
hasError: boolean;
|
|
258
|
+
/** Array of failed script states (status 'failed' or 'partial') */
|
|
259
|
+
failedScripts: ScriptLoadState[];
|
|
260
|
+
/** Convenience getter for the first error message, if any */
|
|
261
|
+
errorMessage: string | null;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Hook to capture GTM script load errors.
|
|
265
|
+
* Returns reactive state that updates when scripts fail to load.
|
|
266
|
+
*
|
|
267
|
+
* @example
|
|
268
|
+
* ```tsx
|
|
269
|
+
* const { hasError, failedScripts, errorMessage } = useGtmError();
|
|
270
|
+
*
|
|
271
|
+
* if (hasError) {
|
|
272
|
+
* console.error('GTM failed to load:', errorMessage);
|
|
273
|
+
* // Optionally show fallback UI or retry logic
|
|
274
|
+
* }
|
|
275
|
+
* ```
|
|
276
|
+
*/
|
|
277
|
+
declare const useGtmError: () => GtmErrorState;
|
|
278
|
+
/**
|
|
279
|
+
* Props for GtmErrorBoundary component.
|
|
280
|
+
*/
|
|
281
|
+
interface GtmErrorBoundaryProps {
|
|
282
|
+
children: ReactNode;
|
|
283
|
+
/** Fallback UI to render when an error occurs */
|
|
284
|
+
fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);
|
|
285
|
+
/** Callback invoked when an error is caught */
|
|
286
|
+
onError?: (error: Error, errorInfo: ErrorInfo) => void;
|
|
287
|
+
/** Whether to log errors to console (default: true in development) */
|
|
288
|
+
logErrors?: boolean;
|
|
289
|
+
}
|
|
290
|
+
interface GtmErrorBoundaryState {
|
|
291
|
+
hasError: boolean;
|
|
292
|
+
error: Error | null;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Error boundary component for GTM provider.
|
|
296
|
+
* Catches errors during GTM initialization and renders a fallback UI.
|
|
297
|
+
* Analytics and tracking will be disabled when an error occurs.
|
|
298
|
+
*/
|
|
299
|
+
declare class GtmErrorBoundary extends Component<GtmErrorBoundaryProps, GtmErrorBoundaryState> {
|
|
300
|
+
constructor(props: GtmErrorBoundaryProps);
|
|
301
|
+
static getDerivedStateFromError(error: Error): GtmErrorBoundaryState;
|
|
302
|
+
componentDidCatch(error: Error, errorInfo: ErrorInfo): void;
|
|
303
|
+
reset: () => void;
|
|
304
|
+
render(): ReactNode;
|
|
305
|
+
}
|
|
25
306
|
|
|
26
|
-
export { GtmConsentApi, GtmContextValue, GtmProvider, GtmProviderProps, useGtm, useGtmClient, useGtmConsent, useGtmPush, useGtmReady };
|
|
307
|
+
export { GtmConsentApi, GtmContextValue, GtmErrorBoundary, GtmErrorBoundaryProps, GtmErrorState, GtmProvider, GtmProviderProps, isSsr, useGtm, useGtmClient, useGtmConsent, useGtmError, useGtmInitialized, useGtmPush, useGtmReady, useHydrated, useIsGtmProviderPresent, useIsGtmReady };
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,229 @@
|
|
|
1
|
-
import { createContext, useEffect, useMemo,
|
|
1
|
+
import { createContext, useSyncExternalStore, useContext, useEffect, useMemo, useState, Component, useRef } from 'react';
|
|
2
2
|
import { createGtmClient } from '@jwiedeman/gtm-kit';
|
|
3
|
-
import { jsx } from 'react/jsx-runtime';
|
|
3
|
+
import { jsx, Fragment } from 'react/jsx-runtime';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// src/provider.tsx
|
|
6
|
+
var isSsr = () => typeof window === "undefined";
|
|
7
|
+
var useHydrated = () => {
|
|
8
|
+
return useSyncExternalStore(
|
|
9
|
+
// Subscribe function (no-op, state never changes after hydration)
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
11
|
+
() => () => {
|
|
12
|
+
},
|
|
13
|
+
// getSnapshot (client): always true after first render
|
|
14
|
+
() => true,
|
|
15
|
+
// getServerSnapshot (server): always false
|
|
16
|
+
() => false
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
var GtmContext = createContext(null);
|
|
20
|
+
var warnOnNestedProvider = () => {
|
|
21
|
+
if (process.env.NODE_ENV !== "production") {
|
|
22
|
+
console.warn(
|
|
23
|
+
"[gtm-kit/react] Nested GtmProvider detected. You should only have one GtmProvider at the root of your app. The nested provider will be ignored."
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
var warnOnConfigChange = (initialConfig, nextConfig) => {
|
|
28
|
+
if (process.env.NODE_ENV !== "production" && initialConfig !== nextConfig) {
|
|
29
|
+
console.warn(
|
|
30
|
+
"[gtm-kit/react] GtmProvider received new configuration; reconfiguration after mount is not supported. The initial configuration will continue to be used."
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
var extractContainerIds = (config) => {
|
|
35
|
+
const containers = config.containers;
|
|
36
|
+
const containerArray = Array.isArray(containers) ? containers : [containers];
|
|
37
|
+
return containerArray.map((container) => {
|
|
38
|
+
if (typeof container === "string") {
|
|
39
|
+
return container;
|
|
40
|
+
}
|
|
41
|
+
return container.id;
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
var warnOnOrphanedSsrScripts = (configuredContainers) => {
|
|
45
|
+
if (process.env.NODE_ENV !== "production" && typeof document !== "undefined") {
|
|
46
|
+
const gtmScripts = document.querySelectorAll('script[src*="googletagmanager.com/gtm.js"]');
|
|
47
|
+
gtmScripts.forEach((script) => {
|
|
48
|
+
const src = script.src;
|
|
49
|
+
const idMatch = src.match(/[?&]id=([^&]+)/);
|
|
50
|
+
const scriptContainerId = idMatch == null ? void 0 : idMatch[1];
|
|
51
|
+
if (scriptContainerId && !configuredContainers.includes(scriptContainerId)) {
|
|
52
|
+
console.warn(
|
|
53
|
+
`[gtm-kit/react] Found pre-rendered GTM script for container "${scriptContainerId}" that is not configured in GtmProvider. This may indicate a hydration mismatch between SSR and client-side rendering. Configure GtmProvider with containers: "${scriptContainerId}" to properly hydrate.`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
const gtmNoscripts = document.querySelectorAll('noscript iframe[src*="googletagmanager.com/ns.html"]');
|
|
58
|
+
gtmNoscripts.forEach((iframe) => {
|
|
59
|
+
const src = iframe.src;
|
|
60
|
+
const idMatch = src.match(/[?&]id=([^&]+)/);
|
|
61
|
+
const iframeContainerId = idMatch == null ? void 0 : idMatch[1];
|
|
62
|
+
if (iframeContainerId && !configuredContainers.includes(iframeContainerId)) {
|
|
63
|
+
console.warn(
|
|
64
|
+
`[gtm-kit/react] Found pre-rendered GTM noscript iframe for container "${iframeContainerId}" that is not configured in GtmProvider. If you pre-render noscript fallbacks on the server, ensure your GtmProvider has the same container ID.`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
var useStableClient = (config) => {
|
|
71
|
+
const clientRef = useRef();
|
|
72
|
+
const configRef = useRef();
|
|
73
|
+
if (!clientRef.current) {
|
|
74
|
+
clientRef.current = createGtmClient(config);
|
|
75
|
+
configRef.current = config;
|
|
76
|
+
} else if (configRef.current) {
|
|
77
|
+
warnOnConfigChange(configRef.current, config);
|
|
78
|
+
}
|
|
79
|
+
return clientRef.current;
|
|
80
|
+
};
|
|
81
|
+
var GtmProvider = ({ config, children }) => {
|
|
82
|
+
const existingContext = useContext(GtmContext);
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (existingContext) {
|
|
85
|
+
warnOnNestedProvider();
|
|
86
|
+
}
|
|
87
|
+
}, [existingContext]);
|
|
88
|
+
if (existingContext) {
|
|
89
|
+
return /* @__PURE__ */ jsx(Fragment, { children });
|
|
90
|
+
}
|
|
91
|
+
return /* @__PURE__ */ jsx(GtmProviderInner, { config, children });
|
|
92
|
+
};
|
|
93
|
+
var GtmProviderInner = ({ config, children }) => {
|
|
94
|
+
const client = useStableClient(config);
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
warnOnOrphanedSsrScripts(extractContainerIds(config));
|
|
97
|
+
client.init();
|
|
98
|
+
return () => {
|
|
99
|
+
client.teardown();
|
|
100
|
+
};
|
|
101
|
+
}, [client, config]);
|
|
102
|
+
const value = useMemo(
|
|
103
|
+
() => ({
|
|
104
|
+
client,
|
|
105
|
+
push: (value2) => client.push(value2),
|
|
106
|
+
setConsentDefaults: (state, options) => client.setConsentDefaults(state, options),
|
|
107
|
+
updateConsent: (state, options) => client.updateConsent(state, options),
|
|
108
|
+
isReady: () => client.isReady(),
|
|
109
|
+
whenReady: () => client.whenReady(),
|
|
110
|
+
onReady: (callback) => client.onReady(callback)
|
|
111
|
+
}),
|
|
112
|
+
[client]
|
|
113
|
+
);
|
|
114
|
+
return /* @__PURE__ */ jsx(GtmContext.Provider, { value, children });
|
|
115
|
+
};
|
|
116
|
+
var useGtmContext = () => {
|
|
117
|
+
const context = useContext(GtmContext);
|
|
118
|
+
if (!context) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
'[gtm-kit/react] useGtm() was called outside of a GtmProvider. Make sure to wrap your app with <GtmProvider config={{ containers: "GTM-XXXXXX" }}>.'
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
return context;
|
|
124
|
+
};
|
|
125
|
+
var useGtm = useGtmContext;
|
|
126
|
+
var useIsGtmProviderPresent = () => {
|
|
127
|
+
const context = useContext(GtmContext);
|
|
128
|
+
return context !== null;
|
|
129
|
+
};
|
|
130
|
+
var useGtmClient = () => {
|
|
131
|
+
return useGtmContext().client;
|
|
132
|
+
};
|
|
133
|
+
var useGtmPush = () => {
|
|
134
|
+
return useGtmContext().push;
|
|
135
|
+
};
|
|
136
|
+
var useGtmConsent = () => {
|
|
137
|
+
const { setConsentDefaults, updateConsent } = useGtmContext();
|
|
138
|
+
return useMemo(() => ({ setConsentDefaults, updateConsent }), [setConsentDefaults, updateConsent]);
|
|
139
|
+
};
|
|
140
|
+
var useGtmReady = () => {
|
|
141
|
+
const { whenReady } = useGtmContext();
|
|
142
|
+
return whenReady;
|
|
143
|
+
};
|
|
144
|
+
var useIsGtmReady = () => {
|
|
145
|
+
const { isReady } = useGtmContext();
|
|
146
|
+
return isReady;
|
|
147
|
+
};
|
|
148
|
+
var useGtmInitialized = () => {
|
|
149
|
+
const { isReady, onReady } = useGtmContext();
|
|
150
|
+
const [initialized, setInitialized] = useState(() => isReady());
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
if (isReady()) {
|
|
153
|
+
setInitialized(true);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const unsubscribe = onReady(() => {
|
|
157
|
+
setInitialized(true);
|
|
158
|
+
});
|
|
159
|
+
return unsubscribe;
|
|
160
|
+
}, [isReady, onReady]);
|
|
161
|
+
return initialized;
|
|
162
|
+
};
|
|
163
|
+
var useGtmError = () => {
|
|
164
|
+
const { onReady } = useGtmContext();
|
|
165
|
+
const [errorState, setErrorState] = useState({
|
|
166
|
+
hasError: false,
|
|
167
|
+
failedScripts: [],
|
|
168
|
+
errorMessage: null
|
|
169
|
+
});
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
const unsubscribe = onReady((states) => {
|
|
172
|
+
var _a, _b;
|
|
173
|
+
const failedScripts = states.filter((s) => s.status === "failed" || s.status === "partial");
|
|
174
|
+
if (failedScripts.length > 0) {
|
|
175
|
+
const firstError = (_b = (_a = failedScripts.find((s) => s.error)) == null ? void 0 : _a.error) != null ? _b : null;
|
|
176
|
+
setErrorState({
|
|
177
|
+
hasError: true,
|
|
178
|
+
failedScripts,
|
|
179
|
+
errorMessage: firstError
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
return unsubscribe;
|
|
184
|
+
}, [onReady]);
|
|
185
|
+
return errorState;
|
|
186
|
+
};
|
|
187
|
+
var GtmErrorBoundary = class extends Component {
|
|
188
|
+
constructor(props) {
|
|
189
|
+
super(props);
|
|
190
|
+
this.reset = () => {
|
|
191
|
+
this.setState({ hasError: false, error: null });
|
|
192
|
+
};
|
|
193
|
+
this.state = { hasError: false, error: null };
|
|
194
|
+
}
|
|
195
|
+
static getDerivedStateFromError(error) {
|
|
196
|
+
return { hasError: true, error };
|
|
197
|
+
}
|
|
198
|
+
componentDidCatch(error, errorInfo) {
|
|
199
|
+
const { onError, logErrors = process.env.NODE_ENV !== "production" } = this.props;
|
|
200
|
+
if (logErrors) {
|
|
201
|
+
console.error("[gtm-kit/react] Error caught by GtmErrorBoundary:", error);
|
|
202
|
+
console.error("[gtm-kit/react] Component stack:", errorInfo.componentStack);
|
|
203
|
+
}
|
|
204
|
+
if (onError) {
|
|
205
|
+
try {
|
|
206
|
+
onError(error, errorInfo);
|
|
207
|
+
} catch (e) {
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
render() {
|
|
212
|
+
const { hasError, error } = this.state;
|
|
213
|
+
const { children, fallback } = this.props;
|
|
214
|
+
if (hasError && error) {
|
|
215
|
+
if (fallback === void 0) {
|
|
216
|
+
return children;
|
|
217
|
+
}
|
|
218
|
+
if (typeof fallback === "function") {
|
|
219
|
+
return fallback(error, this.reset);
|
|
220
|
+
}
|
|
221
|
+
return fallback;
|
|
222
|
+
}
|
|
223
|
+
return children;
|
|
224
|
+
}
|
|
225
|
+
};
|
|
6
226
|
|
|
7
|
-
export {
|
|
227
|
+
export { GtmErrorBoundary, GtmProvider, isSsr, useGtm, useGtmClient, useGtmConsent, useGtmError, useGtmInitialized, useGtmPush, useGtmReady, useHydrated, useIsGtmProviderPresent, useIsGtmReady };
|
|
8
228
|
//# sourceMappingURL=out.js.map
|
|
9
229
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/provider.tsx"],"names":["createContext","useContext","useEffect","useMemo","useRef","createGtmClient","jsx","GtmContext","warnOnConfigChange","initialConfig","nextConfig","useStableClient","config","clientRef","configRef","GtmProvider","children","client","value","state","options","callback","useGtmContext","context","useGtm","useGtmClient","useGtmPush","useGtmConsent","setConsentDefaults","updateConsent","useGtmReady","whenReady"],"mappings":"AAAA,OAAS,iBAAAA,EAAe,cAAAC,EAAY,aAAAC,EAAW,WAAAC,EAAS,UAAAC,MAAgD,QACxG,OACE,mBAAAC,MAOK,qBAmEE,cAAAC,MAAA,oBA/CT,IAAMC,EAAaP,EAAsC,IAAI,EAEvDQ,EAAqB,CAACC,EAAuCC,IAA6C,CAC1G,QAAQ,IAAI,WAAa,cAAgBD,IAAkBC,GAC7D,QAAQ,KACN,2JAEF,CAEJ,EAEMC,EAAmBC,GAA8C,CACrE,IAAMC,EAAYT,EAAkB,EAC9BU,EAAYV,EAA+B,EAEjD,OAAKS,EAAU,QAGJC,EAAU,SACnBN,EAAmBM,EAAU,QAASF,CAAM,GAH5CC,EAAU,QAAUR,EAAgBO,CAAM,EAC1CE,EAAU,QAAUF,GAKfC,EAAU,OACnB,EAEaE,EAAc,CAAC,CAAE,OAAAH,EAAQ,SAAAI,CAAS,IAAqC,CAClF,IAAMC,EAASN,EAAgBC,CAAM,EAErCV,EAAU,KACRe,EAAO,KAAK,EACL,IAAM,CACXA,EAAO,SAAS,CAClB,GACC,CAACA,CAAM,CAAC,EAEX,IAAMC,EAAQf,EACZ,KAAO,CACL,OAAAc,EACA,KAAOC,GAAUD,EAAO,KAAKC,CAAK,EAClC,mBAAoB,CAACC,EAAOC,IAAYH,EAAO,mBAAmBE,EAAOC,CAAO,EAChF,cAAe,CAACD,EAAOC,IAAYH,EAAO,cAAcE,EAAOC,CAAO,EACtE,UAAW,IAAMH,EAAO,UAAU,EAClC,QAAUI,GAAaJ,EAAO,QAAQI,CAAQ,CAChD,GACA,CAACJ,CAAM,CACT,EAEA,OAAOX,EAACC,EAAW,SAAX,CAAoB,MAAOW,EAAQ,SAAAF,EAAS,CACtD,EAEMM,EAAgB,IAAuB,CAC3C,IAAMC,EAAUtB,EAAWM,CAAU,EACrC,GAAI,CAACgB,EACH,MAAM,IAAI,MAAM,yDAAyD,EAE3E,OAAOA,CACT,EAEaC,EAASF,EAETG,EAAe,IACnBH,EAAc,EAAE,OAGZI,EAAa,IACjBJ,EAAc,EAAE,KAGZK,EAAgB,IAAqB,CAChD,GAAM,CAAE,mBAAAC,EAAoB,cAAAC,CAAc,EAAIP,EAAc,EAC5D,OAAOnB,EAAQ,KAAO,CAAE,mBAAAyB,EAAoB,cAAAC,CAAc,GAAI,CAACD,EAAoBC,CAAa,CAAC,CACnG,EAEaC,EAAc,IAA0C,CACnE,GAAM,CAAE,UAAAC,CAAU,EAAIT,EAAc,EACpC,OAAOS,CACT","sourcesContent":["import { createContext, useContext, useEffect, useMemo, useRef, type PropsWithChildren, type JSX } from 'react';\nimport {\n createGtmClient,\n type ConsentRegionOptions,\n type ConsentState,\n type CreateGtmClientOptions,\n type DataLayerValue,\n type GtmClient,\n type ScriptLoadState\n} from '@jwiedeman/gtm-kit';\n\nexport interface GtmProviderProps extends PropsWithChildren {\n config: CreateGtmClientOptions;\n}\n\nexport interface GtmContextValue {\n client: GtmClient;\n push: (value: DataLayerValue) => void;\n setConsentDefaults: (state: ConsentState, options?: ConsentRegionOptions) => void;\n updateConsent: (state: ConsentState, options?: ConsentRegionOptions) => void;\n whenReady: () => Promise<ScriptLoadState[]>;\n onReady: (callback: (state: ScriptLoadState[]) => void) => () => void;\n}\n\nexport interface GtmConsentApi {\n setConsentDefaults: (state: ConsentState, options?: ConsentRegionOptions) => void;\n updateConsent: (state: ConsentState, options?: ConsentRegionOptions) => void;\n}\n\nconst GtmContext = createContext<GtmContextValue | null>(null);\n\nconst warnOnConfigChange = (initialConfig: CreateGtmClientOptions, nextConfig: CreateGtmClientOptions): void => {\n if (process.env.NODE_ENV !== 'production' && initialConfig !== nextConfig) {\n console.warn(\n '[react-gtm-kit] GtmProvider received new configuration; reconfiguration after mount is not supported. ' +\n 'The initial configuration will continue to be used.'\n );\n }\n};\n\nconst useStableClient = (config: CreateGtmClientOptions): GtmClient => {\n const clientRef = useRef<GtmClient>();\n const configRef = useRef<CreateGtmClientOptions>();\n\n if (!clientRef.current) {\n clientRef.current = createGtmClient(config);\n configRef.current = config;\n } else if (configRef.current) {\n warnOnConfigChange(configRef.current, config);\n }\n\n return clientRef.current!;\n};\n\nexport const GtmProvider = ({ config, children }: GtmProviderProps): JSX.Element => {\n const client = useStableClient(config);\n\n useEffect(() => {\n client.init();\n return () => {\n client.teardown();\n };\n }, [client]);\n\n const value = useMemo<GtmContextValue>(\n () => ({\n client,\n push: (value) => client.push(value),\n setConsentDefaults: (state, options) => client.setConsentDefaults(state, options),\n updateConsent: (state, options) => client.updateConsent(state, options),\n whenReady: () => client.whenReady(),\n onReady: (callback) => client.onReady(callback)\n }),\n [client]\n );\n\n return <GtmContext.Provider value={value}>{children}</GtmContext.Provider>;\n};\n\nconst useGtmContext = (): GtmContextValue => {\n const context = useContext(GtmContext);\n if (!context) {\n throw new Error('useGtm hook must be used within a GtmProvider instance.');\n }\n return context;\n};\n\nexport const useGtm = useGtmContext;\n\nexport const useGtmClient = (): GtmClient => {\n return useGtmContext().client;\n};\n\nexport const useGtmPush = (): ((value: DataLayerValue) => void) => {\n return useGtmContext().push;\n};\n\nexport const useGtmConsent = (): GtmConsentApi => {\n const { setConsentDefaults, updateConsent } = useGtmContext();\n return useMemo(() => ({ setConsentDefaults, updateConsent }), [setConsentDefaults, updateConsent]);\n};\n\nexport const useGtmReady = (): (() => Promise<ScriptLoadState[]>) => {\n const { whenReady } = useGtmContext();\n return whenReady;\n};\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/provider.tsx"],"names":["value"],"mappings":"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","sourcesContent":["import {\n Component,\n createContext,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n useSyncExternalStore,\n type ErrorInfo,\n type PropsWithChildren,\n type ReactNode,\n type JSX\n} from 'react';\nimport {\n createGtmClient,\n type ConsentRegionOptions,\n type ConsentState,\n type CreateGtmClientOptions,\n type DataLayerValue,\n type GtmClient,\n type ScriptLoadState\n} from '@jwiedeman/gtm-kit';\n\n/**\n * Check if code is running in a server-side rendering environment.\n * Returns true if window is undefined (Node.js/SSR), false in browser.\n */\nexport const isSsr = (): boolean => typeof window === 'undefined';\n\n/**\n * Hook that returns false during SSR and initial hydration, then true after hydration completes.\n * Use this to prevent hydration mismatches when rendering GTM-dependent content.\n *\n * @example\n * ```tsx\n * const isHydrated = useHydrated();\n *\n * // Safe: won't cause hydration mismatch\n * return isHydrated ? <DynamicContent /> : <StaticPlaceholder />;\n * ```\n */\nexport const useHydrated = (): boolean => {\n // useSyncExternalStore with getServerSnapshot ensures consistent SSR/hydration\n return useSyncExternalStore(\n // Subscribe function (no-op, state never changes after hydration)\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n () => () => {},\n // getSnapshot (client): always true after first render\n () => true,\n // getServerSnapshot (server): always false\n () => false\n );\n};\n\nexport interface GtmProviderProps extends PropsWithChildren {\n config: CreateGtmClientOptions;\n}\n\nexport interface GtmContextValue {\n client: GtmClient;\n push: (value: DataLayerValue) => void;\n setConsentDefaults: (state: ConsentState, options?: ConsentRegionOptions) => void;\n updateConsent: (state: ConsentState, options?: ConsentRegionOptions) => void;\n /** Synchronously check if all GTM scripts have finished loading */\n isReady: () => boolean;\n whenReady: () => Promise<ScriptLoadState[]>;\n onReady: (callback: (state: ScriptLoadState[]) => void) => () => void;\n}\n\nexport interface GtmConsentApi {\n setConsentDefaults: (state: ConsentState, options?: ConsentRegionOptions) => void;\n updateConsent: (state: ConsentState, options?: ConsentRegionOptions) => void;\n}\n\nconst GtmContext = createContext<GtmContextValue | null>(null);\n\nconst warnOnNestedProvider = (): void => {\n if (process.env.NODE_ENV !== 'production') {\n console.warn(\n '[gtm-kit/react] Nested GtmProvider detected. You should only have one GtmProvider at the root of your app. ' +\n 'The nested provider will be ignored.'\n );\n }\n};\n\nconst warnOnConfigChange = (initialConfig: CreateGtmClientOptions, nextConfig: CreateGtmClientOptions): void => {\n if (process.env.NODE_ENV !== 'production' && initialConfig !== nextConfig) {\n console.warn(\n '[gtm-kit/react] GtmProvider received new configuration; reconfiguration after mount is not supported. ' +\n 'The initial configuration will continue to be used.'\n );\n }\n};\n\n/**\n * Extracts container IDs from the provider config.\n */\nconst extractContainerIds = (config: CreateGtmClientOptions): string[] => {\n const containers = config.containers;\n const containerArray = Array.isArray(containers) ? containers : [containers];\n\n return containerArray.map((container) => {\n if (typeof container === 'string') {\n return container;\n }\n return container.id;\n });\n};\n\n/**\n * Warns in development if there are orphaned SSR-rendered GTM scripts without a matching client.\n * This helps developers identify hydration mismatches where GTM was rendered on the server\n * but the GtmProvider config doesn't match or is missing.\n */\nconst warnOnOrphanedSsrScripts = (configuredContainers: string[]): void => {\n if (process.env.NODE_ENV !== 'production' && typeof document !== 'undefined') {\n // Find all GTM scripts on the page\n const gtmScripts = document.querySelectorAll('script[src*=\"googletagmanager.com/gtm.js\"]');\n\n gtmScripts.forEach((script) => {\n const src = (script as HTMLScriptElement).src;\n const idMatch = src.match(/[?&]id=([^&]+)/);\n const scriptContainerId = idMatch?.[1];\n\n if (scriptContainerId && !configuredContainers.includes(scriptContainerId)) {\n console.warn(\n `[gtm-kit/react] Found pre-rendered GTM script for container \"${scriptContainerId}\" that is not configured in GtmProvider. ` +\n 'This may indicate a hydration mismatch between SSR and client-side rendering. ' +\n `Configure GtmProvider with containers: \"${scriptContainerId}\" to properly hydrate.`\n );\n }\n });\n\n // Also check for noscript iframes\n const gtmNoscripts = document.querySelectorAll('noscript iframe[src*=\"googletagmanager.com/ns.html\"]');\n gtmNoscripts.forEach((iframe) => {\n const src = (iframe as HTMLIFrameElement).src;\n const idMatch = src.match(/[?&]id=([^&]+)/);\n const iframeContainerId = idMatch?.[1];\n\n if (iframeContainerId && !configuredContainers.includes(iframeContainerId)) {\n console.warn(\n `[gtm-kit/react] Found pre-rendered GTM noscript iframe for container \"${iframeContainerId}\" that is not configured in GtmProvider. ` +\n 'If you pre-render noscript fallbacks on the server, ensure your GtmProvider has the same container ID.'\n );\n }\n });\n }\n};\n\nconst useStableClient = (config: CreateGtmClientOptions): GtmClient => {\n const clientRef = useRef<GtmClient>();\n const configRef = useRef<CreateGtmClientOptions>();\n\n if (!clientRef.current) {\n clientRef.current = createGtmClient(config);\n configRef.current = config;\n } else if (configRef.current) {\n warnOnConfigChange(configRef.current, config);\n }\n\n return clientRef.current!;\n};\n\n/**\n * GTM Provider component that initializes Google Tag Manager for your React app.\n *\n * ## SSR/Hydration Behavior\n *\n * This provider is SSR-safe and handles hydration correctly:\n * - **Server**: No GTM initialization occurs (no window/document access)\n * - **Client Hydration**: GTM initializes only after hydration via useEffect\n * - **No Hydration Mismatch**: Provider renders the same on server and client\n *\n * ## Usage with SSR Frameworks\n *\n * ```tsx\n * // Next.js, Remix, etc.\n * export default function App({ children }) {\n * return (\n * <GtmProvider config={{ containers: 'GTM-XXXXXX' }}>\n * {children}\n * </GtmProvider>\n * );\n * }\n * ```\n *\n * ## Preventing Hydration Mismatches\n *\n * If you render different content based on GTM state, use `useHydrated`:\n *\n * ```tsx\n * const isHydrated = useHydrated();\n * const isGtmReady = useGtmInitialized();\n *\n * // Safe: both server and client initially render the placeholder\n * if (!isHydrated) return <Placeholder />;\n * return isGtmReady ? <TrackedContent /> : <LoadingContent />;\n * ```\n */\nexport const GtmProvider = ({ config, children }: GtmProviderProps): JSX.Element => {\n const existingContext = useContext(GtmContext);\n\n // Warn if we're inside another GtmProvider (nested providers)\n useEffect(() => {\n if (existingContext) {\n warnOnNestedProvider();\n }\n }, [existingContext]);\n\n // If nested, just pass through children without creating a new context\n if (existingContext) {\n return <>{children}</>;\n }\n\n return <GtmProviderInner config={config}>{children}</GtmProviderInner>;\n};\n\nconst GtmProviderInner = ({ config, children }: GtmProviderProps): JSX.Element => {\n const client = useStableClient(config);\n\n useEffect(() => {\n // Check for orphaned SSR scripts before initializing\n warnOnOrphanedSsrScripts(extractContainerIds(config));\n\n client.init();\n return () => {\n client.teardown();\n };\n }, [client, config]);\n\n const value = useMemo<GtmContextValue>(\n () => ({\n client,\n push: (value) => client.push(value),\n setConsentDefaults: (state, options) => client.setConsentDefaults(state, options),\n updateConsent: (state, options) => client.updateConsent(state, options),\n isReady: () => client.isReady(),\n whenReady: () => client.whenReady(),\n onReady: (callback) => client.onReady(callback)\n }),\n [client]\n );\n\n return <GtmContext.Provider value={value}>{children}</GtmContext.Provider>;\n};\n\nconst useGtmContext = (): GtmContextValue => {\n const context = useContext(GtmContext);\n if (!context) {\n throw new Error(\n '[gtm-kit/react] useGtm() was called outside of a GtmProvider. ' +\n 'Make sure to wrap your app with <GtmProvider config={{ containers: \"GTM-XXXXXX\" }}>.'\n );\n }\n return context;\n};\n\n/**\n * Hook to access the full GTM context. Throws if used outside GtmProvider.\n *\n * For most use cases, prefer the specific hooks:\n * - `useGtmPush()` - Push events to dataLayer\n * - `useGtmConsent()` - Manage consent state\n * - `useGtmClient()` - Access the raw GTM client\n *\n * @throws Error if called outside of GtmProvider\n *\n * @example\n * ```tsx\n * const { push, client, isReady, whenReady } = useGtm();\n * ```\n */\nexport const useGtm = useGtmContext;\n\n/**\n * Hook to check if GtmProvider is present without throwing.\n *\n * Useful for components that may be rendered before the provider mounts,\n * or for optional GTM integration.\n *\n * @returns `true` if inside GtmProvider, `false` otherwise\n *\n * @example\n * ```tsx\n * const hasProvider = useIsGtmProviderPresent();\n *\n * const handleClick = () => {\n * if (hasProvider) {\n * // Safe to use GTM hooks\n * }\n * };\n * ```\n */\nexport const useIsGtmProviderPresent = (): boolean => {\n const context = useContext(GtmContext);\n return context !== null;\n};\n\n/**\n * Hook to access the raw GTM client instance.\n *\n * @throws Error if called outside of GtmProvider\n * @returns The GTM client instance\n *\n * @example\n * ```tsx\n * const client = useGtmClient();\n *\n * // Access low-level APIs\n * const diagnostics = client.getDiagnostics();\n * ```\n */\nexport const useGtmClient = (): GtmClient => {\n return useGtmContext().client;\n};\n\n/**\n * Hook to get the push function for sending events to the dataLayer.\n *\n * This is the most commonly used hook for tracking events.\n *\n * @throws Error if called outside of GtmProvider\n * @returns Function to push values to the dataLayer\n *\n * @example\n * ```tsx\n * const push = useGtmPush();\n *\n * const handleAddToCart = (product) => {\n * push({\n * event: 'add_to_cart',\n * ecommerce: {\n * items: [{ item_id: product.id, item_name: product.name }]\n * }\n * });\n * };\n * ```\n */\nexport const useGtmPush = (): ((value: DataLayerValue) => void) => {\n return useGtmContext().push;\n};\n\n/**\n * Hook to access consent management functions.\n *\n * @throws Error if called outside of GtmProvider\n * @returns Object with `setConsentDefaults` and `updateConsent` functions\n *\n * @example\n * ```tsx\n * const { updateConsent } = useGtmConsent();\n *\n * const handleAcceptCookies = () => {\n * updateConsent({\n * ad_storage: 'granted',\n * analytics_storage: 'granted'\n * });\n * };\n * ```\n */\nexport const useGtmConsent = (): GtmConsentApi => {\n const { setConsentDefaults, updateConsent } = useGtmContext();\n return useMemo(() => ({ setConsentDefaults, updateConsent }), [setConsentDefaults, updateConsent]);\n};\n\n/**\n * Hook that returns the `whenReady` promise function.\n *\n * **When to use**: When you need to await GTM script loading before taking an action.\n * The returned function returns a Promise that resolves when scripts finish loading.\n *\n * **Comparison of GTM readiness hooks:**\n * | Hook | Returns | Re-renders | Use Case |\n * |------|---------|------------|----------|\n * | `useGtmReady()` | `() => Promise` | No | Await in event handlers |\n * | `useIsGtmReady()` | `() => boolean` | No | Synchronous checks in callbacks |\n * | `useGtmInitialized()` | `boolean` | Yes | Conditional rendering |\n *\n * @returns Function that returns a Promise resolving to script load states\n *\n * @example\n * ```tsx\n * const whenReady = useGtmReady();\n *\n * const handleClick = async () => {\n * const states = await whenReady();\n * if (states.every(s => s.status === 'loaded')) {\n * // Safe to rely on GTM being fully loaded\n * }\n * };\n * ```\n */\nexport const useGtmReady = (): (() => Promise<ScriptLoadState[]>) => {\n const { whenReady } = useGtmContext();\n return whenReady;\n};\n\n/**\n * Hook that returns a function to synchronously check if GTM is ready.\n *\n * **When to use**: When you need to check readiness without triggering re-renders,\n * typically in event handlers or callbacks.\n *\n * **Comparison of GTM readiness hooks:**\n * | Hook | Returns | Re-renders | Use Case |\n * |------|---------|------------|----------|\n * | `useGtmReady()` | `() => Promise` | No | Await in event handlers |\n * | `useIsGtmReady()` | `() => boolean` | No | Synchronous checks in callbacks |\n * | `useGtmInitialized()` | `boolean` | Yes | Conditional rendering |\n *\n * @returns Function that returns `true` if scripts loaded, `false` if still loading\n *\n * @example\n * ```tsx\n * const checkReady = useIsGtmReady();\n *\n * const handleSubmit = () => {\n * if (checkReady()) {\n * // GTM is ready, proceed with tracking\n * push({ event: 'form_submit' });\n * }\n * };\n * ```\n */\nexport const useIsGtmReady = (): (() => boolean) => {\n const { isReady } = useGtmContext();\n return isReady;\n};\n\n/**\n * Reactive hook that returns `true` when GTM scripts have finished loading.\n *\n * **When to use**: When you need to conditionally render UI based on GTM readiness.\n * This hook triggers a re-render when the state changes.\n *\n * **Comparison of GTM readiness hooks:**\n * | Hook | Returns | Re-renders | Use Case |\n * |------|---------|------------|----------|\n * | `useGtmReady()` | `() => Promise` | No | Await in event handlers |\n * | `useIsGtmReady()` | `() => boolean` | No | Synchronous checks in callbacks |\n * | `useGtmInitialized()` | `boolean` | Yes | Conditional rendering |\n *\n * @returns `true` if GTM is initialized, `false` otherwise (reactive)\n *\n * @example\n * ```tsx\n * const isInitialized = useGtmInitialized();\n *\n * if (!isInitialized) {\n * return <LoadingSpinner />;\n * }\n *\n * return <AnalyticsDashboard />;\n * ```\n */\nexport const useGtmInitialized = (): boolean => {\n const { isReady, onReady } = useGtmContext();\n const [initialized, setInitialized] = useState(() => isReady());\n\n useEffect(() => {\n // Already initialized on mount\n if (isReady()) {\n setInitialized(true);\n return;\n }\n\n // Subscribe to ready event\n const unsubscribe = onReady(() => {\n setInitialized(true);\n });\n\n return unsubscribe;\n }, [isReady, onReady]);\n\n return initialized;\n};\n\n/**\n * Result from the useGtmError hook.\n */\nexport interface GtmErrorState {\n /** Whether any scripts failed to load */\n hasError: boolean;\n /** Array of failed script states (status 'failed' or 'partial') */\n failedScripts: ScriptLoadState[];\n /** Convenience getter for the first error message, if any */\n errorMessage: string | null;\n}\n\n/**\n * Hook to capture GTM script load errors.\n * Returns reactive state that updates when scripts fail to load.\n *\n * @example\n * ```tsx\n * const { hasError, failedScripts, errorMessage } = useGtmError();\n *\n * if (hasError) {\n * console.error('GTM failed to load:', errorMessage);\n * // Optionally show fallback UI or retry logic\n * }\n * ```\n */\nexport const useGtmError = (): GtmErrorState => {\n const { onReady } = useGtmContext();\n const [errorState, setErrorState] = useState<GtmErrorState>({\n hasError: false,\n failedScripts: [],\n errorMessage: null\n });\n\n useEffect(() => {\n const unsubscribe = onReady((states) => {\n const failedScripts = states.filter((s) => s.status === 'failed' || s.status === 'partial');\n\n if (failedScripts.length > 0) {\n const firstError = failedScripts.find((s) => s.error)?.error ?? null;\n setErrorState({\n hasError: true,\n failedScripts,\n errorMessage: firstError\n });\n }\n });\n\n return unsubscribe;\n }, [onReady]);\n\n return errorState;\n};\n\n/**\n * Props for GtmErrorBoundary component.\n */\nexport interface GtmErrorBoundaryProps {\n children: ReactNode;\n /** Fallback UI to render when an error occurs */\n fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);\n /** Callback invoked when an error is caught */\n onError?: (error: Error, errorInfo: ErrorInfo) => void;\n /** Whether to log errors to console (default: true in development) */\n logErrors?: boolean;\n}\n\ninterface GtmErrorBoundaryState {\n hasError: boolean;\n error: Error | null;\n}\n\n/**\n * Error boundary component for GTM provider.\n * Catches errors during GTM initialization and renders a fallback UI.\n * Analytics and tracking will be disabled when an error occurs.\n */\nexport class GtmErrorBoundary extends Component<GtmErrorBoundaryProps, GtmErrorBoundaryState> {\n constructor(props: GtmErrorBoundaryProps) {\n super(props);\n this.state = { hasError: false, error: null };\n }\n\n static getDerivedStateFromError(error: Error): GtmErrorBoundaryState {\n return { hasError: true, error };\n }\n\n componentDidCatch(error: Error, errorInfo: ErrorInfo): void {\n const { onError, logErrors = process.env.NODE_ENV !== 'production' } = this.props;\n\n if (logErrors) {\n console.error('[gtm-kit/react] Error caught by GtmErrorBoundary:', error);\n console.error('[gtm-kit/react] Component stack:', errorInfo.componentStack);\n }\n\n if (onError) {\n try {\n onError(error, errorInfo);\n } catch {\n // Ignore callback errors\n }\n }\n }\n\n reset = (): void => {\n this.setState({ hasError: false, error: null });\n };\n\n render(): ReactNode {\n const { hasError, error } = this.state;\n const { children, fallback } = this.props;\n\n if (hasError && error) {\n if (fallback === undefined) {\n // Default: render children without GTM (silent fallback)\n return children;\n }\n\n if (typeof fallback === 'function') {\n return fallback(error, this.reset);\n }\n\n return fallback;\n }\n\n return children;\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jwiedeman/gtm-kit-react",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "React hooks and provider for GTM Kit - Google Tag Manager integration. Supports React 16.8+.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"typecheck": "tsc --noEmit"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@jwiedeman/gtm-kit": "^1.
|
|
52
|
+
"@jwiedeman/gtm-kit": "^1.2.0"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|