Skip to content

Commit 9c77bfd

Browse files
[FSSDK-12296] Async Hook implementation (#324)
1 parent 899e2d8 commit 9c77bfd

14 files changed

Lines changed: 1517 additions & 26 deletions

src/hooks/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
export { useOptimizelyClient } from './useOptimizelyClient';
1818
export { useOptimizelyUserContext } from './useOptimizelyUserContext';
1919
export type { UseOptimizelyUserContextResult } from './useOptimizelyUserContext';
20+
export type { UseDecideConfig, UseDecideResult, UseDecideMultiResult } from './types';
2021
export { useDecide } from './useDecide';
21-
export type { UseDecideConfig, UseDecideResult } from './useDecide';
2222
export { useDecideForKeys } from './useDecideForKeys';
23-
export type { UseDecideMultiResult } from './useDecideForKeys';
2423
export { useDecideAll } from './useDecideAll';
24+
export { useDecideAsync } from './useDecideAsync';
25+
export { useDecideForKeysAsync } from './useDecideForKeysAsync';
26+
export { useDecideAllAsync } from './useDecideAllAsync';

src/hooks/testUtils.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,7 @@ export const MOCK_DECISIONS: Record<string, OptimizelyDecision> = {
4848
* Creates a mock OptimizelyUserContext with all methods stubbed.
4949
* Override specific methods via the overrides parameter.
5050
*/
51-
export function createMockUserContext(
52-
overrides?: Partial<Record<string, unknown>>,
53-
): OptimizelyUserContext {
51+
export function createMockUserContext(overrides?: Partial<Record<string, unknown>>): OptimizelyUserContext {
5452
return {
5553
getUserId: vi.fn().mockReturnValue('test-user'),
5654
getAttributes: vi.fn().mockReturnValue({}),
@@ -66,6 +64,17 @@ export function createMockUserContext(
6664
}
6765
return result;
6866
}),
67+
decideAsync: vi.fn().mockResolvedValue(MOCK_DECISION),
68+
decideAllAsync: vi.fn().mockResolvedValue(MOCK_DECISIONS),
69+
decideForKeysAsync: vi.fn().mockImplementation((keys: string[]) => {
70+
const result: Record<string, OptimizelyDecision> = {};
71+
for (const key of keys) {
72+
if (MOCK_DECISIONS[key]) {
73+
result[key] = MOCK_DECISIONS[key];
74+
}
75+
}
76+
return Promise.resolve(result);
77+
}),
6978
setForcedDecision: vi.fn().mockReturnValue(true),
7079
getForcedDecision: vi.fn(),
7180
removeForcedDecision: vi.fn().mockReturnValue(true),

src/hooks/types.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* Copyright 2026, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import type { OptimizelyDecideOption, OptimizelyDecision } from '@optimizely/optimizely-sdk';
18+
19+
export interface UseDecideConfig {
20+
decideOptions?: OptimizelyDecideOption[];
21+
}
22+
23+
export type UseDecideResult =
24+
| { isLoading: true; error: null; decision: null }
25+
| { isLoading: false; error: Error; decision: null }
26+
| { isLoading: false; error: null; decision: OptimizelyDecision };
27+
28+
export type UseDecideMultiResult =
29+
| { isLoading: true; error: null; decisions: Record<string, never> }
30+
| { isLoading: false; error: Error; decisions: Record<string, never> }
31+
| { isLoading: false; error: null; decisions: Record<string, OptimizelyDecision> };

src/hooks/useAsyncDecision.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/**
2+
* Copyright 2026, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { useEffect, useState } from 'react';
18+
import type { OptimizelyUserContext } from '@optimizely/optimizely-sdk';
19+
20+
import type { Client } from '@optimizely/optimizely-sdk';
21+
import type { ProviderState } from '../provider/index';
22+
23+
interface AsyncState<TResult> {
24+
result: TResult;
25+
error: Error | null;
26+
isLoading: boolean;
27+
}
28+
29+
/**
30+
* Shared async decision state machine used by useDecideAsync,
31+
* useDecideForKeysAsync, and useDecideAllAsync.
32+
*
33+
* Handles: loading state, error propagation, cancellation of stale promises,
34+
* and redundant re-render avoidance on first mount.
35+
*
36+
* @param state - Provider state from useProviderState
37+
* @param client - Optimizely client instance
38+
* @param fdVersion - Forced decision version counter (triggers re-evaluation)
39+
* @param emptyResult - Default/empty result value (null for single, {} for multi)
40+
* @param execute - Callback that performs the async SDK call
41+
*/
42+
export function useAsyncDecision<TResult>(
43+
state: ProviderState,
44+
client: Client,
45+
fdVersion: number,
46+
emptyResult: TResult,
47+
execute: (userContext: OptimizelyUserContext) => Promise<TResult>
48+
): AsyncState<TResult> {
49+
const [asyncState, setAsyncState] = useState<AsyncState<TResult>>({
50+
result: emptyResult,
51+
error: null,
52+
isLoading: true,
53+
});
54+
55+
useEffect(() => {
56+
const { userContext, error } = state;
57+
const hasConfig = client.getOptimizelyConfig() !== null;
58+
59+
// Store-level error — no async call needed
60+
if (error) {
61+
setAsyncState({ result: emptyResult, error, isLoading: false });
62+
return;
63+
}
64+
65+
// Ensure loading state (skip if already loading to avoid re-render)
66+
setAsyncState((prev) => {
67+
if (prev.isLoading) return prev;
68+
return { result: emptyResult, error: null, isLoading: true };
69+
});
70+
71+
// Store not ready — wait for config/user context
72+
if (!hasConfig || userContext === null) {
73+
return;
74+
}
75+
76+
// Store is ready — fire async decision
77+
let cancelled = false;
78+
79+
execute(userContext).then(
80+
(result) => {
81+
if (!cancelled) {
82+
setAsyncState({ result, error: null, isLoading: false });
83+
}
84+
},
85+
(err) => {
86+
if (!cancelled) {
87+
setAsyncState({
88+
result: emptyResult,
89+
error: err instanceof Error ? err : new Error(String(err)),
90+
isLoading: false,
91+
});
92+
}
93+
}
94+
);
95+
96+
return () => {
97+
cancelled = true;
98+
};
99+
}, [state, fdVersion, client, execute, emptyResult]);
100+
101+
return asyncState;
102+
}

src/hooks/useDecide.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,10 @@
1515
*/
1616

1717
import { useEffect, useMemo, useState } from 'react';
18-
import type { OptimizelyDecideOption, OptimizelyDecision } from '@optimizely/optimizely-sdk';
19-
2018
import { useOptimizelyContext } from './useOptimizelyContext';
2119
import { useProviderState } from './useProviderState';
2220
import { useStableArray } from './useStableArray';
23-
24-
export interface UseDecideConfig {
25-
decideOptions?: OptimizelyDecideOption[];
26-
}
27-
28-
export type UseDecideResult =
29-
| { isLoading: true; error: null; decision: null }
30-
| { isLoading: false; error: Error; decision: null }
31-
| { isLoading: false; error: null; decision: OptimizelyDecision };
21+
import type { UseDecideConfig, UseDecideResult } from './types';
3222

3323
/**
3424
* Returns a feature flag decision for the given flag key.

src/hooks/useDecideAll.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@ import { useEffect, useMemo, useState } from 'react';
1919
import { useOptimizelyContext } from './useOptimizelyContext';
2020
import { useProviderState } from './useProviderState';
2121
import { useStableArray } from './useStableArray';
22-
import type { UseDecideConfig } from './useDecide';
23-
import type { UseDecideMultiResult } from './useDecideForKeys';
22+
import type { UseDecideConfig, UseDecideMultiResult } from './types';
2423

2524
/**
2625
* Returns feature flag decisions for all flags.

0 commit comments

Comments
 (0)