@ninetailed/experience.js-react 7.15.1 → 7.16.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/index.cjs.js CHANGED
@@ -153,32 +153,34 @@ const usePersonalize = (baseline, variants, options = {
153
153
  };
154
154
 
155
155
  /**
156
- * Hook to access a Ninetailed variable flag with built-in tracking.
156
+ * Hook to access a Ninetailed variable flag with manual tracking control.
157
157
  */
158
- function useFlag(flagKey, defaultValue, options = {}) {
158
+ function useFlagWithManualTracking(flagKey, defaultValue) {
159
159
  const ninetailed = useNinetailed();
160
- const lastProcessedState = React.useRef(null);
161
- const defaultValueRef = React.useRef(defaultValue);
162
160
  const flagKeyRef = React.useRef(flagKey);
161
+ const defaultValueRef = React.useRef(defaultValue);
162
+ const lastProcessedState = React.useRef(null);
163
+ const changeRef = React.useRef(null);
163
164
  const [result, setResult] = React.useState({
164
165
  value: defaultValue,
165
166
  status: 'loading',
166
167
  error: null
167
168
  });
168
- // Reset on input change
169
+ // Reset if inputs change
169
170
  React.useEffect(() => {
170
171
  if (!radash.isEqual(defaultValueRef.current, defaultValue) || flagKeyRef.current !== flagKey) {
171
172
  defaultValueRef.current = defaultValue;
172
173
  flagKeyRef.current = flagKey;
174
+ lastProcessedState.current = null;
175
+ changeRef.current = null;
173
176
  setResult({
174
177
  value: defaultValue,
175
178
  status: 'loading',
176
179
  error: null
177
180
  });
178
- lastProcessedState.current = null;
179
181
  }
180
182
  }, [flagKey, defaultValue]);
181
- // Track changes
183
+ // Listen for personalization state changes
182
184
  React.useEffect(() => {
183
185
  const unsubscribe = ninetailed.onChangesChange(changesState => {
184
186
  if (lastProcessedState.current && radash.isEqual(lastProcessedState.current, changesState)) {
@@ -201,46 +203,59 @@ function useFlag(flagKey, defaultValue, options = {}) {
201
203
  });
202
204
  return;
203
205
  }
204
- try {
205
- const change = changesState.changes.find(change => change.key === flagKeyRef.current);
206
- if (change && change.type === experience_jsShared.ChangeTypes.Variable) {
207
- const rawValue = change.value;
208
- const actualValue = rawValue && typeof rawValue === 'object' && rawValue !== null && 'value' in rawValue && typeof rawValue['value'] === 'object' ? rawValue['value'] : rawValue;
209
- setResult({
210
- value: actualValue,
211
- status: 'success',
212
- error: null
213
- });
214
- const key = flagKeyRef.current;
215
- const shouldTrack = typeof options.shouldTrack === 'function' ? options.shouldTrack() : options.shouldTrack !== false;
216
- if (shouldTrack) {
217
- ninetailed.trackVariableComponentView({
218
- variable: change.value,
219
- variant: {
220
- id: `Variable-${key}`
221
- },
222
- componentType: 'Variable',
223
- variantIndex: change.meta.variantIndex,
224
- experienceId: change.meta.experienceId
225
- });
226
- }
227
- } else {
228
- setResult({
229
- value: defaultValueRef.current,
230
- status: 'success',
231
- error: null
232
- });
233
- }
234
- } catch (error) {
206
+ // Find relevant change for this flag
207
+ const change = changesState.changes.find(c => c.key === flagKeyRef.current && c.type === experience_jsShared.ChangeTypes.Variable);
208
+ if (change) {
209
+ changeRef.current = change;
210
+ const rawValue = change.value;
211
+ const actualValue = rawValue && typeof rawValue === 'object' && rawValue !== null && 'value' in rawValue && typeof rawValue['value'] === 'object' ? rawValue['value'] : rawValue;
212
+ setResult({
213
+ value: actualValue,
214
+ status: 'success',
215
+ error: null
216
+ });
217
+ } else {
218
+ changeRef.current = null;
235
219
  setResult({
236
220
  value: defaultValueRef.current,
237
- status: 'error',
238
- error: error instanceof Error ? error : new Error(String(error))
221
+ status: 'success',
222
+ error: null
239
223
  });
240
224
  }
241
225
  });
242
226
  return unsubscribe;
243
227
  }, [ninetailed]);
228
+ // Manual tracking function
229
+ const track = React.useCallback(() => {
230
+ const change = changeRef.current;
231
+ if (!change) return;
232
+ ninetailed.trackVariableComponentView({
233
+ variable: change.value,
234
+ variant: {
235
+ id: `Variable-${flagKeyRef.current}`
236
+ },
237
+ componentType: 'Variable',
238
+ variantIndex: change.meta.variantIndex,
239
+ experienceId: change.meta.experienceId
240
+ });
241
+ }, [ninetailed]);
242
+ return [result, track];
243
+ }
244
+
245
+ /**
246
+ * Hook to access a Ninetailed variable flag with built-in auto-tracking.
247
+ *
248
+ * @remarks
249
+ * For manual control over tracking behavior, consider using {@link useFlagWithManualTracking}.
250
+ */
251
+ function useFlag(flagKey, defaultValue, options = {}) {
252
+ const [result, track] = useFlagWithManualTracking(flagKey, defaultValue);
253
+ React.useEffect(() => {
254
+ const shouldAutoTrack = typeof options.shouldAutoTrack === 'function' ? options.shouldAutoTrack() : options.shouldAutoTrack !== false;
255
+ if (result.status === 'success' && shouldAutoTrack) {
256
+ track();
257
+ }
258
+ }, [result.status, track, options.shouldAutoTrack]);
244
259
  return result;
245
260
  }
246
261
 
@@ -720,6 +735,7 @@ exports.Personalize = Personalize;
720
735
  exports.TrackHasSeenComponent = TrackHasSeenComponent;
721
736
  exports.useExperience = useExperience;
722
737
  exports.useFlag = useFlag;
738
+ exports.useFlagWithManualTracking = useFlagWithManualTracking;
723
739
  exports.useNinetailed = useNinetailed;
724
740
  exports.usePersonalize = usePersonalize;
725
741
  exports.useProfile = useProfile;
package/index.esm.js CHANGED
@@ -1,4 +1,4 @@
1
- import React, { createContext, useMemo, useContext, useState, useRef, useEffect, createElement, forwardRef } from 'react';
1
+ import React, { createContext, useMemo, useContext, useState, useRef, useEffect, useCallback, createElement, forwardRef } from 'react';
2
2
  import { Ninetailed, selectVariant, selectHasExperienceVariants } from '@ninetailed/experience.js';
3
3
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
4
  import { logger, ChangeTypes, isBrowser, circularJsonStringify } from '@ninetailed/experience.js-shared';
@@ -134,34 +134,36 @@ const usePersonalize = (baseline, variants, options = {
134
134
  };
135
135
 
136
136
  /**
137
- * Hook to access a Ninetailed variable flag with built-in tracking.
137
+ * Hook to access a Ninetailed variable flag with manual tracking control.
138
138
  */
139
- function useFlag(flagKey, defaultValue, options = {}) {
139
+ function useFlagWithManualTracking(flagKey, defaultValue) {
140
140
  const ninetailed = useNinetailed();
141
- const lastProcessedState = useRef(null);
142
- const defaultValueRef = useRef(defaultValue);
143
141
  const flagKeyRef = useRef(flagKey);
142
+ const defaultValueRef = useRef(defaultValue);
143
+ const lastProcessedState = useRef(null);
144
+ const changeRef = useRef(null);
144
145
  const [result, setResult] = useState({
145
146
  value: defaultValue,
146
147
  status: 'loading',
147
148
  error: null
148
149
  });
149
150
 
150
- // Reset on input change
151
+ // Reset if inputs change
151
152
  useEffect(() => {
152
153
  if (!isEqual(defaultValueRef.current, defaultValue) || flagKeyRef.current !== flagKey) {
153
154
  defaultValueRef.current = defaultValue;
154
155
  flagKeyRef.current = flagKey;
156
+ lastProcessedState.current = null;
157
+ changeRef.current = null;
155
158
  setResult({
156
159
  value: defaultValue,
157
160
  status: 'loading',
158
161
  error: null
159
162
  });
160
- lastProcessedState.current = null;
161
163
  }
162
164
  }, [flagKey, defaultValue]);
163
165
 
164
- // Track changes
166
+ // Listen for personalization state changes
165
167
  useEffect(() => {
166
168
  const unsubscribe = ninetailed.onChangesChange(changesState => {
167
169
  if (lastProcessedState.current && isEqual(lastProcessedState.current, changesState)) {
@@ -184,46 +186,61 @@ function useFlag(flagKey, defaultValue, options = {}) {
184
186
  });
185
187
  return;
186
188
  }
187
- try {
188
- const change = changesState.changes.find(change => change.key === flagKeyRef.current);
189
- if (change && change.type === ChangeTypes.Variable) {
190
- const rawValue = change.value;
191
- const actualValue = rawValue && typeof rawValue === 'object' && rawValue !== null && 'value' in rawValue && typeof rawValue['value'] === 'object' ? rawValue['value'] : rawValue;
192
- setResult({
193
- value: actualValue,
194
- status: 'success',
195
- error: null
196
- });
197
- const key = flagKeyRef.current;
198
- const shouldTrack = typeof options.shouldTrack === 'function' ? options.shouldTrack() : options.shouldTrack !== false;
199
- if (shouldTrack) {
200
- ninetailed.trackVariableComponentView({
201
- variable: change.value,
202
- variant: {
203
- id: `Variable-${key}`
204
- },
205
- componentType: 'Variable',
206
- variantIndex: change.meta.variantIndex,
207
- experienceId: change.meta.experienceId
208
- });
209
- }
210
- } else {
211
- setResult({
212
- value: defaultValueRef.current,
213
- status: 'success',
214
- error: null
215
- });
216
- }
217
- } catch (error) {
189
+
190
+ // Find relevant change for this flag
191
+ const change = changesState.changes.find(c => c.key === flagKeyRef.current && c.type === ChangeTypes.Variable);
192
+ if (change) {
193
+ changeRef.current = change;
194
+ const rawValue = change.value;
195
+ const actualValue = rawValue && typeof rawValue === 'object' && rawValue !== null && 'value' in rawValue && typeof rawValue['value'] === 'object' ? rawValue['value'] : rawValue;
196
+ setResult({
197
+ value: actualValue,
198
+ status: 'success',
199
+ error: null
200
+ });
201
+ } else {
202
+ changeRef.current = null;
218
203
  setResult({
219
204
  value: defaultValueRef.current,
220
- status: 'error',
221
- error: error instanceof Error ? error : new Error(String(error))
205
+ status: 'success',
206
+ error: null
222
207
  });
223
208
  }
224
209
  });
225
210
  return unsubscribe;
226
211
  }, [ninetailed]);
212
+
213
+ // Manual tracking function
214
+ const track = useCallback(() => {
215
+ const change = changeRef.current;
216
+ if (!change) return;
217
+ ninetailed.trackVariableComponentView({
218
+ variable: change.value,
219
+ variant: {
220
+ id: `Variable-${flagKeyRef.current}`
221
+ },
222
+ componentType: 'Variable',
223
+ variantIndex: change.meta.variantIndex,
224
+ experienceId: change.meta.experienceId
225
+ });
226
+ }, [ninetailed]);
227
+ return [result, track];
228
+ }
229
+
230
+ /**
231
+ * Hook to access a Ninetailed variable flag with built-in auto-tracking.
232
+ *
233
+ * @remarks
234
+ * For manual control over tracking behavior, consider using {@link useFlagWithManualTracking}.
235
+ */
236
+ function useFlag(flagKey, defaultValue, options = {}) {
237
+ const [result, track] = useFlagWithManualTracking(flagKey, defaultValue);
238
+ useEffect(() => {
239
+ const shouldAutoTrack = typeof options.shouldAutoTrack === 'function' ? options.shouldAutoTrack() : options.shouldAutoTrack !== false;
240
+ if (result.status === 'success' && shouldAutoTrack) {
241
+ track();
242
+ }
243
+ }, [result.status, track, options.shouldAutoTrack]);
227
244
  return result;
228
245
  }
229
246
 
@@ -696,4 +713,4 @@ const ESRLoadingComponent = _ref => {
696
713
  }));
697
714
  };
698
715
 
699
- export { DefaultExperienceLoadingComponent, ESRLoadingComponent, ESRProvider, Experience, MergeTag, NinetailedProvider, Personalize, TrackHasSeenComponent, useExperience, useFlag, useNinetailed, usePersonalize, useProfile };
716
+ export { DefaultExperienceLoadingComponent, ESRLoadingComponent, ESRProvider, Experience, MergeTag, NinetailedProvider, Personalize, TrackHasSeenComponent, useExperience, useFlag, useFlagWithManualTracking, useNinetailed, usePersonalize, useProfile };
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@ninetailed/experience.js-react",
3
- "version": "7.15.1",
3
+ "version": "7.16.0",
4
4
  "description": "Ninetailed SDK for React",
5
5
  "dependencies": {
6
- "@ninetailed/experience.js": "7.15.1",
7
- "@ninetailed/experience.js-shared": "7.15.1",
8
- "@ninetailed/experience.js-plugin-analytics": "7.15.1",
6
+ "@ninetailed/experience.js": "7.16.0",
7
+ "@ninetailed/experience.js-shared": "7.16.0",
8
+ "@ninetailed/experience.js-plugin-analytics": "7.16.0",
9
9
  "radash": "10.9.0",
10
10
  "react-is": "18.2.0"
11
11
  },
@@ -6,6 +6,7 @@ export { useNinetailed } from './useNinetailed';
6
6
  export { useProfile } from './useProfile';
7
7
  export { usePersonalize } from './usePersonalize';
8
8
  export { useFlag } from './useFlag';
9
+ export { useFlagWithManualTracking } from './useFlagWithManualTracking';
9
10
  export { Personalize } from './Personalize';
10
11
  export type { PersonalizedComponent } from './Personalize';
11
12
  export { MergeTag } from './MergeTag';
@@ -13,10 +13,13 @@ export type FlagResult<T> = {
13
13
  error: Error;
14
14
  };
15
15
  type UseFlagOptions = {
16
- shouldTrack?: boolean | (() => boolean);
16
+ shouldAutoTrack?: boolean | (() => boolean);
17
17
  };
18
18
  /**
19
- * Hook to access a Ninetailed variable flag with built-in tracking.
19
+ * Hook to access a Ninetailed variable flag with built-in auto-tracking.
20
+ *
21
+ * @remarks
22
+ * For manual control over tracking behavior, consider using {@link useFlagWithManualTracking}.
20
23
  */
21
24
  export declare function useFlag<T extends AllowedVariableType>(flagKey: string, defaultValue: T, options?: UseFlagOptions): FlagResult<T>;
22
25
  export {};
@@ -0,0 +1,19 @@
1
+ import { AllowedVariableType } from '@ninetailed/experience.js-shared';
2
+ export type FlagResult<T> = {
3
+ status: 'loading';
4
+ value: T;
5
+ error: null;
6
+ } | {
7
+ status: 'success';
8
+ value: T;
9
+ error: null;
10
+ } | {
11
+ status: 'error';
12
+ value: T;
13
+ error: Error;
14
+ };
15
+ export type FlagResultWithTracking<T> = [FlagResult<T>, () => void];
16
+ /**
17
+ * Hook to access a Ninetailed variable flag with manual tracking control.
18
+ */
19
+ export declare function useFlagWithManualTracking<T extends AllowedVariableType>(flagKey: string, defaultValue: T): FlagResultWithTracking<T>;