Skip to content

Commit 6c0b314

Browse files
[FSSDK-12295] client hook update
1 parent 33ca8f5 commit 6c0b314

3 files changed

Lines changed: 126 additions & 0 deletions

File tree

src/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@
1414
* limitations under the License.
1515
*/
1616

17+
export { useOptimizelyClient } from './useOptimizelyClient';
1718
export { useOptimizelyUserContext } from './useOptimizelyUserContext';
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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 { vi, describe, it, expect } from 'vitest';
18+
import React, { useRef } from 'react';
19+
import { render, screen, act } from '@testing-library/react';
20+
import { renderHook } from '@testing-library/react';
21+
import { useOptimizelyClient } from './useOptimizelyClient';
22+
import { OptimizelyProvider, ProviderStateStore, OptimizelyContext } from '../provider/index';
23+
import { createInstance, createStaticProjectConfigManager } from '../client/index';
24+
import type { OptimizelyContextValue } from '../provider/index';
25+
26+
function createClient() {
27+
return createInstance({
28+
projectConfigManager: createStaticProjectConfigManager({ datafile: JSON.stringify({}) }),
29+
});
30+
}
31+
32+
function useRenderCount() {
33+
const renderCount = useRef(0);
34+
return ++renderCount.current;
35+
}
36+
37+
describe('useOptimizelyClient', () => {
38+
it('should throw when used outside of OptimizelyProvider', () => {
39+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
40+
41+
expect(() => {
42+
renderHook(() => useOptimizelyClient());
43+
}).toThrow('useOptimizelyClient must be used within an <OptimizelyProvider>');
44+
45+
consoleSpy.mockRestore();
46+
});
47+
48+
it('should return the same client instance passed to OptimizelyProvider', () => {
49+
const client = createClient();
50+
51+
const { result } = renderHook(() => useOptimizelyClient(), {
52+
wrapper: ({ children }) => <OptimizelyProvider client={client}>{children}</OptimizelyProvider>,
53+
});
54+
55+
expect(result.current).toBe(client);
56+
});
57+
58+
it('should not re-render when store state changes', () => {
59+
const client = createClient();
60+
const store = new ProviderStateStore();
61+
62+
const contextValue: OptimizelyContextValue = { store, client };
63+
64+
let capturedRenderCount = 0;
65+
66+
function TestComponent() {
67+
const hookClient = useOptimizelyClient();
68+
const renderCount = useRenderCount();
69+
capturedRenderCount = renderCount;
70+
return <div data-testid="client">{hookClient ? 'has-client' : 'no-client'}</div>;
71+
}
72+
73+
render(
74+
<OptimizelyContext.Provider value={contextValue}>
75+
<TestComponent />
76+
</OptimizelyContext.Provider>
77+
);
78+
79+
expect(screen.getByTestId('client').textContent).toBe('has-client');
80+
const initialRenderCount = capturedRenderCount;
81+
82+
// Trigger store state changes that should NOT cause useOptimizelyClient to re-render
83+
act(() => {
84+
store.setClientReady(true);
85+
});
86+
expect(capturedRenderCount).toBe(initialRenderCount);
87+
88+
act(() => {
89+
store.setError(new Error('test'));
90+
});
91+
expect(capturedRenderCount).toBe(initialRenderCount);
92+
});
93+
});

src/hooks/useOptimizelyClient.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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 { useContext } from 'react';
18+
import { OptimizelyContext } from '../provider/index';
19+
import type { Client } from '@optimizely/optimizely-sdk';
20+
21+
/**
22+
* Returns the Optimizely client instance from the nearest `<OptimizelyProvider>`.
23+
*/
24+
export function useOptimizelyClient(): Client {
25+
const context = useContext(OptimizelyContext);
26+
27+
if (!context) {
28+
throw new Error('useOptimizelyClient must be used within an <OptimizelyProvider>');
29+
}
30+
31+
return context.client;
32+
}

0 commit comments

Comments
 (0)