From c32e8f153292f1fe87d0414dabd5543fb5af385f Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Wed, 22 May 2024 14:18:44 +0200 Subject: [PATCH 01/28] chore: update peer dependency version (#167) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 614165b..5db0986 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,6 @@ "vitest": "^0.34.6" }, "peerDependencies": { - "unleash-proxy-client": "^3.2.0" + "unleash-proxy-client": "^3.4.0" } } From 4656873b1ddb699f6268fb0b13c719e451d3a84b Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot <> Date: Wed, 22 May 2024 12:20:37 +0000 Subject: [PATCH 02/28] v4.2.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5db0986..25210e2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@unleash/proxy-client-react", - "version": "4.2.3", + "version": "4.2.4", "description": "React interface for working with unleash", "type": "module", "main": "./dist/unleash-react.umd.cjs", From e87a1aba0234d3498f54dc9c43d6ce72be1f0b07 Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Fri, 28 Jun 2024 11:23:50 +0200 Subject: [PATCH 03/28] fix: update build pipeline (#171) --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 58b08b5..12d4486 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [18.x, 20.x, 21.x] + node-version: [18.x, 20.x, 21.x, 22.x] steps: - uses: actions/checkout@v2 @@ -22,7 +22,7 @@ jobs: node-version: ${{ matrix.node-version }} - name: 📌 Install - run: yarn + run: yarn install --frozen-lockfile - name: 🔨 Build run: yarn build From 9553a709c05b79a245d9713c5cde71383ee42e6b Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Fri, 28 Jun 2024 11:40:26 +0200 Subject: [PATCH 04/28] fix: provider initialization (#169) There was a bug where the flagsReady status, returned from the useFlagsStatus hook, remains false even when an already started Unleash client is passed to the Unleash flag provider. This occurred despite the client being initialized and ready. --- package.json | 4 +- src/FlagContext.ts | 2 +- src/FlagProvider.tsx | 55 +++++++---------- src/useFlagStatus.test.tsx | 120 +++++++++++++++++++++++++++++++++++++ yarn.lock | 8 +-- 5 files changed, 149 insertions(+), 40 deletions(-) create mode 100644 src/useFlagStatus.test.tsx diff --git a/package.json b/package.json index 25210e2..968e253 100644 --- a/package.json +++ b/package.json @@ -50,12 +50,12 @@ "react-dom": "^18.2.0", "react-test-renderer": "^18.2.0", "typescript": "^5.3.2", - "unleash-proxy-client": "^3.4.0", + "unleash-proxy-client": "^3.5.1", "vite": "^4.5.0", "vite-plugin-dts": "^3.6.3", "vitest": "^0.34.6" }, "peerDependencies": { - "unleash-proxy-client": "^3.4.0" + "unleash-proxy-client": "^3.5.1" } } diff --git a/src/FlagContext.ts b/src/FlagContext.ts index 597996c..537720f 100644 --- a/src/FlagContext.ts +++ b/src/FlagContext.ts @@ -4,7 +4,7 @@ import type { UnleashClient } from 'unleash-proxy-client'; export interface IFlagContextValue extends Pick< UnleashClient, - 'on' | 'updateContext' | 'isEnabled' | 'getVariant' + 'on' | 'off' | 'updateContext' | 'isEnabled' | 'getVariant' > { client: UnleashClient; flagsReady: boolean; diff --git a/src/FlagProvider.tsx b/src/FlagProvider.tsx index bb3f9c3..3e5a742 100644 --- a/src/FlagProvider.tsx +++ b/src/FlagProvider.tsx @@ -1,13 +1,14 @@ /** @format */ -import * as React from 'react'; -import { IConfig, UnleashClient } from 'unleash-proxy-client'; -import FlagContext, { IFlagContextValue } from './FlagContext'; +import React, { type FC, type PropsWithChildren, useEffect, useMemo, useState } from 'react'; +import { type IConfig, UnleashClient } from 'unleash-proxy-client'; +import FlagContext, { type IFlagContextValue } from './FlagContext'; export interface IFlagProvider { config?: IConfig; unleashClient?: UnleashClient; startClient?: boolean; + stopClient?: boolean; } const offlineConfig: IConfig = { @@ -24,11 +25,12 @@ const _startTransition = 'startTransition'; // fallback for React <18 which doesn't support startTransition const startTransition = React[_startTransition] || (fn => fn()); -const FlagProvider: React.FC> = ({ +const FlagProvider: FC> = ({ config: customConfig, children, unleashClient, startClient = true, + stopClient = true, }) => { const config = customConfig || offlineConfig; const client = React.useRef( @@ -37,13 +39,13 @@ const FlagProvider: React.FC> = ({ const [flagsReady, setFlagsReady] = React.useState( Boolean( unleashClient - ? customConfig?.bootstrap && customConfig?.bootstrapOverride !== false + ? (customConfig?.bootstrap && customConfig?.bootstrapOverride !== false) || unleashClient.isReady?.() : config.bootstrap && config.bootstrapOverride !== false ) ); - const [flagsError, setFlagsError] = React.useState(null); + const [flagsError, setFlagsError] = useState(client.current.getError?.() || null); - React.useEffect(() => { + useEffect(() => { if (!config && !unleashClient) { console.error( `You must provide either a config or an unleash client to the flag provider. @@ -54,7 +56,7 @@ const FlagProvider: React.FC> = ({ const errorCallback = (e: any) => { startTransition(() => { - setFlagsError(currentError => currentError || e); + setFlagsError((currentError: any) => currentError || e); }); }; @@ -62,9 +64,9 @@ const FlagProvider: React.FC> = ({ startTransition(() => { setFlagsError(null); }); - } + } - let timeout: any; + let timeout: ReturnType | null = null; const readyCallback = () => { // wait for flags to resolve after useFlag gets the same event timeout = setTimeout(() => { @@ -90,8 +92,10 @@ const FlagProvider: React.FC> = ({ if (client.current) { client.current.off('error', errorCallback); client.current.off('ready', readyCallback); - client.current.off('recovered', clearErrorCallback) - client.current.stop(); + client.current.off('recovered', clearErrorCallback); + if (stopClient) { + client.current.stop(); + } } if (timeout) { clearTimeout(timeout); @@ -99,28 +103,13 @@ const FlagProvider: React.FC> = ({ }; }, []); - const updateContext: IFlagContextValue['updateContext'] = async (context) => { - await client.current.updateContext(context); - }; - - const isEnabled: IFlagContextValue['isEnabled'] = (toggleName) => { - return client.current.isEnabled(toggleName); - }; - - const getVariant: IFlagContextValue['getVariant'] = (toggleName) => { - return client.current.getVariant(toggleName); - }; - - const on: IFlagContextValue['on'] = (event, callback, ctx) => { - return client.current.on(event, callback, ctx); - }; - - const context = React.useMemo( + const context = useMemo( () => ({ - on, - updateContext, - isEnabled, - getVariant, + 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), client: client.current, flagsReady, flagsError, diff --git a/src/useFlagStatus.test.tsx b/src/useFlagStatus.test.tsx new file mode 100644 index 0000000..25663ee --- /dev/null +++ b/src/useFlagStatus.test.tsx @@ -0,0 +1,120 @@ +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import useFlagsStatus from './useFlagsStatus'; +import FlagProvider from './FlagProvider'; +import { UnleashClient } from 'unleash-proxy-client'; + +const TestComponent = () => { + const { flagsReady } = useFlagsStatus(); + + return
{flagsReady ? 'flagsReady' : 'loading'}
; +}; + +const ErrorTestComponent = () => { + const { flagsError } = useFlagsStatus(); + + return
{flagsError ? 'flagsError' : 'no issue'}
; +}; + + +const mockClient = { + on: vi.fn(), + off: vi.fn(), + start: vi.fn(), + stop: vi.fn(), + updateContext: vi.fn(), + isEnabled: vi.fn(), + getVariant: vi.fn(), + isReady: vi.fn(), +} as unknown as UnleashClient; + +test('should initialize', async () => { + const onEventHandler = (event: string, callback: () => void) => { + if (event === 'ready') { + callback(); + } + }; + + mockClient.on = onEventHandler as typeof mockClient.on; + + const ui = ( + + + + ); + + render(ui); + + await waitFor(() => { + expect(screen.queryByText('flagsReady')).toBeInTheDocument(); + }); +}); + +// https://github.com/Unleash/proxy-client-react/issues/168 +test('should start when already initialized client is passed', async () => { + const client = new UnleashClient({ + url: 'http://localhost:4242/api', + fetch: async () => + new Promise((resolve) => { + setTimeout(() => + resolve({ + status: 200, + ok: true, + json: async () => ({ + toggles: [], + }), + headers: new Headers(), + }) + ); + }), + clientKey: '123', + appName: 'test', + }); + await client.start(); + expect(client.isReady()).toBe(true); + + const ui = ( + + + + ); + + render(ui); + + await waitFor(() => { + expect(screen.queryByText('flagsReady')).toBeInTheDocument(); + }); +}); + +test('should handle client errors', async () => { + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}); + + const client = new UnleashClient({ + url: 'http://localhost:4242/api', + fetch: async () => { + throw new Error('test error'); + }, + clientKey: '123', + appName: 'test', + }); + + await client.start(); + + const ui = ( + + + + ); + + render(ui); + + await waitFor(() => { + expect(screen.queryByText('flagsError')).toBeInTheDocument(); + }); + + expect(consoleError).toHaveBeenCalledWith( + 'Unleash: unable to fetch feature toggles', + expect.any(Error) + ); + consoleError.mockRestore(); +}); diff --git a/yarn.lock b/yarn.lock index b3bb1eb..f44da1a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1972,10 +1972,10 @@ universalify@^0.2.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== -unleash-proxy-client@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/unleash-proxy-client/-/unleash-proxy-client-3.4.0.tgz#c9c4a8b0f18d77dc0b041eb76478c6ce74c98c1e" - integrity sha512-ivCzm//z+S2T3gSBSZY7HN+5GfoLXZIovMyH6lIZRe2/vCicEdXtXD6cnLTQ2LAiXGV7DpoSM1m8WZGoiLRzkw== +unleash-proxy-client@^3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/unleash-proxy-client/-/unleash-proxy-client-3.5.1.tgz#603af23b4c30e3509b8123e7a9ae99a9c38f335d" + integrity sha512-vfWAozp5O16ZedPPH7wFobsZaj8TQQEp/pfj+4jpWZTnOXyFpH6fAgrztRHO26bQ6iC95vVtfeVRQvgw9lo5zA== dependencies: tiny-emitter "^2.1.0" uuid "^9.0.1" From 29ca66a9dad5c8029449b65a887b936fcf9b3c9f Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot <> Date: Fri, 28 Jun 2024 09:52:25 +0000 Subject: [PATCH 05/28] v4.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 968e253..e351914 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@unleash/proxy-client-react", - "version": "4.2.4", + "version": "4.3.0", "description": "React interface for working with unleash", "type": "module", "main": "./dist/unleash-react.umd.cjs", From e44c17db8ce386c1b10b3e564bbc4fb2a715a8e8 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Mon, 22 Jul 2024 14:29:58 +0200 Subject: [PATCH 06/28] chore: bump JS SDK dependency to `^3.5.2` (#172) This updates the JS SDK dependency to 3.5.2, which contains a fix for an [issue with React Native, where DOMExceptions don't exist](https://github.com/Unleash/unleash-proxy-client-js/issues/221). --- package.json | 4 ++-- yarn.lock | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index e351914..446953e 100644 --- a/package.json +++ b/package.json @@ -50,12 +50,12 @@ "react-dom": "^18.2.0", "react-test-renderer": "^18.2.0", "typescript": "^5.3.2", - "unleash-proxy-client": "^3.5.1", + "unleash-proxy-client": "^3.5.2", "vite": "^4.5.0", "vite-plugin-dts": "^3.6.3", "vitest": "^0.34.6" }, "peerDependencies": { - "unleash-proxy-client": "^3.5.1" + "unleash-proxy-client": "^3.5.2" } } diff --git a/yarn.lock b/yarn.lock index f44da1a..c00d58d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1972,10 +1972,10 @@ universalify@^0.2.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== -unleash-proxy-client@^3.5.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/unleash-proxy-client/-/unleash-proxy-client-3.5.1.tgz#603af23b4c30e3509b8123e7a9ae99a9c38f335d" - integrity sha512-vfWAozp5O16ZedPPH7wFobsZaj8TQQEp/pfj+4jpWZTnOXyFpH6fAgrztRHO26bQ6iC95vVtfeVRQvgw9lo5zA== +unleash-proxy-client@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/unleash-proxy-client/-/unleash-proxy-client-3.5.2.tgz#e5c1735e94ada7abec33958e2b252ba66ad83074" + integrity sha512-fbxTmNyJ/B6uKAZSRcfzZ9IXHokPikWgI14/6DQU3poJjZr+P7hX2KyZbkucd1/0VFYWnNTPAn+ihwyV3C8F/Q== dependencies: tiny-emitter "^2.1.0" uuid "^9.0.1" From 65e3f59006e2a1436cb4322c5866a4420da9a9f1 Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot <> Date: Mon, 22 Jul 2024 12:37:08 +0000 Subject: [PATCH 07/28] v4.3.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 446953e..eec2698 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@unleash/proxy-client-react", - "version": "4.3.0", + "version": "4.3.1", "description": "React interface for working with unleash", "type": "module", "main": "./dist/unleash-react.umd.cjs", From 1d1d3aa02b9d852400fda678c52b73f78529c888 Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Thu, 31 Oct 2024 13:47:14 +0100 Subject: [PATCH 08/28] fix: avoid startTransition in React Native (#178) --- src/FlagProvider.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/FlagProvider.tsx b/src/FlagProvider.tsx index 3e5a742..de05c05 100644 --- a/src/FlagProvider.tsx +++ b/src/FlagProvider.tsx @@ -9,6 +9,7 @@ export interface IFlagProvider { unleashClient?: UnleashClient; startClient?: boolean; stopClient?: boolean; + startTransition?: (fn: () => void) => void; } const offlineConfig: IConfig = { @@ -23,7 +24,8 @@ const offlineConfig: IConfig = { // save startTransition as var to avoid webpack analysis (https://github.com/webpack/webpack/issues/14814) const _startTransition = 'startTransition'; // fallback for React <18 which doesn't support startTransition -const startTransition = React[_startTransition] || (fn => fn()); +// Fallback for React <18 and exclude startTransition if in React Native +const defaultStartTransition = React[_startTransition] || (fn => fn()); const FlagProvider: FC> = ({ config: customConfig, @@ -31,6 +33,7 @@ const FlagProvider: FC> = ({ unleashClient, startClient = true, stopClient = true, + startTransition = defaultStartTransition }) => { const config = customConfig || offlineConfig; const client = React.useRef( From 1dd2c690cedfbb80aa9c3060f2e3ba395fc869b5 Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot <> Date: Thu, 31 Oct 2024 12:49:30 +0000 Subject: [PATCH 09/28] v4.4.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index eec2698..d55703c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@unleash/proxy-client-react", - "version": "4.3.1", + "version": "4.4.0", "description": "React interface for working with unleash", "type": "module", "main": "./dist/unleash-react.umd.cjs", From dad10dfc97491465caf1b71830fb3d7c8fdb48a5 Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Fri, 29 Nov 2024 10:31:48 +0100 Subject: [PATCH 10/28] docs: startTransition in React Native (#181) --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index ce2078f..bde1e50 100644 --- a/README.md +++ b/README.md @@ -265,6 +265,8 @@ Wrap your components like so: ## React Native +### localStorage + IMPORTANT: This no longer comes included in the unleash-proxy-client-js library. You will need to install the storage adapter for your preferred storage solution. Because React Native doesn't run in a web browser, it doesn't have access to the `localStorage` API. Instead, you need to tell Unleash to use your specific storage provider. The most common storage provider for React Native is [AsyncStorage](https://github.com/react-native-async-storage/async-storage). @@ -282,6 +284,13 @@ const config = { }; ``` +### startTransition + +If your version of React Native doesn't support `startTransition`, you can provide fallback implementation: +```jsx + fn()} > +``` + # Migration guide ## Upgrade path from v1 -> v2 From 87555d168429e5d0e588609870e16d3b9497ddaf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Dec 2024 11:00:23 +0100 Subject: [PATCH 11/28] chore(deps): bump nanoid from 3.3.6 to 3.3.8 (#182) Bumps [nanoid](https://github.com/ai/nanoid) from 3.3.6 to 3.3.8. - [Release notes](https://github.com/ai/nanoid/releases) - [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md) - [Commits](https://github.com/ai/nanoid/compare/3.3.6...3.3.8) --- updated-dependencies: - dependency-name: nanoid dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index c00d58d..27b2502 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1545,9 +1545,9 @@ muggle-string@^0.3.1: integrity sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg== nanoid@^3.3.6: - version "3.3.6" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" - integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== + version "3.3.8" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" + integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== node-releases@^2.0.12: version "2.0.12" From 4dde7bf8770da24278c9b515cff051fe42c84cae Mon Sep 17 00:00:00 2001 From: Fredrik Strand Oseberg Date: Tue, 14 Jan 2025 15:45:41 +0100 Subject: [PATCH 12/28] Docs/document events (#184) --- README.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/README.md b/README.md index bde1e50..ae41aa8 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,12 @@ yarn add @unleash/proxy-client-react unleash-proxy-client # How to use +This library uses the core [unleash-proxy-client](https://github.com/Unleash/unleash-proxy-client-js) JS client as a base. + ## Initialize the client +*NOTE*: [unleash-proxy](https://github.com/Unleash/unleash-proxy) is in maintenance mode. It is recommend to use the [Frontend API](https://docs.getunleash.io/reference/front-end-api) or [unleash-edge](https://github.com/Unleash/unleash-edge) instead. + Prepare [Unleash Proxy](https://docs.getunleash.io/reference/unleash-proxy) secret or [Frontend API Access](https://docs.getunleash.io/reference/front-end-api) token. @@ -42,6 +46,8 @@ root.render( To connect this SDK to your Unleash instance's [front-end API](https://docs.getunleash.io/reference/front-end-api), use the URL to your Unleash instance's front-end API (`/api/frontend`) as the `url` parameter. For the `clientKey` parameter, use a `FRONTEND` token generated from your Unleash instance. Refer to the [_how to create API tokens_](https://docs.getunleash.io/how-to/how-to-create-api-tokens) guide for the necessary steps. +To connect this SDK to unleash-edge, follow the documentation provided in the [unleash-edge repository](https://github.com/unleash/unleash-edge). + To connect this SDK to the [Unleash proxy](https://docs.getunleash.io/reference/unleash-proxy), use the proxy's URL and a [proxy client key](https://docs.getunleash.io/reference/api-tokens-and-client-keys#proxy-client-keys). The [_configuration_ section of the Unleash proxy docs](https://docs.getunleash.io/reference/unleash-proxy#configuration) contains more info on how to configure client keys for your proxy. ## Check feature toggle status @@ -134,6 +140,36 @@ const MyComponent = ({ userId }) => { }; ``` +## Listening to events + +The core JavaScript client emits various types of events depending on internal activities. You can listen to these events by using a hook to access the client and then directly attaching event listeners. Alternatively, if you're using the FlagProvider with a client, you can directly use this client to listen to the events. [See the full list of events here.](https://github.com/Unleash/unleash-proxy-client-js?tab=readme-ov-file#available-events) + +NOTE: `FlagProvider` uses these internal events to provide information through `useFlagsStatus`. + +```jsx +import { useUnleashContext, useFlag } from '@unleash/proxy-client-react'; + +const MyComponent = ({ userId }) => { + const client = useUnleashClient(); + + useEffect(() => { + if (client) { + const handleError = () => { + // Handle error + } + + client.on('error', handleError) + } + + return () => { + client.off('error', handleError) + } + }, [client]) + + // ...rest of component +}; +``` + # Advanced use cases ## Deferring client start From a3e78363246e58eee74b822cb7bed0e9b893d7b1 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Wed, 15 Jan 2025 13:41:20 +0100 Subject: [PATCH 13/28] Meta: update release template to use npm release v2 (#186) Updates the release template to use the new npm release v2 action, which relies on the GitHub bot instead of a personal PAT. --- .github/workflows/release.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5af9618..cded89c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,10 +15,11 @@ on: jobs: from-template: - uses: Unleash/.github/.github/workflows/npm-release.yml@v1.1.0 + uses: Unleash/.github/.github/workflows/npm-release.yml@v2.0.0 with: version: ${{ github.event.inputs.version }} tag: ${{ github.event.inputs.tag }} secrets: - GH_ACCESS_TOKEN: ${{ secrets.GH_TOKEN}} NPM_ACCESS_TOKEN: ${{ secrets.NPM_TOKEN }} + UNLEASH_BOT_APP_ID: ${{ secrets.UNLEASH_BOT_APP_ID }} + UNLEASH_BOT_PRIVATE_KEY: ${{ secrets.UNLEASH_BOT_PRIVATE_KEY }} From 5d59503bc308f22ada362771a13ab2a06741f456 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Thu, 16 Jan 2025 08:22:37 +0100 Subject: [PATCH 14/28] feat: add sdk uniqueness headers (#185) Updates the dependency on the underlying Unleash client to 3.7.1. This version of the client adds new x-unleash headers which lets Unleash count unique connections and see SDK version information. --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index d55703c..a596b78 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "react-dom": "^18.2.0", "react-test-renderer": "^18.2.0", "typescript": "^5.3.2", - "unleash-proxy-client": "^3.5.2", + "unleash-proxy-client": "^3.7.1", "vite": "^4.5.0", "vite-plugin-dts": "^3.6.3", "vitest": "^0.34.6" diff --git a/yarn.lock b/yarn.lock index 27b2502..c6d81d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1972,10 +1972,10 @@ universalify@^0.2.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== -unleash-proxy-client@^3.5.2: - version "3.5.2" - resolved "https://registry.yarnpkg.com/unleash-proxy-client/-/unleash-proxy-client-3.5.2.tgz#e5c1735e94ada7abec33958e2b252ba66ad83074" - integrity sha512-fbxTmNyJ/B6uKAZSRcfzZ9IXHokPikWgI14/6DQU3poJjZr+P7hX2KyZbkucd1/0VFYWnNTPAn+ihwyV3C8F/Q== +unleash-proxy-client@^3.7.1: + version "3.7.1" + resolved "https://registry.yarnpkg.com/unleash-proxy-client/-/unleash-proxy-client-3.7.1.tgz#c51483aebaad664e6d6ea70828f5f10bece61a40" + integrity sha512-ri3cauGfQBjvRvwwJIQfnHlpOfFvQz0iy5hVd82Tr4t0LkqpqFr248tuO8jkI39JXelUcX7TCGNHneeMMPZM1A== dependencies: tiny-emitter "^2.1.0" uuid "^9.0.1" From 971ab39d11b2ba72ff76ea9c6a0d9aa598046d51 Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot <> Date: Thu, 16 Jan 2025 10:15:10 +0000 Subject: [PATCH 15/28] v4.5.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a596b78..49a9fc8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@unleash/proxy-client-react", - "version": "4.4.0", + "version": "4.5.0", "description": "React interface for working with unleash", "type": "module", "main": "./dist/unleash-react.umd.cjs", From 6574bf697ba432da60ce6831725a034bf6f5be31 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Thu, 16 Jan 2025 13:56:03 +0100 Subject: [PATCH 16/28] fix: update proxy client dep to 3.7.2 (#187) * fix: update proxy client dep to 3.7.2 This update includes uuid generation that doesn't rely on the `crypto` module for connection id. * fix: also update peer dep --- package.json | 4 ++-- yarn.lock | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 49a9fc8..04a4733 100644 --- a/package.json +++ b/package.json @@ -50,12 +50,12 @@ "react-dom": "^18.2.0", "react-test-renderer": "^18.2.0", "typescript": "^5.3.2", - "unleash-proxy-client": "^3.7.1", + "unleash-proxy-client": "^3.7.2", "vite": "^4.5.0", "vite-plugin-dts": "^3.6.3", "vitest": "^0.34.6" }, "peerDependencies": { - "unleash-proxy-client": "^3.5.2" + "unleash-proxy-client": "^3.7.2" } } diff --git a/yarn.lock b/yarn.lock index c6d81d0..97fb114 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1972,10 +1972,10 @@ universalify@^0.2.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== -unleash-proxy-client@^3.7.1: - version "3.7.1" - resolved "https://registry.yarnpkg.com/unleash-proxy-client/-/unleash-proxy-client-3.7.1.tgz#c51483aebaad664e6d6ea70828f5f10bece61a40" - integrity sha512-ri3cauGfQBjvRvwwJIQfnHlpOfFvQz0iy5hVd82Tr4t0LkqpqFr248tuO8jkI39JXelUcX7TCGNHneeMMPZM1A== +unleash-proxy-client@^3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/unleash-proxy-client/-/unleash-proxy-client-3.7.2.tgz#c6166bbaf293f8dea12cf65d061d72234c5b76a8" + integrity sha512-1SvHsl3kQh1DT9EKMQsN9alOvXZEz9hpxa3mG6QWtTmXJqa6VZi25dQ2U8Y2KAULKg6ARLMUQkod74Fe/pKp0g== dependencies: tiny-emitter "^2.1.0" uuid "^9.0.1" From 6c75948a3b6819a8b9fc7301b9e4e1981d652237 Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot <> Date: Thu, 16 Jan 2025 13:00:41 +0000 Subject: [PATCH 17/28] v4.5.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 04a4733..5df2724 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@unleash/proxy-client-react", - "version": "4.5.0", + "version": "4.5.1", "description": "React interface for working with unleash", "type": "module", "main": "./dist/unleash-react.umd.cjs", From 2abbff7fde7eb9ab54ab125d488eb3161dae0a04 Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Wed, 5 Feb 2025 12:10:31 +0100 Subject: [PATCH 18/28] chore: update js client (#189) --- package.json | 4 ++-- yarn.lock | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 5df2724..31f4306 100644 --- a/package.json +++ b/package.json @@ -50,12 +50,12 @@ "react-dom": "^18.2.0", "react-test-renderer": "^18.2.0", "typescript": "^5.3.2", - "unleash-proxy-client": "^3.7.2", + "unleash-proxy-client": "^3.7.3", "vite": "^4.5.0", "vite-plugin-dts": "^3.6.3", "vitest": "^0.34.6" }, "peerDependencies": { - "unleash-proxy-client": "^3.7.2" + "unleash-proxy-client": "^3.7.3" } } diff --git a/yarn.lock b/yarn.lock index 97fb114..d27d8dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1972,10 +1972,10 @@ universalify@^0.2.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== -unleash-proxy-client@^3.7.2: - version "3.7.2" - resolved "https://registry.yarnpkg.com/unleash-proxy-client/-/unleash-proxy-client-3.7.2.tgz#c6166bbaf293f8dea12cf65d061d72234c5b76a8" - integrity sha512-1SvHsl3kQh1DT9EKMQsN9alOvXZEz9hpxa3mG6QWtTmXJqa6VZi25dQ2U8Y2KAULKg6ARLMUQkod74Fe/pKp0g== +unleash-proxy-client@^3.7.3: + version "3.7.3" + resolved "https://registry.yarnpkg.com/unleash-proxy-client/-/unleash-proxy-client-3.7.3.tgz#2f69b4d4a3ea673f6f1c647b50e5a4433b955045" + integrity sha512-Cd7ovrhAIpwveNFdtpzs7HO0WeArC2fbjIVGL2Cjza8bHD+jJ1JbSuy3tFuKvvUkbVKq/EGV0RgosEa/3UVLgg== dependencies: tiny-emitter "^2.1.0" uuid "^9.0.1" From 55db6820f46af99a9d33b2fd991a2eb3ef35a866 Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot <> Date: Wed, 5 Feb 2025 11:31:01 +0000 Subject: [PATCH 19/28] v4.5.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 31f4306..ccbe371 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@unleash/proxy-client-react", - "version": "4.5.1", + "version": "4.5.2", "description": "React interface for working with unleash", "type": "module", "main": "./dist/unleash-react.umd.cjs", From cf9d52c302bd093f9141339cefd0d14ce1756cc2 Mon Sep 17 00:00:00 2001 From: Przemek Maszczynski Date: Fri, 7 Feb 2025 10:39:49 +0100 Subject: [PATCH 20/28] 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 21/28] 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 22/28] 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 23/28] 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 24/28] 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 25/28] 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 26/28] 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 27/28] 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 28/28] 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 (