Skip to content

Commit 2646830

Browse files
[FSSDK-12295] useOptimizelyUserContext impl.
1 parent 09a1f5f commit 2646830

4 files changed

Lines changed: 25 additions & 34 deletions

File tree

package-lock.json

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"@types/jest": "^30.0.0",
6161
"@types/react": "^18.3.0",
6262
"@types/react-dom": "^18.3.0",
63+
"@types/use-sync-external-store": "^1.5.0",
6364
"@typescript-eslint/eslint-plugin": "^8.0.0",
6465
"@typescript-eslint/parser": "^8.0.0",
6566
"@vitest/coverage-v8": "^4.0.18",

src/hooks/useOptimizelyUserContext.spec.tsx

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,14 @@
1515
*/
1616

1717
import { vi, describe, it, expect, beforeEach } from 'vitest';
18-
import React, { act, useRef } from 'react';
19-
import { render, screen } from '@testing-library/react';
18+
import React, { useRef } from 'react';
19+
import { render, screen, act } from '@testing-library/react';
2020
import { renderHook } from '@testing-library/react';
21-
import type { OptimizelyUserContext } from '@optimizely/optimizely-sdk';
22-
2321
import { OptimizelyContext } from '../provider/OptimizelyProvider';
2422
import { ProviderStateStore } from '../provider/ProviderStateStore';
25-
import type { OptimizelyContextValue } from '../provider/types';
2623
import { useOptimizelyUserContext } from './useOptimizelyUserContext';
24+
import type { OptimizelyUserContext } from '@optimizely/optimizely-sdk';
25+
import type { OptimizelyContextValue } from '../provider/types';
2726

2827
function useRenderCount() {
2928
const renderCount = useRef(0);
@@ -97,7 +96,7 @@ describe('useOptimizelyUserContext', () => {
9796
expect(result.current).toBe(mockUserContext);
9897
});
9998

100-
it('should update when user context changes', () => {
99+
it('should update when user context changes', async () => {
101100
const wrapper = createWrapper(store);
102101
const { result } = renderHook(() => useOptimizelyUserContext(), { wrapper });
103102

@@ -194,16 +193,4 @@ describe('useOptimizelyUserContext', () => {
194193
expect(capturedRenderCount).toBe(initialRenderCount);
195194
expect(screen.getByTestId('user-id').textContent).toBe('test-user');
196195
});
197-
198-
it('should return wrapped user context with forced decision methods', () => {
199-
const mockUserContext = createMockUserContext();
200-
store.setUserContext(mockUserContext);
201-
202-
const wrapper = createWrapper(store);
203-
const { result } = renderHook(() => useOptimizelyUserContext(), { wrapper });
204-
205-
// The returned context should be the same reference as what's in the store
206-
// (already wrapped by ProviderStateStore.setUserContext)
207-
expect(result.current).toBe(store.getState().userContext);
208-
});
209196
});

src/hooks/useOptimizelyUserContext.ts

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,22 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { useContext, useState, useEffect, useRef } from 'react';
17+
import { useContext, useCallback } from 'react';
18+
import { useSyncExternalStore } from 'use-sync-external-store/shim';
1819
import type { OptimizelyUserContext } from '@optimizely/optimizely-sdk';
1920

2021
import { OptimizelyContext } from '../provider/index';
2122

2223
/**
23-
* Returns the current OptimizelyUserContext from the ProviderStateStore.
24+
* Returns the current {@link OptimizelyUserContext} for the nearest `<OptimizelyProvider>`.
2425
*
25-
* The returned context has wrapped forced decision methods — calling
26-
* `setForcedDecision()`, `removeForcedDecision()`, or `removeAllForcedDecisions()`
27-
* on it will automatically trigger React re-renders in hooks watching the affected flags.
26+
* The user context gives access to the user's identity (user ID and attributes)
27+
* and methods for working with forced decisions (`setForcedDecision`,
28+
* `removeForcedDecision`, `removeAllForcedDecisions`).
2829
*
29-
* Returns `null` while the SDK is initializing or if the user context has not been created yet.
30+
* Returns `null` while the SDK is initializing or if no user has been set yet.
3031
*/
32+
3133
export function useOptimizelyUserContext(): OptimizelyUserContext | null {
3234
const context = useContext(OptimizelyContext);
3335

@@ -37,16 +39,9 @@ export function useOptimizelyUserContext(): OptimizelyUserContext | null {
3739

3840
const { store } = context;
3941

40-
const [userContext, setUserContext] = useState<OptimizelyUserContext | null>(() => store.getState().userContext);
41-
42-
useEffect(() => {
43-
setUserContext(store.getState().userContext);
44-
const unsubscribe = store.subscribe((state) => {
45-
setUserContext(state.userContext);
46-
});
42+
const subscribe = useCallback((onStoreChange: () => void) => store.subscribe(onStoreChange), [store]);
4743

48-
return unsubscribe;
49-
}, [store]);
44+
const getSnapshot = useCallback(() => store.getState().userContext, [store]);
5045

51-
return userContext;
46+
return useSyncExternalStore(subscribe, getSnapshot);
5247
}

0 commit comments

Comments
 (0)