From cf9d52c302bd093f9141339cefd0d14ce1756cc2 Mon Sep 17 00:00:00 2001 From: Przemek Maszczynski Date: Fri, 7 Feb 2025 10:39:49 +0100 Subject: [PATCH 1/9] docs: fix wrong import in events section of README.md (#191) Listening to events contained import of `useUnleashContext` instead `useUnleashContext` --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ae41aa8..647c702 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ The core JavaScript client emits various types of events depending on internal a NOTE: `FlagProvider` uses these internal events to provide information through `useFlagsStatus`. ```jsx -import { useUnleashContext, useFlag } from '@unleash/proxy-client-react'; +import { useUnleashClient, useFlag } from '@unleash/proxy-client-react'; const MyComponent = ({ userId }) => { const client = useUnleashClient(); From b783ef4016dbb881ac3d878cffaf5241b047cc35 Mon Sep 17 00:00:00 2001 From: Daniel Basilio Date: Thu, 6 Mar 2025 03:13:15 -0500 Subject: [PATCH 2/9] fix: Throw a better error message if context is null (#192) Now throws an error message if context is not set. --- src/FlagContext.ts | 2 +- src/FlagProvider.test.tsx | 13 +++++-------- src/integration.test.tsx | 10 +++++----- 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 +++--- 11 files changed, 53 insertions(+), 33 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..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; 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; 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..8178cf3 --- /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(); +}); diff --git a/src/useFlagContext.ts b/src/useFlagContext.ts new file mode 100644 index 0000000..d60c83d --- /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; +} 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 7e7d4f6dd9478091d46008f632f03db1a29f9d70 Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot <> Date: Thu, 6 Mar 2025 08:17:51 +0000 Subject: [PATCH 3/9] v5.0.0-rc.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ccbe371..a89eb22 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@unleash/proxy-client-react", - "version": "4.5.2", + "version": "5.0.0-rc.0", "description": "React interface for working with unleash", "type": "module", "main": "./dist/unleash-react.umd.cjs", From 9fe2488c49e9105cd46e44b69cde60ebd0d1593b Mon Sep 17 00:00:00 2001 From: Fredrik Strand Oseberg Date: Tue, 11 Mar 2025 10:08:39 +0100 Subject: [PATCH 4/9] fix: console.error (#194) Use console.error instead of throwing error --- README.md | 5 +++ src/useFlagContext.test.ts | 23 +++++++++----- src/useFlagContext.ts | 65 ++++++++++++++++++++++++++++++++++---- 3 files changed, 78 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 647c702..e9ae783 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 code slightly to accomodate the new type changes. + + #### 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. diff --git a/src/useFlagContext.test.ts b/src/useFlagContext.test.ts index 8178cf3..c87a9ad 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", () => { - const { result } = renderHook(() => useFlagContext()); +test("logs an error if used outside of a FlagProvider", () => { + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - expect(result.error).toEqual( - Error("This hook must be used within a FlagProvider") - ); + renderHook(() => useFlagContext()); + expect(consoleSpy).toHaveBeenCalledWith("useFlagContext() 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(result.error).toBeUndefined(); + expect(consoleSpy).not.toHaveBeenCalled(); + expect(result.current).not.toBeNull(); + + consoleSpy.mockRestore(); }); diff --git a/src/useFlagContext.ts b/src/useFlagContext.ts index d60c83d..673e7cb 100644 --- a/src/useFlagContext.ts +++ b/src/useFlagContext.ts @@ -1,10 +1,61 @@ -import { useContext } from 'react'; -import FlagContext from './FlagContext'; +import { useContext } from "react"; +import FlagContext, { type IFlagContextValue } from "./FlagContext"; +import type { UnleashClient } from "unleash-proxy-client"; + +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 = { + ...methods, + toggles: [], + impressionDataAll: {}, + context: {}, + storage: {}, + start: () => {}, + stop: () => {}, + isReady: () => false, + getError: () => null, + getAllToggles: () => [] +} as unknown as UnleashClient; + +const defaultContextValue: IFlagContextValue = { + ...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) { - throw new Error('This hook must be used within a FlagProvider'); - } - return context; + const context = useContext(FlagContext); + if (!context) { + console.error("useFlagContext() must be used within a FlagProvider"); + return defaultContextValue; + } + return context; } From 75e8a317b609515d2e3e7792fb366cfee329324e Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot <> Date: Tue, 11 Mar 2025 09:10:13 +0000 Subject: [PATCH 5/9] v5.0.0-rc.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a89eb22..3d194a4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@unleash/proxy-client-react", - "version": "5.0.0-rc.0", + "version": "5.0.0-rc.1", "description": "React interface for working with unleash", "type": "module", "main": "./dist/unleash-react.umd.cjs", From 21327f540ba8a8aa5b79f48668eeab14804011c0 Mon Sep 17 00:00:00 2001 From: Fredrik Strand Oseberg Date: Tue, 18 Mar 2025 09:11:46 +0100 Subject: [PATCH 6/9] docs: add note on v4 under the v4 migration header (#195) --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e9ae783..be53ef4 100644 --- a/README.md +++ b/README.md @@ -345,13 +345,12 @@ 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`. +#### 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. + ## 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 code slightly to accomodate the new type changes. - -#### 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. - ## Design philosophy This feature flag SDK is designed according to our design philosophy. You can [read more about that here](https://docs.getunleash.io/topics/feature-flags/feature-flag-best-practices). From e1e5c2827eaab0ab0eb17016a4c4e145a0aeb0b0 Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot <> Date: Tue, 18 Mar 2025 08:16:39 +0000 Subject: [PATCH 7/9] v5.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3d194a4..21e1796 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@unleash/proxy-client-react", - "version": "5.0.0-rc.1", + "version": "5.0.0", "description": "React interface for working with unleash", "type": "module", "main": "./dist/unleash-react.umd.cjs", From 557f32d6e347de64dc5829f5292c1205d374b278 Mon Sep 17 00:00:00 2001 From: Criez's <68372390+Criezc@users.noreply.github.com> Date: Wed, 9 Apr 2025 22:18:46 +0700 Subject: [PATCH 8/9] Fix context value reference changing across rendering (#196) feat: fix context value reference change across render --- src/FlagProvider.tsx | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/src/FlagProvider.tsx b/src/FlagProvider.tsx index de05c05..2943ecc 100644 --- a/src/FlagProvider.tsx +++ b/src/FlagProvider.tsx @@ -1,7 +1,7 @@ /** @format */ -import React, { type FC, type PropsWithChildren, useEffect, useMemo, useState } from 'react'; -import { type IConfig, UnleashClient } from 'unleash-proxy-client'; +import React, { type FC, type PropsWithChildren, useCallback, useEffect, useMemo, useState } from 'react'; +import { type IConfig, IMutableContext, UnleashClient } from 'unleash-proxy-client'; import FlagContext, { type IFlagContextValue } from './FlagContext'; export interface IFlagProvider { @@ -106,20 +106,40 @@ const FlagProvider: FC> = ({ }; }, []); + const on = useCallback(client.current.on, []); + + const off = useCallback(client.current.off, []); + + const isEnabled = useCallback( + (toggleName: string) => client.current.isEnabled(toggleName), + [] + ) + + const updateContext = useCallback( + async (context: IMutableContext) => + await client.current.updateContext(context), + [] + ) + + const getVariant = useCallback( + (toggleName: string) => client.current.getVariant(toggleName), + [] + ) + const context = useMemo( () => ({ - on: ((event, callback, ctx) => client.current.on(event, callback, ctx)) as IFlagContextValue['on'], - off: ((event, callback) => client.current.off(event, callback)) as IFlagContextValue['off'], - updateContext: async (context) => await client.current.updateContext(context), - isEnabled: (toggleName) => client.current.isEnabled(toggleName), - getVariant: (toggleName) => client.current.getVariant(toggleName), + on, + off, + updateContext, + isEnabled, + getVariant, client: client.current, flagsReady, flagsError, setFlagsReady, setFlagsError, }), - [flagsReady, flagsError] + [flagsReady, flagsError, on, off, updateContext, isEnabled, getVariant] ); return ( From b30cd252e33b4f5c55633d29ab07c496b5c87eb1 Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Fri, 11 Apr 2025 21:46:53 +0200 Subject: [PATCH 9/9] revert context method changes (#201) --- src/FlagProvider.tsx | 35 ++++++++--------------------------- 1 file changed, 8 insertions(+), 27 deletions(-) diff --git a/src/FlagProvider.tsx b/src/FlagProvider.tsx index 2943ecc..50331f4 100644 --- a/src/FlagProvider.tsx +++ b/src/FlagProvider.tsx @@ -1,6 +1,6 @@ /** @format */ -import React, { type FC, type PropsWithChildren, useCallback, useEffect, useMemo, useState } from 'react'; +import React, { type FC, type PropsWithChildren, useEffect, useMemo, useState } from 'react'; import { type IConfig, IMutableContext, UnleashClient } from 'unleash-proxy-client'; import FlagContext, { type IFlagContextValue } from './FlagContext'; @@ -106,40 +106,21 @@ const FlagProvider: FC> = ({ }; }, []); - const on = useCallback(client.current.on, []); - - const off = useCallback(client.current.off, []); - - const isEnabled = useCallback( - (toggleName: string) => client.current.isEnabled(toggleName), - [] - ) - - const updateContext = useCallback( - async (context: IMutableContext) => - await client.current.updateContext(context), - [] - ) - - const getVariant = useCallback( - (toggleName: string) => client.current.getVariant(toggleName), - [] - ) - const context = useMemo( () => ({ - on, - off, - updateContext, - isEnabled, - getVariant, + on: (...args) => client.current.on(...args), + off: (...args) => client.current.off(...args), + isEnabled: (...args) => client.current.isEnabled(...args), + updateContext: async (...args) => + await client.current.updateContext(...args), + getVariant: (...args) => client.current.getVariant(...args), client: client.current, flagsReady, flagsError, setFlagsReady, setFlagsError, }), - [flagsReady, flagsError, on, off, updateContext, isEnabled, getVariant] + [flagsReady, flagsError] ); return (