@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 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
- useEffect(() => {
133
- whenReady().then(() => {
134
- console.log('GTM is ready!');
135
- });
136
- }, [whenReady]);
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
- var u=react.createContext(null),d=(t,n)=>{process.env.NODE_ENV!=="production"&&t!==n&&console.warn("[react-gtm-kit] GtmProvider received new configuration; reconfiguration after mount is not supported. The initial configuration will continue to be used.");},G=t=>{let n=react.useRef(),e=react.useRef();return n.current?e.current&&d(e.current,t):(n.current=gtmKit.createGtmClient(t),e.current=t),n.current},f=({config:t,children:n})=>{let e=G(t);react.useEffect(()=>(e.init(),()=>{e.teardown();}),[e]);let p=react.useMemo(()=>({client:e,push:o=>e.push(o),setConsentDefaults:(o,r)=>e.setConsentDefaults(o,r),updateConsent:(o,r)=>e.updateConsent(o,r),whenReady:()=>e.whenReady(),onReady:o=>e.onReady(o)}),[e]);return jsxRuntime.jsx(u.Provider,{value:p,children:n})},s=()=>{let t=react.useContext(u);if(!t)throw new Error("useGtm hook must be used within a GtmProvider instance.");return t},v=s,x=()=>s().client,y=()=>s().push,h=()=>{let{setConsentDefaults:t,updateConsent:n}=s();return react.useMemo(()=>({setConsentDefaults:t,updateConsent:n}),[t,n])},P=()=>{let{whenReady:t}=s();return t};
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.GtmProvider = f;
10
- exports.useGtm = v;
11
- exports.useGtmClient = x;
12
- exports.useGtmConsent = h;
13
- exports.useGtmPush = y;
14
- exports.useGtmReady = P;
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
@@ -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, useRef, useContext } from 'react';
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
- var u=createContext(null),d=(t,n)=>{process.env.NODE_ENV!=="production"&&t!==n&&console.warn("[react-gtm-kit] GtmProvider received new configuration; reconfiguration after mount is not supported. The initial configuration will continue to be used.");},G=t=>{let n=useRef(),e=useRef();return n.current?e.current&&d(e.current,t):(n.current=createGtmClient(t),e.current=t),n.current},f=({config:t,children:n})=>{let e=G(t);useEffect(()=>(e.init(),()=>{e.teardown();}),[e]);let p=useMemo(()=>({client:e,push:o=>e.push(o),setConsentDefaults:(o,r)=>e.setConsentDefaults(o,r),updateConsent:(o,r)=>e.updateConsent(o,r),whenReady:()=>e.whenReady(),onReady:o=>e.onReady(o)}),[e]);return jsx(u.Provider,{value:p,children:n})},s=()=>{let t=useContext(u);if(!t)throw new Error("useGtm hook must be used within a GtmProvider instance.");return t},v=s,x=()=>s().client,y=()=>s().push,h=()=>{let{setConsentDefaults:t,updateConsent:n}=s();return useMemo(()=>({setConsentDefaults:t,updateConsent:n}),[t,n])},P=()=>{let{whenReady:t}=s();return t};
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 { f as GtmProvider, v as useGtm, x as useGtmClient, h as useGtmConsent, y as useGtmPush, P as useGtmReady };
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.1.6",
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.0.0"
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"