From 2029ff804acd79361d1d800fed13e5fbbdd6bd51 Mon Sep 17 00:00:00 2001 From: FredrikOseberg Date: Mon, 10 Mar 2025 14:37:23 +0100 Subject: [PATCH 1/6] fix: console.error --- src/useFlagContext.test.ts | 21 ++++++++++++++------- src/useFlagContext.ts | 3 ++- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/useFlagContext.test.ts b/src/useFlagContext.test.ts index 8178cf3..efd51c5 100644 --- a/src/useFlagContext.test.ts +++ b/src/useFlagContext.test.ts @@ -1,17 +1,24 @@ -import { renderHook } from '@testing-library/react-hooks/native'; +import { renderHook } from '@testing-library/react'; +import { vi, test, expect } from 'vitest'; import FlagProvider from "./FlagProvider"; import { useFlagContext } from "./useFlagContext"; -test("throws an error if used outside of a FlagProvider", () => { +test("logs an error if used outside of a FlagProvider", () => { + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + const { result } = renderHook(() => useFlagContext()); + expect(consoleSpy).toHaveBeenCalledWith("This hook must be used within a FlagProvider"); + expect(result.current).toBeNull(); - expect(result.error).toEqual( - Error("This hook must be used within a FlagProvider") - ); + consoleSpy.mockRestore(); }); -test("does not throw an error if used inside of a FlagProvider", () => { +test("does not log an error if used inside of a FlagProvider", () => { + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + const { result } = renderHook(() => useFlagContext(), { wrapper: FlagProvider }); + expect(consoleSpy).not.toHaveBeenCalled(); + expect(result.current).not.toBeNull(); - expect(result.error).toBeUndefined(); + consoleSpy.mockRestore(); }); diff --git a/src/useFlagContext.ts b/src/useFlagContext.ts index d60c83d..d81c9a4 100644 --- a/src/useFlagContext.ts +++ b/src/useFlagContext.ts @@ -4,7 +4,8 @@ import FlagContext from './FlagContext'; export function useFlagContext() { const context = useContext(FlagContext); if (!context) { - throw new Error('This hook must be used within a FlagProvider'); + console.error('This hook must be used within a FlagProvider'); + return null; } return context; } From a66399c9a0e94786d9fafcc31e787175b590aabf Mon Sep 17 00:00:00 2001 From: FredrikOseberg Date: Mon, 10 Mar 2025 14:41:50 +0100 Subject: [PATCH 2/6] docs: update documentation --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 647c702..f2570df 100644 --- a/README.md +++ b/README.md @@ -345,6 +345,11 @@ Upgrading should be as easy as running yarn again with the new version, but we m `startClient` option has been simplified. Now it will also work if you don't pass custom client with it. It defaults to `true`. +## Upgrade path from v4 -> v5 + +[FlagContext public interface changed](https://github.com/Unleash/proxy-client-react/commit/b783ef4016dbb881ac3d878cffaf5241b047cc35#diff-825c82ad66c3934257e0ee3e0511d9223db22e7ddf5de9cbdf6485206e3e02cfL20-R20). if you used FlagContext directly you may have to adjust your types. + + #### Note on v4.0.0: The major release is driven by Node14 end of life and represents no other changes. From this version onwards we do not guarantee that this library will work server side with Node 14. From 6c6994905631234e1f1cfff1365f48b316bbe7d2 Mon Sep 17 00:00:00 2001 From: FredrikOseberg Date: Mon, 10 Mar 2025 14:42:20 +0100 Subject: [PATCH 3/6] docs: better description of change --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f2570df..e9ae783 100644 --- a/README.md +++ b/README.md @@ -347,7 +347,7 @@ Upgrading should be as easy as running yarn again with the new version, but we m ## Upgrade path from v4 -> v5 -[FlagContext public interface changed](https://github.com/Unleash/proxy-client-react/commit/b783ef4016dbb881ac3d878cffaf5241b047cc35#diff-825c82ad66c3934257e0ee3e0511d9223db22e7ddf5de9cbdf6485206e3e02cfL20-R20). if you used FlagContext directly you may have to adjust your types. +[FlagContext public interface changed](https://github.com/Unleash/proxy-client-react/commit/b783ef4016dbb881ac3d878cffaf5241b047cc35#diff-825c82ad66c3934257e0ee3e0511d9223db22e7ddf5de9cbdf6485206e3e02cfL20-R20). If you used FlagContext directly you may have to adjust your code slightly to accomodate the new type changes. #### Note on v4.0.0: From a32d2d523ea0970c62092577ca14d819967a5773 Mon Sep 17 00:00:00 2001 From: FredrikOseberg Date: Mon, 10 Mar 2025 14:54:44 +0100 Subject: [PATCH 4/6] fix: return mock client if provider is not present --- src/useFlagContext.test.ts | 5 ++- src/useFlagContext.ts | 78 ++++++++++++++++++++++++++++++++++---- 2 files changed, 74 insertions(+), 9 deletions(-) diff --git a/src/useFlagContext.test.ts b/src/useFlagContext.test.ts index efd51c5..3b1b21c 100644 --- a/src/useFlagContext.test.ts +++ b/src/useFlagContext.test.ts @@ -6,9 +6,9 @@ import { useFlagContext } from "./useFlagContext"; test("logs an error if used outside of a FlagProvider", () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - const { result } = renderHook(() => useFlagContext()); + renderHook(() => useFlagContext()); + expect(consoleSpy).toHaveBeenCalledWith("This hook must be used within a FlagProvider"); - expect(result.current).toBeNull(); consoleSpy.mockRestore(); }); @@ -17,6 +17,7 @@ test("does not log an error if used inside of a FlagProvider", () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const { result } = renderHook(() => useFlagContext(), { wrapper: FlagProvider }); + expect(consoleSpy).not.toHaveBeenCalled(); expect(result.current).not.toBeNull(); diff --git a/src/useFlagContext.ts b/src/useFlagContext.ts index d81c9a4..0b7f951 100644 --- a/src/useFlagContext.ts +++ b/src/useFlagContext.ts @@ -1,11 +1,75 @@ import { useContext } from 'react'; -import FlagContext from './FlagContext'; +import FlagContext, { IFlagContextValue } from './FlagContext'; +import { UnleashClient } from 'unleash-proxy-client'; + +const noopOn = (event: string, callback: Function, ctx?: any): UnleashClient => { + console.error('This hook must be used within a FlagProvider'); + return mockUnleashClient; +}; + +const noopOff = (event: string, callback?: Function): UnleashClient => { + console.error('This hook must be used within a FlagProvider'); + return mockUnleashClient; +}; + +const mockUnleashClient = { + on: noopOn, + off: noopOff, + updateContext: async () => { + console.error('This hook must be used within a FlagProvider'); + return undefined; + }, + isEnabled: () => { + console.error('This hook must be used within a FlagProvider'); + return false; + }, + getVariant: () => { + console.error('This hook must be used within a FlagProvider'); + return { name: 'disabled', enabled: false }; + }, + toggles: [], + impressionDataAll: {}, + context: {}, + storage: {}, + start: () => {}, + stop: () => {}, + isReady: () => false, + getError: () => null, + getAllToggles: () => [], +} as unknown as UnleashClient; + +// Create a default context value +const defaultContextValue: IFlagContextValue = { + on: noopOn, + off: noopOff, + updateContext: async () => { + console.error('updateContext hook must be used within a FlagProvider'); + return undefined; + }, + isEnabled: () => { + console.error('isEnabled hook must be used within a FlagProvider'); + return false; + }, + getVariant: () => { + console.error('getVariant hook must be used within a FlagProvider'); + return { name: 'disabled', enabled: false }; + }, + client: mockUnleashClient, + flagsReady: false, + setFlagsReady: () => { + console.error('setFlagsReady hook must be used within a FlagProvider'); + }, + flagsError: null, + setFlagsError: () => { + console.error('setFlagsError hook must be used within a FlagProvider'); + } +}; export function useFlagContext() { - const context = useContext(FlagContext); - if (!context) { - console.error('This hook must be used within a FlagProvider'); - return null; - } - return context; + const context = useContext(FlagContext); + if (!context) { + console.error('useFlagContext hook must be used within a FlagProvider'); + return defaultContextValue; + } + return context; } From 0a0d21c468a9aeb5777bade55a67f472dac7c13a Mon Sep 17 00:00:00 2001 From: FredrikOseberg Date: Mon, 10 Mar 2025 15:33:59 +0100 Subject: [PATCH 5/6] fix: test --- src/useFlagContext.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/useFlagContext.test.ts b/src/useFlagContext.test.ts index 3b1b21c..a5a6f7d 100644 --- a/src/useFlagContext.test.ts +++ b/src/useFlagContext.test.ts @@ -7,8 +7,7 @@ test("logs an error if used outside of a FlagProvider", () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); renderHook(() => useFlagContext()); - - expect(consoleSpy).toHaveBeenCalledWith("This hook must be used within a FlagProvider"); + expect(consoleSpy).toHaveBeenCalledWith("useFlagContext hook must be used within a FlagProvider"); consoleSpy.mockRestore(); }); From 00ab47382b6f5b5a89b637a37ccae7bec12e75f2 Mon Sep 17 00:00:00 2001 From: FredrikOseberg Date: Tue, 11 Mar 2025 10:00:00 +0100 Subject: [PATCH 6/6] refactor: share mocked methods --- src/useFlagContext.test.ts | 2 +- src/useFlagContext.ts | 114 ++++++++++++++++--------------------- 2 files changed, 51 insertions(+), 65 deletions(-) diff --git a/src/useFlagContext.test.ts b/src/useFlagContext.test.ts index a5a6f7d..c87a9ad 100644 --- a/src/useFlagContext.test.ts +++ b/src/useFlagContext.test.ts @@ -7,7 +7,7 @@ test("logs an error if used outside of a FlagProvider", () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); renderHook(() => useFlagContext()); - expect(consoleSpy).toHaveBeenCalledWith("useFlagContext hook must be used within a FlagProvider"); + expect(consoleSpy).toHaveBeenCalledWith("useFlagContext() must be used within a FlagProvider"); consoleSpy.mockRestore(); }); diff --git a/src/useFlagContext.ts b/src/useFlagContext.ts index 0b7f951..673e7cb 100644 --- a/src/useFlagContext.ts +++ b/src/useFlagContext.ts @@ -1,75 +1,61 @@ -import { useContext } from 'react'; -import FlagContext, { IFlagContextValue } from './FlagContext'; -import { UnleashClient } from 'unleash-proxy-client'; +import { useContext } from "react"; +import FlagContext, { type IFlagContextValue } from "./FlagContext"; +import type { UnleashClient } from "unleash-proxy-client"; -const noopOn = (event: string, callback: Function, ctx?: any): UnleashClient => { - console.error('This hook must be used within a FlagProvider'); - return mockUnleashClient; -}; - -const noopOff = (event: string, callback?: Function): UnleashClient => { - console.error('This hook must be used within a FlagProvider'); - return mockUnleashClient; +const methods = { + on: (event: string, callback: Function, ctx?: any): UnleashClient => { + console.error("on() must be used within a FlagProvider"); + return mockUnleashClient; + }, + off: (event: string, callback?: Function): UnleashClient => { + console.error("off() must be used within a FlagProvider"); + return mockUnleashClient; + }, + updateContext: async () => { + console.error("updateContext() must be used within a FlagProvider"); + return undefined; + }, + isEnabled: () => { + console.error("isEnabled() must be used within a FlagProvider"); + return false; + }, + getVariant: () => { + console.error("getVariant() must be used within a FlagProvider"); + return { name: "disabled", enabled: false }; + } }; const mockUnleashClient = { - on: noopOn, - off: noopOff, - updateContext: async () => { - console.error('This hook must be used within a FlagProvider'); - return undefined; - }, - isEnabled: () => { - console.error('This hook must be used within a FlagProvider'); - return false; - }, - getVariant: () => { - console.error('This hook must be used within a FlagProvider'); - return { name: 'disabled', enabled: false }; - }, - toggles: [], - impressionDataAll: {}, - context: {}, - storage: {}, - start: () => {}, - stop: () => {}, - isReady: () => false, - getError: () => null, - getAllToggles: () => [], + ...methods, + toggles: [], + impressionDataAll: {}, + context: {}, + storage: {}, + start: () => {}, + stop: () => {}, + isReady: () => false, + getError: () => null, + getAllToggles: () => [] } as unknown as UnleashClient; -// Create a default context value const defaultContextValue: IFlagContextValue = { - on: noopOn, - off: noopOff, - updateContext: async () => { - console.error('updateContext hook must be used within a FlagProvider'); - return undefined; - }, - isEnabled: () => { - console.error('isEnabled hook must be used within a FlagProvider'); - return false; - }, - getVariant: () => { - console.error('getVariant hook must be used within a FlagProvider'); - return { name: 'disabled', enabled: false }; - }, - client: mockUnleashClient, - flagsReady: false, - setFlagsReady: () => { - console.error('setFlagsReady hook must be used within a FlagProvider'); - }, - flagsError: null, - setFlagsError: () => { - console.error('setFlagsError hook must be used within a FlagProvider'); - } + ...methods, + client: mockUnleashClient, + flagsReady: false, + setFlagsReady: () => { + console.error("setFlagsReady() must be used within a FlagProvider"); + }, + flagsError: null, + setFlagsError: () => { + console.error("setFlagsError() must be used within a FlagProvider"); + } }; export function useFlagContext() { - const context = useContext(FlagContext); - if (!context) { - console.error('useFlagContext hook must be used within a FlagProvider'); - return defaultContextValue; - } - return context; + const context = useContext(FlagContext); + if (!context) { + console.error("useFlagContext() must be used within a FlagProvider"); + return defaultContextValue; + } + return context; }