From 7cafd8767fdf433b6f02fd2aeef8eac1bfbc28a4 Mon Sep 17 00:00:00 2001 From: Daniel Basilio Date: Mon, 24 Feb 2025 12:15:14 -0500 Subject: [PATCH 1/4] fix: Throw a better error if provider is missing --- src/FlagContext.ts | 2 +- src/useFlag.ts | 6 +++--- src/useFlagContext.test.ts | 17 +++++++++++++++++ src/useFlagContext.ts | 10 ++++++++++ src/useFlags.ts | 6 +++--- src/useFlagsStatus.ts | 6 ++---- src/useUnleashClient.ts | 5 ++--- src/useUnleashContext.ts | 5 ++--- src/useVariant.ts | 6 +++--- 9 files changed, 43 insertions(+), 20 deletions(-) create mode 100644 src/useFlagContext.test.ts create mode 100644 src/useFlagContext.ts diff --git a/src/FlagContext.ts b/src/FlagContext.ts index 537720f..4c0dfdd 100644 --- a/src/FlagContext.ts +++ b/src/FlagContext.ts @@ -17,6 +17,6 @@ export interface IFlagContextValue >; } -const FlagContext = React.createContext(null as never); +const FlagContext = React.createContext(null as never); export default FlagContext; diff --git a/src/useFlag.ts b/src/useFlag.ts index 8e13ea8..e853b95 100644 --- a/src/useFlag.ts +++ b/src/useFlag.ts @@ -1,8 +1,8 @@ -import { useContext, useEffect, useState, useRef } from 'react'; -import FlagContext from './FlagContext'; +import { useEffect, useState, useRef } from 'react'; +import { useFlagContext } from './useFlagContext'; const useFlag = (featureName: string) => { - const { isEnabled, client } = useContext(FlagContext); + const { isEnabled, client } = useFlagContext(); const [flag, setFlag] = useState(!!isEnabled(featureName)); const flagRef = useRef(); flagRef.current = flag; diff --git a/src/useFlagContext.test.ts b/src/useFlagContext.test.ts new file mode 100644 index 0000000..2e762e1 --- /dev/null +++ b/src/useFlagContext.test.ts @@ -0,0 +1,17 @@ +import { renderHook } from '@testing-library/react-hooks/native'; +import FlagProvider from "./FlagProvider"; +import { useFlagContext } from "./useFlagContext"; + +test("throws an error if used outside of a FlagProvider", () => { + const { result } = renderHook(() => useFlagContext()); + + expect(result.error).toEqual( + Error("This hook must be used within a FlagProvider") + ); +}); + +test("does not throw an error if used inside of a FlagProvider", () => { + const { result } = renderHook(() => useFlagContext(), { wrapper: FlagProvider }); + + expect(result.error).toBeUndefined(); +}); \ No newline at end of file diff --git a/src/useFlagContext.ts b/src/useFlagContext.ts new file mode 100644 index 0000000..a927123 --- /dev/null +++ b/src/useFlagContext.ts @@ -0,0 +1,10 @@ +import { useContext } from 'react'; +import FlagContext from './FlagContext'; + +export function useFlagContext() { + const context = useContext(FlagContext); + if (!context) { + throw new Error('This hook must be used within a FlagProvider'); + } + return context; +} \ No newline at end of file diff --git a/src/useFlags.ts b/src/useFlags.ts index 1630144..7a10748 100644 --- a/src/useFlags.ts +++ b/src/useFlags.ts @@ -1,8 +1,8 @@ -import { useContext, useEffect, useState } from 'react'; -import FlagContext from './FlagContext'; +import { useEffect, useState } from 'react'; +import { useFlagContext } from './useFlagContext'; const useFlags = () => { - const { client } = useContext(FlagContext); + const { client } = useFlagContext(); const [flags, setFlags] = useState(client.getAllToggles()); useEffect(() => { diff --git a/src/useFlagsStatus.ts b/src/useFlagsStatus.ts index 5e1cda6..1e0c697 100644 --- a/src/useFlagsStatus.ts +++ b/src/useFlagsStatus.ts @@ -1,10 +1,8 @@ /** @format */ - -import { useContext } from 'react'; -import FlagContext from './FlagContext'; +import { useFlagContext } from './useFlagContext'; const useFlagsStatus = () => { - const { flagsReady, flagsError } = useContext(FlagContext); + const { flagsReady, flagsError } = useFlagContext(); return { flagsReady, flagsError }; }; diff --git a/src/useUnleashClient.ts b/src/useUnleashClient.ts index fae34d5..edcd46a 100644 --- a/src/useUnleashClient.ts +++ b/src/useUnleashClient.ts @@ -1,8 +1,7 @@ -import { useContext } from 'react'; -import FlagContext from './FlagContext'; +import { useFlagContext } from './useFlagContext'; const useUnleashClient = () => { - const { client } = useContext(FlagContext); + const { client } = useFlagContext(); return client; }; diff --git a/src/useUnleashContext.ts b/src/useUnleashContext.ts index 477cce7..c1da10f 100644 --- a/src/useUnleashContext.ts +++ b/src/useUnleashContext.ts @@ -1,8 +1,7 @@ -import { useContext } from 'react'; -import FlagContext from './FlagContext'; +import { useFlagContext } from './useFlagContext'; const useUnleashContext = () => { - const { updateContext } = useContext(FlagContext); + const { updateContext } = useFlagContext(); return updateContext; }; diff --git a/src/useVariant.ts b/src/useVariant.ts index f99e18e..90980d1 100644 --- a/src/useVariant.ts +++ b/src/useVariant.ts @@ -1,6 +1,6 @@ -import { useContext, useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { IVariant } from 'unleash-proxy-client'; -import FlagContext from './FlagContext'; +import { useFlagContext } from './useFlagContext'; export const variantHasChanged = ( oldVariant: IVariant, @@ -17,7 +17,7 @@ export const variantHasChanged = ( }; const useVariant = (featureName: string): Partial => { - const { getVariant, client } = useContext(FlagContext); + const { getVariant, client } = useFlagContext(); const [variant, setVariant] = useState(getVariant(featureName)); const variantRef = useRef({ From ecb12ee92856fea969126dbe84e78edb7e686626 Mon Sep 17 00:00:00 2001 From: Daniel Basilio Date: Mon, 24 Feb 2025 12:17:56 -0500 Subject: [PATCH 2/4] fix: The context can be null --- src/FlagContext.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FlagContext.ts b/src/FlagContext.ts index 4c0dfdd..dd0cfc5 100644 --- a/src/FlagContext.ts +++ b/src/FlagContext.ts @@ -17,6 +17,6 @@ export interface IFlagContextValue >; } -const FlagContext = React.createContext(null as never); +const FlagContext = React.createContext(null); export default FlagContext; From 6ff8dad93a4fffc27849365f6327f18b0a477911 Mon Sep 17 00:00:00 2001 From: Daniel Basilio Date: Mon, 24 Feb 2025 12:21:18 -0500 Subject: [PATCH 3/4] fix: Add newlines --- src/useFlagContext.test.ts | 2 +- src/useFlagContext.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/useFlagContext.test.ts b/src/useFlagContext.test.ts index 2e762e1..8178cf3 100644 --- a/src/useFlagContext.test.ts +++ b/src/useFlagContext.test.ts @@ -14,4 +14,4 @@ test("does not throw an error if used inside of a FlagProvider", () => { const { result } = renderHook(() => useFlagContext(), { wrapper: FlagProvider }); expect(result.error).toBeUndefined(); -}); \ No newline at end of file +}); diff --git a/src/useFlagContext.ts b/src/useFlagContext.ts index a927123..d60c83d 100644 --- a/src/useFlagContext.ts +++ b/src/useFlagContext.ts @@ -7,4 +7,4 @@ export function useFlagContext() { throw new Error('This hook must be used within a FlagProvider'); } return context; -} \ No newline at end of file +} From 68bc7b66c5399476860e02f5d24904d06b8198bb Mon Sep 17 00:00:00 2001 From: Daniel Basilio Date: Tue, 4 Mar 2025 15:10:38 -0500 Subject: [PATCH 4/4] tests: Update tests with new context --- src/FlagProvider.test.tsx | 13 +++++-------- src/integration.test.tsx | 10 +++++----- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/FlagProvider.test.tsx b/src/FlagProvider.test.tsx index 4061eed..8c8d679 100644 --- a/src/FlagProvider.test.tsx +++ b/src/FlagProvider.test.tsx @@ -1,9 +1,9 @@ -import React, { useContext, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { render, screen } from '@testing-library/react'; import { type Mock } from 'vitest'; import { UnleashClient, type IVariant, EVENTS } from 'unleash-proxy-client'; import FlagProvider from './FlagProvider'; -import FlagContext from './FlagContext'; +import { useFlagContext } from './useFlagContext'; import '@testing-library/jest-dom'; const getVariantMock = vi.fn().mockReturnValue('A'); @@ -44,8 +44,7 @@ vi.mock('unleash-proxy-client', async (importOriginal) => { const noop = () => {}; const FlagConsumerAfterClientInit = () => { - const { updateContext, isEnabled, getVariant, client, on } = - useContext(FlagContext); + const { updateContext, isEnabled, getVariant, client, on } = useFlagContext(); const [enabled, setIsEnabled] = useState(false); const [variant, setVariant] = useState(null); const [context, setContext] = useState('nothing'); @@ -71,8 +70,7 @@ const FlagConsumerAfterClientInit = () => { }; const FlagConsumerBeforeClientInit = () => { - const { updateContext, isEnabled, getVariant, client, on } = - useContext(FlagContext); + const { updateContext, isEnabled, getVariant, client, on } = useFlagContext(); const [enabled, setIsEnabled] = useState(false); const [variant, setVariant] = useState(null); const [context, setContext] = useState('nothing'); @@ -162,8 +160,7 @@ test('A memoized consumer should not rerender when the context provider values a const renderCounter = vi.fn(); const MemoizedConsumer = React.memo(() => { - const { updateContext, isEnabled, getVariant, client, on } = - useContext(FlagContext); + const { updateContext, isEnabled, getVariant, client, on } = useFlagContext(); renderCounter(); diff --git a/src/integration.test.tsx b/src/integration.test.tsx index 7205e6e..6ef6652 100644 --- a/src/integration.test.tsx +++ b/src/integration.test.tsx @@ -1,12 +1,12 @@ -import React, { useContext } from 'react'; +import React from 'react'; import { render, screen, waitFor } from '@testing-library/react'; import { EVENTS, UnleashClient } from 'unleash-proxy-client'; import FlagProvider from './FlagProvider'; import useFlagsStatus from './useFlagsStatus'; import { act } from 'react-dom/test-utils'; import useFlag from './useFlag'; +import { useFlagContext } from './useFlagContext'; import useVariant from './useVariant'; -import FlagContext from './FlagContext'; const fetchMock = vi.fn(async () => { return Promise.resolve({ @@ -89,7 +89,7 @@ test('should render toggles', async () => { test('should be ready from the start if bootstrapped', () => { const Component = React.memo(() => { - const { flagsReady } = useContext(FlagContext); + const { flagsReady } = useFlagContext(); return <>{flagsReady ? 'ready' : ''}; }); @@ -183,7 +183,7 @@ test('should render limited times when bootstrapped', async () => { const Component = () => { const enabled = useFlag('test-flag'); - const { flagsReady } = useContext(FlagContext); + const { flagsReady } = useFlagContext(); renders += 1; @@ -229,7 +229,7 @@ test('should resolve values before setting flagsReady', async () => { const Component = () => { const enabled = useFlag('test-flag'); - const { flagsReady } = useContext(FlagContext); + const { flagsReady } = useFlagContext(); renders += 1;