├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── actions │ ├── publish-docs │ │ └── action.yml │ └── publish-npm │ │ └── action.yml ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── ci.yml │ ├── manual-publish-docs.yml │ ├── manual-publish.yml │ ├── release-please.yml │ └── stale.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── .release-please-manifest.json ├── .sdk_metadata.json ├── CHANGELOG.md ├── CODEOWNERS ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── SECURITY.md ├── eslint.config.mjs ├── examples ├── async-provider │ ├── .babelrc │ ├── .eslintrc │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── src │ │ ├── client │ │ │ └── index.js │ │ ├── server │ │ │ ├── index.js │ │ │ └── server.js │ │ └── universal │ │ │ ├── app.js │ │ │ ├── home.js │ │ │ ├── hooksDemo.js │ │ │ └── siteNav.js │ ├── webpack.config.client.js │ ├── webpack.config.server.js │ └── yarn.lock ├── deferred-initialization │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ └── index.html │ ├── src │ │ ├── App.css │ │ ├── App.test.tsx │ │ ├── App.tsx │ │ ├── index.css │ │ ├── index.tsx │ │ ├── logo.svg │ │ ├── react-app-env.d.ts │ │ ├── reportWebVitals.ts │ │ ├── setupTests.ts │ │ └── welcome.tsx │ └── tsconfig.json ├── hoc │ ├── .babelrc │ ├── .eslintrc │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── src │ │ ├── client │ │ │ └── index.js │ │ ├── server │ │ │ ├── index.js │ │ │ └── server.js │ │ └── universal │ │ │ ├── app.js │ │ │ ├── home.js │ │ │ ├── hooksDemo.js │ │ │ └── siteNav.js │ ├── webpack.config.client.js │ ├── webpack.config.server.js │ └── yarn.lock └── typescript │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ └── index.html │ ├── src │ ├── App.css │ ├── App.test.tsx │ ├── App.tsx │ ├── index.css │ ├── index.tsx │ ├── logo.svg │ ├── react-app-env.d.ts │ ├── reportWebVitals.ts │ └── setupTests.ts │ └── tsconfig.json ├── jest.config.js ├── link-dev.sh ├── package.json ├── release-please-config.json ├── rollup.config.ts ├── scripts ├── better-audit.sh ├── packaging-test.sh └── publish-npm.sh ├── src ├── __snapshots__ │ ├── asyncWithLDProvider.test.tsx.snap │ ├── provider.test.tsx.snap │ ├── withLDConsumer.test.tsx.snap │ └── withLDProvider.test.tsx.snap ├── asyncWithLDProvider.test.tsx ├── asyncWithLDProvider.tsx ├── context.ts ├── getFlagsProxy.test.ts ├── getFlagsProxy.ts ├── index.ts ├── provider.test.tsx ├── provider.tsx ├── providerState.ts ├── types.ts ├── useFlags.ts ├── useLDClient.ts ├── useLDClientError.tsx ├── utils.test.ts ├── utils.ts ├── withLDConsumer.test.tsx ├── withLDConsumer.tsx ├── withLDProvider.test.tsx ├── withLDProvider.tsx └── wrapperOptions.ts ├── tsconfig.json └── typedoc.json /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is this a support request?** 11 | This issue tracker is maintained by LaunchDarkly SDK developers and is intended for feedback on the SDK code. If you're not sure whether the problem you are having is specifically related to the SDK, or to the LaunchDarkly service overall, it may be more appropriate to contact the LaunchDarkly support team; they can help to investigate the problem and will consult the SDK team if necessary. You can submit a support request by going [here](https://support.launchdarkly.com/hc/en-us/requests/new) or by emailing support@launchdarkly.com. 12 | 13 | Note that issues filed on this issue tracker are publicly accessible. Do not provide any private account information on your issues. If your problem is specific to your account, you should submit a support request as described above. 14 | 15 | **Describe the bug** 16 | A clear and concise description of what the bug is. 17 | 18 | **To reproduce** 19 | Steps to reproduce the behavior. 20 | 21 | **Expected behavior** 22 | A clear and concise description of what you expected to happen. 23 | 24 | **Logs** 25 | If applicable, add any log output related to your problem. 26 | 27 | **SDK version** 28 | The version of this SDK that you are using. 29 | 30 | **Language version, developer tools** 31 | For instance, Go 1.11 or Ruby 2.5.3. If you are using a language that requires a separate compiler, such as C, please include the name and version of the compiler too. 32 | 33 | **OS/platform** 34 | For instance, Ubuntu 16.04, Windows 10, or Android 4.0.3. If your code is running in a browser, please also include the browser type and version. 35 | 36 | **Additional context** 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Support request 4 | url: https://support.launchdarkly.com/hc/en-us/requests/new 5 | about: File your support requests with LaunchDarkly's support team 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I would love to see the SDK [...does something new...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/actions/publish-docs/action.yml: -------------------------------------------------------------------------------- 1 | name: Publish Documentation 2 | description: 'Publish documentation to github pages.' 3 | 4 | inputs: 5 | github_token: 6 | description: 'The github token to use for committing' 7 | required: true 8 | 9 | runs: 10 | using: composite 11 | steps: 12 | - uses: launchdarkly/gh-actions/actions/publish-pages@publish-pages-v1.0.2 13 | name: 'Publish to Github pages' 14 | with: 15 | docs_path: docs 16 | github_token: ${{ inputs.github_token }} 17 | -------------------------------------------------------------------------------- /.github/actions/publish-npm/action.yml: -------------------------------------------------------------------------------- 1 | name: Publish to NPM 2 | description: Publish an npm package. 3 | inputs: 4 | prerelease: 5 | description: 'Is this a prerelease. If so, then the latest tag will not be updated in npm.' 6 | required: false 7 | dry-run: 8 | description: 'Is this a dry run. If so no package will be published.' 9 | required: false 10 | 11 | runs: 12 | using: composite 13 | steps: 14 | - name: Publish 15 | shell: bash 16 | run: | 17 | ./scripts/publish-npm.sh 18 | env: 19 | LD_RELEASE_IS_PRERELEASE: ${{ inputs.prerelease }} 20 | LD_RELEASE_IS_DRYRUN: ${{ inputs.dry-run }} 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "npm" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | **Requirements** 2 | 3 | - [ ] I have added test coverage for new or changed functionality 4 | - [ ] I have followed the repository's [pull request submission guidelines](../blob/main/CONTRIBUTING.md#submitting-pull-requests) 5 | - [ ] I have validated my changes against all supported platform versions 6 | 7 | **Related issues** 8 | 9 | Provide links to any issues in this repository or elsewhere relating to this pull request. 10 | 11 | **Describe the solution you've provided** 12 | 13 | Provide a clear and concise description of what you expect to happen. 14 | 15 | **Describe alternatives you've considered** 16 | 17 | Provide a clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | 21 | Add any other context about the pull request here. 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths-ignore: 7 | - '**.md' #Do not need to run CI for markdown changes. 8 | pull_request: 9 | branches: [main] 10 | paths-ignore: 11 | - '**.md' 12 | 13 | jobs: 14 | build-test: 15 | strategy: 16 | matrix: 17 | variations: [ 18 | # {os: ubuntu-latest, node: latest}, 19 | {os: ubuntu-latest, node: 'lts/*'}, 20 | ] 21 | 22 | runs-on: ${{ matrix.variations.os }} 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: actions/setup-node@v4 27 | with: 28 | node-version: ${{ matrix.variations.node }} 29 | registry-url: 'https://registry.npmjs.org' 30 | - name: Install 31 | run: npm install 32 | - name: Build 33 | run: npm run build 34 | - name: Test 35 | run: npm test 36 | - name: Lint 37 | run: npm run lint 38 | - name: Build Docs 39 | run: npm run doc 40 | -------------------------------------------------------------------------------- /.github/workflows/manual-publish-docs.yml: -------------------------------------------------------------------------------- 1 | name: Manual Publish Docs 2 | on: 3 | workflow_dispatch: 4 | 5 | jobs: 6 | publish-package: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | id-token: write 10 | contents: write 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version: 20.x 17 | registry-url: 'https://registry.npmjs.org' 18 | 19 | - name: Install Dependencies 20 | run: npm install 21 | 22 | - name: Build Documentation 23 | run: npm run doc 24 | 25 | - id: publish-docs 26 | name: Publish Documentation 27 | uses: ./.github/actions/publish-docs 28 | with: 29 | github_token: ${{ secrets.GITHUB_TOKEN }} 30 | -------------------------------------------------------------------------------- /.github/workflows/manual-publish.yml: -------------------------------------------------------------------------------- 1 | name: Manual Publish Package 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | dry-run: 6 | description: 'Is this a dry run. If so no package will be published.' 7 | type: boolean 8 | required: true 9 | prerelease: 10 | description: 'Is this a prerelease. If so, then the latest tag will not be updated in npm.' 11 | type: boolean 12 | required: true 13 | 14 | jobs: 15 | publish-package: 16 | runs-on: ubuntu-latest 17 | permissions: 18 | id-token: write 19 | contents: write 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - uses: actions/setup-node@v4 24 | with: 25 | node-version: 20.x 26 | registry-url: 'https://registry.npmjs.org' 27 | 28 | - uses: launchdarkly/gh-actions/actions/release-secrets@release-secrets-v1.2.0 29 | name: 'Get NPM token' 30 | with: 31 | aws_assume_role: ${{ vars.AWS_ROLE_ARN }} 32 | ssm_parameter_pairs: '/production/common/releasing/npm/token = NODE_AUTH_TOKEN' 33 | 34 | - name: Install Dependencies 35 | run: npm install 36 | 37 | - id: publish-npm 38 | name: Publish NPM Package 39 | uses: ./.github/actions/publish-npm 40 | with: 41 | dry-run: ${{ inputs.dry-run }} 42 | prerelease: ${{ inputs.prerelease }} 43 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | name: Release Please 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release-please: 10 | runs-on: ubuntu-latest 11 | outputs: 12 | release_created: ${{ steps.release.outputs.release_created }} 13 | steps: 14 | - uses: googleapis/release-please-action@v4 15 | id: release 16 | with: 17 | token: ${{secrets.GITHUB_TOKEN}} 18 | 19 | publish-package: 20 | runs-on: ubuntu-latest 21 | needs: ['release-please'] 22 | permissions: 23 | id-token: write 24 | contents: write 25 | if: ${{ needs.release-please.outputs.release_created == 'true' }} 26 | steps: 27 | - uses: actions/checkout@v4 28 | 29 | - uses: actions/setup-node@v4 30 | with: 31 | node-version: 20.x 32 | registry-url: 'https://registry.npmjs.org' 33 | 34 | - uses: launchdarkly/gh-actions/actions/release-secrets@release-secrets-v1.2.0 35 | name: 'Get NPM token' 36 | with: 37 | aws_assume_role: ${{ vars.AWS_ROLE_ARN }} 38 | ssm_parameter_pairs: '/production/common/releasing/npm/token = NODE_AUTH_TOKEN' 39 | 40 | - name: Install Dependencies 41 | run: npm install 42 | # Publishing will build because we have a prepublish script. 43 | 44 | - id: publish-npm 45 | name: Publish NPM Package 46 | uses: ./.github/actions/publish-npm 47 | with: 48 | dry-run: 'false' 49 | prerelease: 'false' 50 | 51 | - name: Build Documentation 52 | run: npm run doc 53 | 54 | - id: publish-docs 55 | name: Publish Documentation 56 | uses: ./.github/actions/publish-docs 57 | with: 58 | github_token: ${{ secrets.GITHUB_TOKEN }} 59 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | # Happen once per day at 1:30 AM 6 | - cron: '30 1 * * *' 7 | 8 | jobs: 9 | sdk-close-stale: 10 | uses: launchdarkly/gh-actions/.github/workflows/sdk-stale.yml@main 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .parcel-cache 3 | **/junit.xml 4 | npm-debug.log 5 | yarn-error.log 6 | pnpm-debug.log 7 | node_modules 8 | dist 9 | lib 10 | .idea 11 | .vscode/ 12 | test-types.js 13 | docs/build/ 14 | yarn.lock 15 | package-lock.json 16 | pnpm-lock.yaml 17 | .npmrc 18 | docs/ 19 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !lib/* -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 120, 5 | "arrowParens": "always" 6 | } 7 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "3.8.1" 3 | } 4 | -------------------------------------------------------------------------------- /.sdk_metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "sdks": { 4 | "react-client-sdk": { 5 | "name": "React Web SDK", 6 | "type": "client-side", 7 | "languages": [ 8 | "JavaScript", "TypeScript" 9 | ], 10 | "wrapperNames": ["react-client-sdk"] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | All notable changes to the LaunchDarkly Client-side SDK for React will be documented in this file. For the source code for versions 2.13.0 and earlier, see the corresponding tags in the [js-client-sdk](https://github.com/launchdarkly/js-client-sdk) repository; this code was previously in a monorepo package there. See also the [JavaScript SDK changelog](https://github.com/launchdarkly/js-client-sdk/blob/main/CHANGELOG.md), since the React SDK inherits all of the underlying functionality of the JavaScript SDK; this file covers only changes that are specific to the React interface. This project adheres to [Semantic Versioning](http://semver.org). 4 | 5 | ## [3.8.1](https://github.com/launchdarkly/react-client-sdk/compare/launchdarkly-react-client-sdk-v3.8.0...launchdarkly-react-client-sdk-v3.8.1) (2025-05-30) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * Update to js-client-sdk 3.8.1 for docs update ([#360](https://github.com/launchdarkly/react-client-sdk/issues/360)) ([a4cb4e3](https://github.com/launchdarkly/react-client-sdk/commit/a4cb4e33615dc25bcc50372ad14a435f54cb0b56)) 11 | 12 | ## [3.8.0](https://github.com/launchdarkly/react-client-sdk/compare/launchdarkly-react-client-sdk-v3.7.0...launchdarkly-react-client-sdk-v3.8.0) (2025-05-28) 13 | 14 | 15 | ### Features 16 | 17 | * Add support for per-context summary events. ([#358](https://github.com/launchdarkly/react-client-sdk/issues/358)) ([42c132e](https://github.com/launchdarkly/react-client-sdk/commit/42c132ec509caa996acf7bae179aa179cd3c7e3f)) 18 | 19 | ## [3.7.0](https://github.com/launchdarkly/react-client-sdk/compare/launchdarkly-react-client-sdk-v3.6.1...launchdarkly-react-client-sdk-v3.7.0) (2025-05-14) 20 | 21 | 22 | ### Features 23 | 24 | * Add hook and plugin support. ([#352](https://github.com/launchdarkly/react-client-sdk/issues/352)) ([6928cc6](https://github.com/launchdarkly/react-client-sdk/commit/6928cc6fd91d4fa47fdc0e18be3dec358453d90c)) 25 | 26 | ## [3.6.1](https://github.com/launchdarkly/react-client-sdk/compare/launchdarkly-react-client-sdk-v3.6.0...launchdarkly-react-client-sdk-v3.6.1) (2025-01-30) 27 | 28 | 29 | ### Bug Fixes 30 | 31 | * update package.json to support react 19 ([#341](https://github.com/launchdarkly/react-client-sdk/issues/341)) ([2c0fc33](https://github.com/launchdarkly/react-client-sdk/commit/2c0fc335eb22262a37a2dcc049771db0b9f862c4)) 32 | 33 | ## [3.6.0](https://github.com/launchdarkly/react-client-sdk/compare/launchdarkly-react-client-sdk-v3.5.0...launchdarkly-react-client-sdk-v3.6.0) (2024-11-01) 34 | 35 | 36 | ### Features 37 | 38 | * Support for providing custom contexts ([#313](https://github.com/launchdarkly/react-client-sdk/issues/313)) ([a8b52f4](https://github.com/launchdarkly/react-client-sdk/commit/a8b52f48b0fa92ee83ace93ae631d5431ccb54ea)) 39 | * **typescript:** export the LDProps interface for access in application code ([#321](https://github.com/launchdarkly/react-client-sdk/issues/321)) ([7a084c5](https://github.com/launchdarkly/react-client-sdk/commit/7a084c5c4b2fc84ba7c8b23c0a8bed29a3d944da)) 40 | 41 | ## [3.5.0](https://github.com/launchdarkly/react-client-sdk/compare/launchdarkly-react-client-sdk-v3.4.0...launchdarkly-react-client-sdk-v3.5.0) (2024-10-18) 42 | 43 | 44 | ### Features 45 | 46 | * Add support for client-side prerequisite events. ([3cb6060](https://github.com/launchdarkly/react-client-sdk/commit/3cb606033d418a08144f88e0f632433951216c9c)) 47 | * Add support for synchronous inspectors. ([3cb6060](https://github.com/launchdarkly/react-client-sdk/commit/3cb606033d418a08144f88e0f632433951216c9c)) 48 | 49 | ## [3.4.0](https://github.com/launchdarkly/react-client-sdk/compare/launchdarkly-react-client-sdk-v3.3.2...launchdarkly-react-client-sdk-v3.4.0) (2024-08-02) 50 | 51 | 52 | ### Features 53 | 54 | * Support a pre-initialized client using asyncWithLDProvider. ([#310](https://github.com/launchdarkly/react-client-sdk/issues/310)) ([6f3ad5c](https://github.com/launchdarkly/react-client-sdk/commit/6f3ad5caa600a596c6c1f46621e971ff5bef9cbb)) 55 | 56 | ## [3.3.2] - 2024-05-28 57 | ### Fixed: 58 | - Set esbuild to target ES2015 aka ES6 so the build is backwards compatible. This is needed because `rollup-plugin-esbuild` [v6](https://github.com/egoist/rollup-plugin-esbuild/releases/tag/v6.0.0) now defaults to es2020 by default and no longer respects tsconfig.json's target. Fixes this [issue](https://github.com/launchdarkly/react-client-sdk/issues/283#issuecomment-2133796297) in #283. 59 | 60 | ## [3.3.1] - 2024-05-28 61 | ### Fixed: 62 | - Fixed a bug introduced after the init timeout change. The ldClient object was omitted from provider state, causing the useLDClient hook to return undefined. 63 | 64 | ## [3.3.0] - 2024-05-23 65 | ### Added: 66 | - Added a new optional timeout to ProviderConfig which gets passed to the underlying Javascript SDK `waitForInitialization` function. 67 | 68 | ## [3.2.0] - 2024-05-01 69 | ### Added: 70 | - Added an optional timeout to the `waitForInitialization` method. When a timeout is specified the returned promise will be rejected after the timeout elapses if the client has not finished initializing within that time. When no timeout is specified the returned promise will not be resolved or rejected until the initialization either completes or fails. 71 | 72 | ### Changed: 73 | - The track method now validates that the provided metricValue is a number. If a metric value is provided, and it is not a number, then a warning will be logged. 74 | 75 | - Updated the link-dev script to better support multiple platforms. 76 | 77 | ### Fixed: 78 | - Fixed the documentation for `evaluationReasons` for the `identify` method. 79 | 80 | ## [3.1.0] - 2024-03-21 81 | ### Changed: 82 | - Redact anonymous attributes within feature events 83 | - Always inline contexts for feature events 84 | 85 | ### Fixed: 86 | - Pin dev version of node to compatible types. 87 | 88 | ## [3.0.10] - 2023-12-11 89 | ### Fixed: 90 | - Added deferred initialization example. 91 | - Fixes #228. Bumped js sdk dependency. 92 | 93 | ## [3.0.9] - 2023-08-21 94 | ### Fixed: 95 | - Types from the js client sdk are now re-exported in the react sdk there's no need to separately install the js sdk just for types. 96 | 97 | ## [3.0.8] - 2023-07-14 98 | ### Fixed: 99 | - #202 Use bootstrap values if there's an error initializing the client. 100 | - #208 Add `types` to `exports` field in package.json to fix typescript compilation error when using `moduleResolution: bundler`. 101 | 102 | ## [3.0.7] - 2023-07-12 103 | ### Changed: 104 | - This release introduces rollup to bundle the build output. The bundle is minified and supports both cjs and esm. 105 | 106 | ## [3.0.6] - 2023-04-12 107 | ### Fixed: 108 | - Rolled back parcel due to erroneous types being generated issue [197](https://github.com/launchdarkly/react-client-sdk/issues/197). 109 | 110 | ## [3.0.5] - 2023-04-12 111 | ### Added: 112 | - Use parcel to reduce the bundle size. 113 | 114 | ### Changed: 115 | - Updated to `js-client-sdk` `3.1.3`. 116 | 117 | ### Fixed: 118 | - Fixed an issue that was preventing page view/click events from being sent. (fixed by `js-client-sdk` `3.1.3`) 119 | 120 | ## [3.0.4] - 2023-03-21 121 | ### Changed: 122 | - Update `LDContext` to allow for key to be optional. This is used when making an anonymous context with a generated key. 123 | 124 | ## [3.0.3] - 2023-03-01 125 | ### Fixed: 126 | - Bugfix for [#180](https://github.com/launchdarkly/react-client-sdk/issues/180) where feature events are not sent when camel case is false. 127 | 128 | ## [3.0.2] - 2023-02-15 129 | ### Changed: 130 | - Upgrade to `js-client-sdk` version `3.1.1`. This removes usage of optional chaining (`?.`) to allow for use with older transpilers. 131 | 132 | ## [3.0.1] - 2022-12-20 133 | ### Fixed: 134 | - We removed unnecessary Proxy overrides to keep compatibility with [proxy-polyfill](https://github.com/GoogleChrome/proxy-polyfill). This was reported in issue [#174](https://github.com/launchdarkly/react-client-sdk/issues/174). 135 | 136 | ## [3.0.0] - 2022-12-07 137 | The latest version of this SDK supports LaunchDarkly's new custom contexts feature. Contexts are an evolution of a previously-existing concept, "users." For more information please read the [JavaScript SDK's latest release notes](https://github.com/launchdarkly/js-client-sdk/releases/tag/3.0.0). 138 | 139 | For detailed information about this version, please refer to the list below. For information on how to upgrade from the previous version, please read the [migration guide](https://docs.launchdarkly.com/sdk/client-side/react/web-migration-2-to-3). 140 | 141 | ### Added: 142 | 143 | - The `context` provider configuration option has been added. 144 | 145 | ### Fixed: 146 | 147 | - We fixed a bug where using native Object functions on the flags proxy object results in errors. This was reported in issue [#162](https://github.com/launchdarkly/react-client-sdk/issues/162). 148 | 149 | ### Deprecated: 150 | 151 | - The `user` provider configuration option has been deprecated. Please use `context` instead. 152 | 153 | ## [2.29.2] - 2022-10-28 154 | ### Fixed: 155 | - An issue with `asyncWithLDProvider` that was causing flags to be empty on first render. 156 | 157 | ## [2.29.1] - 2022-10-21 158 | ### Changed: 159 | - Upgraded to `js-client-sdk` version `2.24.2` which includes implementations of `jitter` and `backoff` for streaming connections. When a connection fails the retry will start at the `streamReconnectDelay` and will double on each unsuccessful consecutive connection attempt (`backoff`) to a max of 30 seconds. The delay will be adjusted from 50%-100% of the calculated delay to prevent many clients from attempting to reconnect at the same time (`jitter`). 160 | 161 | ## [2.29.0] - 2022-10-18 162 | ### Changed: 163 | - Updated `js-client-sdk` to `2.24.0` which added support for `Inspectors` that can be used for collecting information for monitoring, analytics, and debugging. 164 | 165 | ## [2.28.0] - 2022-10-05 166 | ### Changed: 167 | - Updated `js-client-sdk` version which removed event de-duplication functionality which was made redundant by support of summary events. This will improve the default event behavior when using experimentation. 168 | 169 | ## [2.27.0] - 2022-08-31 170 | ### Added: 171 | - `useFlags` hook is now generically typed, allowing you to assert what type your flag set will be. 172 | - `useLDClientError` hook for exposing client initialization failures. 173 | 174 | ### Changed: 175 | - `sendEventsOnlyForVariation` is now set to `true` by default to prevent a flag evaluation event being generated for every flag on load. 176 | - `flags` object (that is either injected via props using `LDConsumer` or returned from the `useFlags` hook) will generate a flag evaluation event on flag read (using a JavaScript proxy). This can be disabled by setting `reactOptions.sendEventsOnFlagRead: false`. 177 | - upgraded from ES5 to ES6. 178 | 179 | ## [2.26.0] - 2022-04-27 180 | ### Added: 181 | - `LDOptions.application`, for configuration of application metadata that may be used in LaunchDarkly analytics or other product features. This does not affect feature flag evaluations. 182 | - Added `basicLogger`, a replacement for the deprecated `createConsoleLogger`. 183 | 184 | ### Fixed: 185 | - The `baseUrl`, `streamUrl`, and `eventsUrl` properties now work properly regardless of whether the URL string has a trailing slash. Previously, a trailing slash would cause request URL paths to have double slashes. 186 | 187 | ## [2.25.2] - 2022-04-15 188 | ### Changed: 189 | - Updated LDProvider and the SDK's peer dependencies so that it can run in an application with React 18 ([#129](https://github.com/launchdarkly/react-client-sdk/issues/129)) 190 | 191 | ## [2.25.1] - 2022-02-18 192 | ### Fixed: 193 | - If the SDK receives invalid JSON data from a streaming connection (possibly as a result of the connection being cut off), it now uses its regular error-handling logic: the error is emitted as an `error` event or, if there are no `error` event listeners, it is logged. Previously, the error would be thrown as an unhandled exception. 194 | 195 | ## [2.25.0] - 2022-02-08 196 | Updated to version 2.20.1 of the JavaScript SDK, incorporating improvements from the [2.19.4](https://github.com/launchdarkly/js-client-sdk/releases/tag/2.19.4), [2.20.0](https://github.com/launchdarkly/js-client-sdk/releases/tag/2.20.0), and [2.20.1](https://github.com/launchdarkly/js-client-sdk/releases/tag/2.20.1) releases. 197 | 198 | ### Added: 199 | - Added exports of the types `LDReactOptions`, `ProviderConfig`, `AsyncProviderConfig`, and `AllFlagsLDClient`. These were referenced in exported functions, but were not previously importable from the main module. 200 | - New property `LDOptions.requestHeaderTransform` allows custom headers to be added to all HTTP requests. This may be necessary if you have an Internet gateway that uses a custom header for authentication. Note that custom headers may cause cross-origin browser requests to be rejected unless you have a way to ensure that the header name also appears in `Access-Control-Allow-Headers` for CORS preflight responses; if you are connecting to the LaunchDarkly Relay Proxy, it has a way to configure this. 201 | 202 | ### Fixed: 203 | - If the browser local storage mechanism throws an exception (for instance, if it is disabled or if storage is full), the SDK now correctly catches the exception and logs a message about the failure. It will only log this message once during the lifetime of the SDK client. ([#109](https://github.com/launchdarkly/react-client-sdk/issues/109)) 204 | - Removed an obsolete warning that would appear in the browser console after calling `track`: `Custom event "_____" does not exist`. Originally, the SDK had an expectation that `track` would be used only for event keys that had been previously defined as custom goals in the LaunchDarkly dashboard. That is still often the case, but it is not required and LaunchDarkly no longer sends custom goal names to the SDK, so the warning was happening even if such a goal did exist. 205 | 206 | ## [2.24.0] - 2021-12-09 207 | ### Added: 208 | - When initializing the SDK, consumers can now optionally pass in a previously-initialized `ldClient` instance (thanks, [TimboTambo](https://github.com/launchdarkly/react-client-sdk/pull/105)!) 209 | - Introduced missing typedoc annotations for `AsyncProviderConfig`. 210 | 211 | ## [2.23.3] - 2021-11-02 212 | ### Added: 213 | - The `AsyncProviderConfig` type was added. This type is a clone of `ProviderConfig` except that `deferInitialization` is marked as deprecated; see the "Deprecated" section below for more information. 214 | 215 | ### Fixed: 216 | - Fixed a bug where sourcemaps did not point to released files. ([#66](https://github.com/launchdarkly/react-client-sdk/issues/66)) 217 | 218 | ### Deprecated: 219 | - Deprecated the ability to specify `deferInitialization` in the `config` object parameter for `asyncWithLDProvider`. The `asyncWithLDProvider` function needs to be initialized at the app entry point prior to render to ensure flags and the `ldClient` are ready at the beginning of the app. As a result, initialization cannot be deferred when using `asyncWithLDProvider`. ([#99](https://github.com/launchdarkly/react-client-sdk/issues/99)) 220 | 221 | ## [2.23.2] - 2021-10-06 222 | ### Changed: 223 | - Improved `withLDProvider` so that prop types can be provided (thanks, [dsifford](https://github.com/launchdarkly/react-client-sdk/pull/97)!) 224 | 225 | ## [2.23.1] - 2021-09-03 226 | ### Fixed: 227 | - When using `asyncWithLDProvider`, components added to the DOM after client initialization now use the latest known flag values instead of the bootstrapped values. 228 | 229 | ## [2.23.0] - 2021-07-16 230 | ### Added: 231 | - HOC now hoists statics (thanks, [NathanWaddell121107](https://github.com/launchdarkly/react-client-sdk/pull/71)!) 232 | 233 | ## [2.22.3] - 2021-06-09 234 | ### Fixed: 235 | - Events for the [LaunchDarkly debugger](https://docs.launchdarkly.com/home/flags/debugger) are now properly pre-processed to omit private user attributes, as well as enforce only expected top level attributes are sent. 236 | - Events for the [LaunchDarkly debugger](https://docs.launchdarkly.com/home/flags/debugger) now include the index of the variation responsible for the evaluation result. 237 | 238 | 239 | ## [2.22.2] - 2021-04-06 240 | ### Changed: 241 | - Updated the SDK's peer dependencies so that it can run in an application with React 17 (thanks, [maclockard](https://github.com/launchdarkly/react-client-sdk/pull/61)!) 242 | 243 | ## [2.22.1] - 2021-04-02 244 | ### Fixed: 245 | - The property `LDOptions.inlineUsersInEvents` was not included in the TypeScript definitions for the JavaScript SDK. 246 | 247 | ## [2.22.0] - 2021-01-27 248 | ### Added: 249 | - Added the `alias` method to `LDClient`. This method can be used to associate two user objects for analytics purposes. When invoked, this method will queue a new alias event to be sent to LaunchDarkly. 250 | - Added the `autoAliasingOptOut` configuration option. This can be used to control the new automatic aliasing behavior of the `identify` method; by passing `autoAliasingOptOut: true`, `identify` will not automatically generate alias events. 251 | 252 | ### Changed: 253 | - `LDClient`'s `identify` method will now automatically generate an alias event when switching from an anonymous to a known user. This event associates the two users for analytics purposes as they most likely represent a single person. 254 | 255 | ## [2.21.0] - 2020-11-17 256 | ### Fixed: 257 | - The `camelCaseKeys` utility function is now exported as a function instead of as an object containing a `camelCaseKeys` function. `camelCaseKeys.camelCaseKeys` remains for backwards compatibility. 258 | - Updated the `LDEvaluationDetail.reason` type definition to be nullable. This value will be `null` when `LDOptions.evaluationReasons` is `false`. 259 | 260 | ### Deprecated: 261 | - `camelCaseKeys.camelCaseKeys` is now deprecated-- see the note above. 262 | 263 | ## [2.20.2] - 2020-09-14 264 | ### Fixed: 265 | - In streaming mode, when connecting to the Relay Proxy rather than directly to the LaunchDarkly streaming service, if the current user was changed twice within a short time it was possible for the SDK to revert to flag values from the previous user. (Fixed in JS SDK 2.18.1) 266 | 267 | ## [2.20.1] - 2020-08-19 268 | ### Fixed: 269 | - Fixed an issue where change listeners would update the component state when any flag was modified, even if the client instance was configured such that it was not subscribed for the modified flag. (Thanks, [clayembry](https://github.com/launchdarkly/react-client-sdk/pull/46)!) 270 | 271 | ## [2.20.0] - 2020-07-17 272 | ### Changed: 273 | - Updated `launchdarkly-js-client-sdk` version to 2.18.0, which adds the [`disable-sync-event-post`](https://launchdarkly.github.io/js-client-sdk/interfaces/_launchdarkly_js_client_sdk_.ldoptions.html#disablesynceventpost) option. 274 | 275 | ## [2.19.0] - 2020-07-15 276 | ### Added: 277 | - Exposed `LDProvider` as a standalone component. (Thanks, [nimi and morton](https://github.com/launchdarkly/react-client-sdk/pull/31)!) 278 | - A new configuration option, `deferInitialization`, allows `LDClient` initialization to be deferred until the user object is defined. (Thanks, [bezreyhan](https://github.com/launchdarkly/react-client-sdk/pull/40)!) 279 | 280 | ### Fixed: 281 | - Removed uses of `String.startsWith` that caused errors in Internet Explorer unless a polyfill for that function was present. 282 | 283 | 284 | ## [2.18.2] - 2020-05-27 285 | ### Fixed: 286 | - Fixed a TypeError where TypeScript attempted to redefine the `default` property on `withLDProvider`. This issue was introduced in version 2.18.1 of this SDK. ([#36](https://github.com/launchdarkly/react-client-sdk/issues/36)) 287 | 288 | ## [2.18.1] - 2020-05-19 289 | ### Fixed: 290 | - Updated JS SDK version to 2.17.5, to pick up bug fixes in [2.17.5](https://github.com/launchdarkly/js-client-sdk/releases/tag/2.17.5), [2.17.4](https://github.com/launchdarkly/js-client-sdk/releases/tag/2.17.4), [2.17.3](https://github.com/launchdarkly/js-client-sdk/releases/tag/2.17.3), [2.17.2](https://github.com/launchdarkly/js-client-sdk/releases/tag/2.17.2), and [2.17.1](https://github.com/launchdarkly/js-client-sdk/releases/tag/2.17.1). The intended practice is to release a new React SDK patch every time there is a JS SDK patch (unless several JS SDK patches are released very close together), but this had fallen behind. 291 | 292 | ## [2.18.0] - 2020-02-19 293 | Note: if you are using the LaunchDarkly Relay Proxy to forward events, update the Relay to version 5.10.0 or later before updating to this React SDK version. 294 | 295 | ### Added: 296 | - The SDK now periodically sends diagnostic data to LaunchDarkly, describing the version and configuration of the SDK, the architecture and version of the runtime platform, and performance statistics. No credentials, hostnames, or other identifiable values are included. This behavior can be disabled with the `diagnosticOptOut` option, or configured with `diagnosticRecordingInterval`. 297 | 298 | ### Fixed: 299 | - Updated JS SDK dependency version to 2.17.0, which includes a fix for streaming mode failing when used with secure mode. See release notes for JS SDK [2.17.0](https://github.com/launchdarkly/js-client-sdk/releases/tag/2.17.0) fror details. 300 | 301 | 302 | ## [2.17.1] - 2020-02-11 303 | ### Fixed: 304 | - Updated JS SDK dependency version from 2.16.0 to 2.16.3 for several recent fixes. See release notes for [2.16.1](https://github.com/launchdarkly/js-client-sdk/releases/tag/2.16.1), [2.16.2](https://github.com/launchdarkly/js-client-sdk/releases/tag/2.16.2), [2.16.3](https://github.com/launchdarkly/js-client-sdk/releases/tag/2.16.3). 305 | 306 | Note that while some transitive dependencies have been changed from exact versions to "best compatible" versions, the dependency on `js-client-sdk` is still an exact version dependency so that each release of `react-client-sdk` has well-defined behavior. 307 | 308 | ## [2.17.0] - 2019-12-18 309 | ### Added: 310 | - The `camelCaseKeys` utility function is now exposed as part of the SDK API. This function can be called from customers' code to work around the fact that `ldClient` functionality does not automatically camel-case keys in the same manner as the React SDK's props and hooks features. 311 | 312 | ## [2.16.2] - 2019-12-17 313 | ### Fixed: 314 | - Turned off the default setting of the `wrapperName` property because the LaunchDarkly service does not support it yet; it was causing CORS errors. 315 | 316 | ## [2.16.1] - 2019-12-17 317 | ***The 2.16.0 release was unpublished due to a packaging error. This is a rerelease containing the same changes but fixing the packaging.*** 318 | 319 | ### Added: 320 | - Configuration property `eventCapacity`: the maximum number of analytics events (not counting evaluation counters) that can be held at once, to prevent the SDK from consuming unexpected amounts of memory in case an application generates events unusually rapidly. In JavaScript code this would not normally be an issue, since the SDK flushes events every two seconds by default, but you may wish to increase this value if you will intentionally be generating a high volume of custom or identify events. The default value is 100. 321 | - Configuration properties `wrapperName` and `wrapperVersion`: used by the React SDK to identify a JS SDK instance that is being used with a wrapper API. 322 | 323 | ### Changed: 324 | - The SDK now logs a warning if any configuration property has an inappropriate type, such as `baseUri:3` or `sendEvents:"no"` (normally not possible in TypeScript, but could happen if an arbitrary object is cast to `LDOptions`). For boolean properties, the SDK will still interpret the value in terms of truthiness, which was the previous behavior. For all other types, since there's no such commonly accepted way to coerce the type, it will fall back to the default setting for that property; previously, the behavior was undefined but most such mistakes would have caused the SDK to throw an exception at some later point. 325 | 326 | ### Fixed: 327 | - When calling `identify`, the current user (as reported by `getUser()`) was being updated before the SDK had received the new flag values for that user, causing the client to be temporarily in an inconsistent state where flag evaluations would be associated with the wrong user in analytics events. Now, the current-user state will stay in sync with the flags and change only when they have finished changing. (Thanks, [edvinerikson](https://github.com/launchdarkly/js-sdk-common/pull/3)!) 328 | 329 | ### Deprecated: 330 | - The `samplingInterval` configuration property was deprecated in the code in the previous minor version release, and in the changelog, but the deprecation notice was accidentally omitted from the documentation comments. It is hereby deprecated again. 331 | 332 | ## [2.16.0] - 2019-12-16 333 | ***This release was broken and has been removed.*** 334 | 335 | ## [2.15.1] - 2019-11-14 336 | ### Fixed: 337 | - Fixed a bug where, when bootstrapping flag data, subsequent flag changes were incorrectly applied to the original bootstrapped data instead of the latest known flag data. 338 | - Fixed browser warnings and errors in the sample application. 339 | 340 | ## [2.15.0] - 2019-11-06 341 | ### Changed: 342 | - Changed the behavior of the warning message that is logged on failing to establish a streaming connection. Rather than the current behavior where the warning message appears upon each failed attempt, it will now only appear on the first failure in each series of attempts. Also, the message has been changed to mention that retries will occur. ([#182](https://github.com/launchdarkly/js-client-sdk/issues/182)) 343 | 344 | ### Fixed: 345 | - The `beforeunload` event handler no longer calls `close` on the client, which was causing the SDK to become unusable if the page did not actually close after this event fired (for instance if the browser navigated to a URL that launched an external application, or if another `beforeunload` handler cancelled leaving the page). Instead, it now only flushes events. There is also an `unload` handler that flushes any additional events that might have been created by any code that ran during the `beforeunload` stage. ([#181](https://github.com/launchdarkly/js-client-sdk/issues/181)) 346 | - Removed uses of `Object.assign` that caused errors in Internet Explorer unless a polyfill for that function was present. These were removed earlier in the 2.1.1 release, but had been mistakenly added again. 347 | - Removed development dependency on `typedoc` which caused some vulnerability warnings. 348 | 349 | ### Deprecated: 350 | - The `samplingInterval` configuration property is deprecated and will be removed in a future version. The intended use case for the `samplingInterval` feature was to reduce analytics event network usage in high-traffic applications. This feature is being deprecated in favor of summary counters, which are meant to track all events. 351 | 352 | 353 | ## [2.14.0] - 2019-09-12 354 | ### Added: 355 | - TypeDoc-generated documentation for all public types and methods is now [online](https://launchdarkly.github.io/react-client-sdk/). 356 | - The `asyncWithLDProvider` function to allow for your flags and the `LDClient` to be ready for use at the beginning of your app's lifecycle. 357 | 358 | ### Changed: 359 | - The `launchdarkly-react-client-sdk` package has been moved from the [`js-client-sdk`](https://github.com/launchdarkly/js-client-sdk) monorepo into its [own repository](https://github.com/launchdarkly/react-client-sdk). All subsequent releases will be made from this new repository. 360 | 361 | ## [2.13.0] - 2019-08-15 362 | ### Added: 363 | - In the React SDK, the new `reactOptions` parameter to `withLDProvider` provides React-specific options that do not affect the underlying JavaScript SDK. Currently, the only such option is `useCamelCaseFlagKeys`, which is true by default but can be set to false to disable the automatic camel-casing of flag keys. 364 | 365 | ### Changed: 366 | - In the React SDK, when omitting the `user` parameter to `withLDProvider`, an anonymous user will be created. This user will remain constant across browser sessions. Previously a new user was generated on each page load. 367 | 368 | ## [2.12.5] - 2019-07-29 369 | ### Fixed: 370 | - The React SDK was incompatible with Internet Explorer 11 due to using `String.startsWith()`. (Thanks, [cvetanov](https://github.com/launchdarkly/js-client-sdk/pull/169)!) 371 | 372 | ## [2.12.4] - 2019-07-10 373 | ### Fixed: 374 | - The `homepage` attribute in the `launchdarkly-react-client-sdk` and `launchdarkly-react-client-sdk-example` packages has been updated to the correct value. 375 | 376 | ## [2.11.0] - 2019-06-06 377 | ### Added: 378 | - Added support for hooks to the React SDK. 379 | 380 | ## [2.10.3] - 2019-05-08 381 | ### Changed: 382 | - Changed the package name from `ldclient-react` to `launchdarkly-react-client-sdk`. 383 | 384 | There are no other changes in this release. Substituting `ldclient-react` version 2.10.2 with `launchdarkly-react-client-sdk` version 2.10.3 will not affect functionality. 385 | 386 | ## [2.9.5] - 2019-03-12 387 | ### Fixed: 388 | - In React, when using the `bootstrap` property to preload the SDK client with flag values, the client will now become ready immediately and make the flags available to other components as soon as it is initialized; previously this did not happen until after `componentDidMount`. 389 | 390 | ## [2.9.3] - 2019-02-12 391 | ### Fixed: 392 | - The React SDK was pulling in the entire `lodash` package. This has been improved to only require the much smaller `camelcase` tool from `lodash`. 393 | - The React SDK now lists React itself as a peer dependency rather than a regular dependency, so it will not included twice in an application that already requires React. 394 | 395 | ## [2.9.1] - 2019-02-08 396 | ### Fixed: 397 | - The previous release of `ldclient-react` was broken: the package did not contain the actual files. The packaging script has been fixed. There are no other changes. 398 | 399 | ## [2.9.0] - 2019-02-01 400 | ### Added: 401 | - The new `ldclient-react` package provides a convenient mechanism for using the LaunchDarkly SDK within the React framework. 402 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Repository Maintainers 2 | * @launchdarkly/team-sdk-js 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the LaunchDarkly Client-side SDK for React 2 | 3 | LaunchDarkly has published an [SDK contributor's guide](https://docs.launchdarkly.com/sdk/concepts/contributors-guide) that provides a detailed explanation of how our SDKs work. See below for additional information on how to contribute to this SDK. 4 | 5 | ## Submitting bug reports and feature requests 6 | 7 | The LaunchDarkly SDK team monitors the [issue tracker](https://github.com/launchdarkly/react-client-sdk/issues) in the SDK repository. Bug reports and feature requests specific to this SDK should be filed in this issue tracker. The SDK team will respond to all newly filed issues within two business days. 8 | 9 | ## Submitting pull requests 10 | 11 | We encourage pull requests and other contributions from the community. Before submitting pull requests, ensure that all temporary or unintended code is removed. Don't worry about adding reviewers to the pull request; the LaunchDarkly SDK team will add themselves. The SDK team will acknowledge all pull requests within two business days. 12 | 13 | ## Build instructions 14 | 15 | Note that this repository contains only the React SDK code that provides a convenient way for React code to interact with the LaunchDarkly JavaScript SDK. The JavaScript SDK functionality is in the [`launchdarkly-js-client-sdk`](https://www.npmjs.com/package/launchdarkly-js-client-sdk) package whose source code is in [js-client-sdk](https://github.com/launchdarkly/js-client-sdk), and also the core package `launchdarkly-js-sdk-common` in [js-sdk-common](https://github.com/launchdarkly/js-sdk-common). 16 | 17 | ### Installing dependencies 18 | 19 | ``` 20 | npm install 21 | ``` 22 | 23 | ### Building 24 | 25 | ``` 26 | npm run build 27 | ``` 28 | 29 | ### Testing 30 | 31 | ``` 32 | npm test 33 | ``` 34 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2019 Catamorphic, Co. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LaunchDarkly Client-side SDK for React 2 | 3 | [![Circle CI](https://circleci.com/gh/launchdarkly/react-client-sdk/tree/main.svg?style=svg)](https://circleci.com/gh/launchdarkly/react-client-sdk/tree/main) 4 | 5 | ## LaunchDarkly overview 6 | 7 | [LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves trillions of feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/home/getting-started) using LaunchDarkly today! 8 | 9 | [![Twitter Follow](https://img.shields.io/twitter/follow/launchdarkly.svg?style=social&label=Follow&maxAge=2592000)](https://twitter.com/intent/follow?screen_name=launchdarkly) 10 | 11 | ## Supported React versions 12 | 13 | This version of the LaunchDarkly SDK is compatible with versions 16.3.0 and later of React because it uses React's Context API. However, if you are using the SDK's Hooks API or `asyncWithLDProvider`, then you must use React version 16.8.0 or later. 14 | 15 | Additionally, refer to the [JavaScript SDK README](https://github.com/launchdarkly/js-client-sdk#browser-compatibility) to learn more about browser compatibility. 16 | 17 | ## Getting started 18 | 19 | Refer to the [SDK documentation](https://docs.launchdarkly.com/sdk/client-side/react/react-web#getting-started) for instructions on getting started with using the SDK. 20 | 21 | Please note that the React SDK has two special requirements in terms of your LaunchDarkly environment. First, in terms of the credentials for your environment that appear on your [Account Settings](https://app.launchdarkly.com/settings/projects) dashboard, the React SDK uses the "Client-side ID"-- not the "SDK key" or the "Mobile key". Second, for any feature flag that you will be using in React code, you must check the "Make this flag available to client-side SDKs" box on that flag's Settings page. 22 | 23 | ## Learn more 24 | 25 | Read our [documentation](https://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. You can also head straight to the [complete reference guide for this SDK](https://docs.launchdarkly.com/docs/react-sdk-reference) or our [code-generated API documentation](https://launchdarkly.github.io/react-client-sdk/). 26 | 27 | This SDK builds upon the [JavaScript SDK](https://github.com/launchdarkly/js-client-sdk), supporting all of the same functionality, but using React's Context API to provide additional conveniences. While using this SDK you may need to directly interact with the underlying JavaScript SDK. For more information on how to use the JavaScript SDK and its characteristics, see the [SDK's README](https://github.com/launchdarkly/js-client-sdk/blob/main/README.md). 28 | 29 | ## Testing 30 | 31 | We run integration tests for all our SDKs using a centralized test harness. This approach gives us the ability to test for consistency across SDKs, as well as test networking behavior in a long-running application. These tests cover each method in the SDK, and verify that event sending, flag evaluation, stream reconnection, and other aspects of the SDK all behave correctly. 32 | 33 | ## Contributing 34 | 35 | We encourage pull requests and other contributions from the community. Check out our [contributing guidelines](CONTRIBUTING.md) for instructions on how to contribute to this SDK. 36 | 37 | ## About LaunchDarkly 38 | 39 | * LaunchDarkly is a continuous delivery platform that provides feature flags as a service and allows developers to iterate quickly and safely. We allow you to easily flag your features and manage them from the LaunchDarkly dashboard. With LaunchDarkly, you can: 40 | * Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases. 41 | * Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?). 42 | * Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file. 43 | * Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). 44 | * Disable parts of your application to facilitate maintenance, without taking everything offline. 45 | * LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Read [our documentation](https://docs.launchdarkly.com/sdk) for a complete list. 46 | * Explore LaunchDarkly 47 | * [launchdarkly.com](https://www.launchdarkly.com/ "LaunchDarkly Main Website") for more information 48 | * [docs.launchdarkly.com](https://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and SDK reference guides 49 | * [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/ "LaunchDarkly API Documentation") for our API documentation 50 | * [blog.launchdarkly.com](https://blog.launchdarkly.com/ "LaunchDarkly Blog Documentation") for the latest product updates 51 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting and Fixing Security Issues 2 | 3 | Please report all security issues to the LaunchDarkly security team by submitting a bug bounty report to our [HackerOne program](https://hackerone.com/launchdarkly?type=team). LaunchDarkly will triage and address all valid security issues following the response targets defined in our program policy. Valid security issues may be eligible for a bounty. 4 | 5 | Please do not open issues or pull requests for security issues. This makes the problem immediately visible to everyone, including potentially malicious actors. 6 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import eslint from '@eslint/js'; 4 | import tseslint from 'typescript-eslint'; 5 | 6 | export default tseslint.config( 7 | eslint.configs.recommended, 8 | ...tseslint.configs.recommended, 9 | ...tseslint.configs.strict, 10 | ...tseslint.configs.stylistic, 11 | { 12 | files: ['**/*.{ts,tsx,mts,cts}'], 13 | rules: { 14 | 'no-undef': 'off', 15 | "@typescript-eslint/no-unused-vars": [ 16 | "error", 17 | { 18 | "args": "all", 19 | "argsIgnorePattern": "^_", 20 | "caughtErrors": "all", 21 | "caughtErrorsIgnorePattern": "^_", 22 | "destructuredArrayIgnorePattern": "^_", 23 | "varsIgnorePattern": "^_", 24 | "ignoreRestSiblings": true 25 | } 26 | ] 27 | }, 28 | }, 29 | ); 30 | -------------------------------------------------------------------------------- /examples/async-provider/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-proposal-class-properties", 8 | "@babel/plugin-transform-runtime", 9 | "babel-plugin-styled-components" 10 | ] 11 | } -------------------------------------------------------------------------------- /examples/async-provider/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "parserOptions": { 4 | "allowImportExportEverywhere": true 5 | }, 6 | "extends": [ 7 | "airbnb", 8 | "eslint:recommended", 9 | "plugin:react/recommended" 10 | ], 11 | "plugins": [ 12 | "babel" 13 | ], 14 | "rules": { 15 | "arrow-parens": 0, 16 | "eol-last": 0, 17 | "global-require": 0, 18 | "arrow-body-style": 0, 19 | "consistent-return": 0, 20 | "no-unneeded-ternary": 0, 21 | "max-len": 0, 22 | "no-param-reassign": 2, 23 | "new-cap": 0, 24 | "no-console": 0, 25 | "object-curly-spacing": 0, 26 | "spaced-comment": 0, 27 | "import/no-extraneous-dependencies": 0, 28 | "import/first": 0, 29 | "import/prefer-default-export": 0, 30 | "import/no-mutable-exports": 0, 31 | "import/no-named-as-default": 0, 32 | "react/display-name": 0, 33 | "react/jsx-filename-extension": 0, 34 | "react/jsx-indent": 0, 35 | "react/jsx-indent-props": 0, 36 | "react/jsx-space-before-closing": 0, 37 | "react/jsx-first-prop-new-line": 0, 38 | "react/prefer-stateless-function": 0, 39 | "react/jsx-closing-bracket-location": 0, 40 | "react/require-extension": 0, 41 | "react/sort-comp": 0, 42 | "react/jsx-wrap-multilines": 0, 43 | "react/jsx-no-bind": 0, 44 | "react/jsx-users-react": 0, 45 | "react/jsx-tag-spacing": 0, 46 | "jsx-a11y/anchor-is-valid": 0, 47 | "jsx-a11y/img-has-alt": 0, 48 | "no-trailing-spaces": 0, 49 | "no-underscore-dangle": 0, 50 | "no-use-before-define": 0, 51 | "no-duplicate-imports": 0, 52 | "import/no-duplicates": 1, 53 | "no-useless-escape": 0, 54 | "no-unused-expressions": [1 , {"allowTernary": true}], 55 | "react/forbid-prop-types": 0 56 | }, 57 | "env": { 58 | "browser": true, 59 | "jest": true, 60 | "node": true 61 | }, 62 | "globals": { 63 | "React": true, 64 | "fetch": true, 65 | "jest": true 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /examples/async-provider/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | npm-debug.log 4 | dist 5 | .eslintcache -------------------------------------------------------------------------------- /examples/async-provider/README.md: -------------------------------------------------------------------------------- 1 | # LaunchDarkly SDK for React - Example app 2 | 3 | This is a simple SPA demonstrating `launchdarkly-react-client-sdk`. 4 | 5 | ## Installation 6 | 7 | ```sh 8 | yarn 9 | ``` 10 | 11 | ## Running the example 12 | 13 | Follow these steps to run the example app: 14 | 15 | * In client/index.js, set `clientSideID` to your own Client-side ID. You can find 16 | this in your LaunchDarkly portal under Account settings / Projects. 17 | 18 | * Create a flag called `dev-test-flag` in your project. Make sure you 19 | make the flag available to the client-side SDK. 20 | 21 | * You should now be able to start the app by doing: 22 | 23 | ```sh 24 | yarn start 25 | ``` 26 | 27 | * Toggle the killswitch for `dev-test-flag` in the dashboard and the 28 | app should respond without a browser refresh. 29 | -------------------------------------------------------------------------------- /examples/async-provider/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "launchdarkly-react-client-sdk-example", 3 | "version": "1.1.0", 4 | "description": "Example usage of launchdarkly-react-client-sdk", 5 | "main": "src/server/index.js", 6 | "scripts": { 7 | "start": "NODE_OPTIONS=--openssl-legacy-provider node src/server/index.js", 8 | "lint": "eslint ./src", 9 | "serve": "webpack-serve webpack.config.server", 10 | "postinstall": "cd ../../ && yarn link-dev" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/launchdarkly/js-client-sdk.git" 15 | }, 16 | "keywords": [ 17 | "launchdarkly", 18 | "launch", 19 | "darkly", 20 | "react", 21 | "sdk", 22 | "bindings" 23 | ], 24 | "author": "LaunchDarkly ", 25 | "license": "Apache-2.0", 26 | "homepage": "https://github.com/launchdarkly/js-client-sdk/tree/main/packages/launchdarkly-react-client-sdk", 27 | "dependencies": { 28 | "@babel/polyfill": "^7.2.5", 29 | "express": "^4.17.3", 30 | "launchdarkly-react-client-sdk": "^3.0.10", 31 | "lodash": "^4.17.21", 32 | "prop-types": "^15.7.2", 33 | "react": "18.2.0", 34 | "react-dom": "18.2.0", 35 | "react-router-dom": "^5.1.2", 36 | "styled-components": "^4.1.3" 37 | }, 38 | "devDependencies": { 39 | "@babel/cli": "^7.5.5", 40 | "@babel/core": "^7.5.5", 41 | "@babel/plugin-proposal-class-properties": "^7.5.5", 42 | "@babel/plugin-transform-runtime": "^7.5.5", 43 | "@babel/preset-env": "^7.5.5", 44 | "@babel/preset-react": "^7.0.0", 45 | "@babel/runtime": "^7.5.5", 46 | "babel-eslint": "^10.0.3", 47 | "babel-loader": "^8.0.6", 48 | "babel-plugin-styled-components": "^1.10.6", 49 | "eslint": "^5.10.0", 50 | "eslint-config-airbnb": "^17.1.0", 51 | "eslint-config-prettier": "^3.3.0", 52 | "eslint-plugin-babel": "^5.3.0", 53 | "eslint-plugin-import": "^2.14.0", 54 | "eslint-plugin-jsx-a11y": "^6.1.2", 55 | "eslint-plugin-react": "^7.11.1", 56 | "universal-hot-reload": "^3.3.4", 57 | "webpack": "^4.39.3", 58 | "webpack-cli": "^3.3.7", 59 | "webpack-node-externals": "^1.7.2", 60 | "webpack-serve": "^3.1.1" 61 | }, 62 | "resolutions": { 63 | "acorn": "npm:acorn-with-stage3" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /examples/async-provider/src/client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import { asyncWithLDProvider } from 'launchdarkly-react-client-sdk'; 5 | import App from '../universal/app'; 6 | 7 | (async () => { 8 | // Set clientSideID to your own Client-side ID. You can find this in 9 | // your LaunchDarkly portal under Account settings / Projects 10 | const LDProvider = await asyncWithLDProvider({ clientSideID: '' }); 11 | 12 | const root = createRoot(document.getElementById('reactDiv')); 13 | root.render( 14 | 15 | 16 | 17 | 18 | ); 19 | })(); 20 | -------------------------------------------------------------------------------- /examples/async-provider/src/server/index.js: -------------------------------------------------------------------------------- 1 | const UniversalHotReload = require('universal-hot-reload').default; 2 | 3 | // supply your own webpack configs 4 | const serverConfig = require('../../webpack.config.server.js'); 5 | const clientConfig = require('../../webpack.config.client.js'); 6 | 7 | // the configs are optional, you can supply either one or both. 8 | // if you omit say the server config, then your server won't hot reload. 9 | UniversalHotReload({ serverConfig, clientConfig }); 10 | -------------------------------------------------------------------------------- /examples/async-provider/src/server/server.js: -------------------------------------------------------------------------------- 1 | import Express from 'express'; 2 | 3 | const PORT = 3000; 4 | const app = Express(); 5 | 6 | app.use('/dist', Express.static('dist', { maxAge: '1d' })); 7 | 8 | app.use((req, res) => { 9 | const html = ` 10 | 11 | 12 | 13 | 14 | ld-react example 15 | 16 | 17 |
18 | 19 | 20 | `; 21 | 22 | res.end(html); 23 | }); 24 | 25 | export default app.listen(PORT, () => { 26 | console.log(`Example app listening at ${PORT}...`); 27 | }); 28 | -------------------------------------------------------------------------------- /examples/async-provider/src/universal/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Route, Redirect } from 'react-router-dom'; 3 | import SiteNav from './siteNav'; 4 | import Home from './home'; 5 | import HooksDemo from './hooksDemo'; 6 | 7 | const App = () => ( 8 |
9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 | ); 21 | 22 | export default App; 23 | -------------------------------------------------------------------------------- /examples/async-provider/src/universal/home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | import { withLDConsumer } from 'launchdarkly-react-client-sdk'; 5 | 6 | const Root = styled.div` 7 | color: #001b44; 8 | `; 9 | const Heading = styled.h1` 10 | color: #00449e; 11 | `; 12 | const ListItem = styled.li` 13 | margin-top: 10px; 14 | `; 15 | const FlagDisplay = styled.div` 16 | font-size: 20px; 17 | font-weight: bold; 18 | `; 19 | const FlagOn = styled.span` 20 | color: #96bf01; 21 | `; 22 | const Home = ({ flags }) => ( 23 | 24 | Welcome to launchdarkly-react-client-sdk Example App 25 |
26 | To run this example: 27 |
    28 | 29 | In app.js, set clientSideID to your own Client-side ID. You can find this in your ld portal under Account 30 | settings / Projects. 31 | 32 | 33 | Create a flag called dev-test-flag in your project. Make sure you make it available for the client side js 34 | sdk. 35 | 36 | Turn the flag on and off to see this app respond without a browser refresh. 37 |
38 |
39 | {flags.devTestFlag ? Flag on : Flag off} 40 |
41 | ); 42 | 43 | Home.propTypes = { 44 | flags: PropTypes.object.isRequired, 45 | }; 46 | 47 | export default withLDConsumer()(Home); 48 | -------------------------------------------------------------------------------- /examples/async-provider/src/universal/hooksDemo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { useFlags } from 'launchdarkly-react-client-sdk'; 4 | 5 | const Root = styled.div` 6 | color: #001b44; 7 | `; 8 | const Heading = styled.h1` 9 | color: #00449e; 10 | `; 11 | const ListItem = styled.li` 12 | margin-top: 10px; 13 | `; 14 | const FlagDisplay = styled.div` 15 | font-size: 20px; 16 | font-weight: bold; 17 | `; 18 | const FlagOn = styled.span` 19 | color: #96bf01; 20 | `; 21 | const HooksDemo = () => { 22 | const { devTestFlag } = useFlags(); 23 | 24 | return ( 25 | 26 | Hooks Demo 27 |
28 | This is the equivalent demo app using hooks. To run this example: 29 |
    30 | 31 | In app.js, set clientSideID to your own Client-side ID. You can find this in your ld portal under Account 32 | settings / Projects. 33 | 34 | 35 | Create a flag called dev-test-flag in your project. Make sure you make it available for the client side js 36 | sdk. 37 | 38 | Turn the flag on and off to see this app respond without a browser refresh. 39 |
40 |
41 | {devTestFlag ? Flag on : Flag off} 42 |
43 | ); 44 | }; 45 | 46 | export default HooksDemo; 47 | -------------------------------------------------------------------------------- /examples/async-provider/src/universal/siteNav.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { css } from 'styled-components'; 3 | import { Link } from 'react-router-dom'; 4 | 5 | export default () => ( 6 |
13 | Home 14 | Hooks Demo 15 |
16 | ); 17 | -------------------------------------------------------------------------------- /examples/async-provider/webpack.config.client.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const WebpackServeUrl = 'http://localhost:3002'; 4 | 5 | module.exports = { 6 | mode: 'development', 7 | devtool: 'source-map', 8 | entry: ['@babel/polyfill', './src/client/index'], 9 | output: { 10 | path: path.resolve('dist'), 11 | publicPath: `${WebpackServeUrl}/dist/`, // MUST BE FULL PATH! 12 | filename: 'bundle.js', 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.jsx?$/, 18 | include: path.resolve('src'), 19 | exclude: /node_modules/, 20 | loader: 'babel-loader', 21 | options: { 22 | cacheDirectory: true, 23 | }, 24 | }, 25 | ], 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /examples/async-provider/webpack.config.server.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const nodeExternals = require('webpack-node-externals'); 3 | 4 | module.exports = { 5 | mode: 'development', 6 | devtool: 'source-map', 7 | entry: ['@babel/polyfill', './src/server/server.js'], // set this to your server entry point. This should be where you start your express server with .listen() 8 | target: 'node', // tell webpack this bundle will be used in nodejs environment. 9 | externals: [nodeExternals()], // Omit node_modules code from the bundle. You don't want and don't need them in the bundle. 10 | output: { 11 | path: path.resolve('dist'), 12 | filename: 'serverBundle.js', 13 | libraryTarget: 'commonjs2', // IMPORTANT! Add module.exports to the beginning of the bundle, so universal-hot-reload can access your app. 14 | }, 15 | // The rest of the config is pretty standard and can contain other webpack stuff you need. 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.jsx?$/, 20 | include: path.resolve('src'), 21 | exclude: /node_modules/, 22 | loader: 'babel-loader', 23 | options: { 24 | cacheDirectory: true, 25 | }, 26 | }], 27 | }, 28 | }; -------------------------------------------------------------------------------- /examples/deferred-initialization/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /examples/deferred-initialization/README.md: -------------------------------------------------------------------------------- 1 | # LaunchDarkly SDK for React - Example deferred initialization app 2 | 3 | This is a CRA typescript app demonstrating `launchdarkly-react-client-sdk` and how to defer the LDClient initialization. 4 | 5 | The LDClient will not be initialized on initial render until a context is set. This is done through clicking the "Login" button in this example app. 6 | 7 | ## Running the deferred initialization example 8 | 9 | Follow these steps to run the app: 10 | 11 | - Create a `.env.local` file and set your clientSideID there like so: 12 | 13 | ```shell 14 | REACT_APP_LD_CLIENT_SIDE_ID=xxx 15 | ``` 16 | 17 | - Create a flag called `dev-test-flag` in your project. Make sure you 18 | make the flag available to the client-side SDK. 19 | 20 | - You should now be able to start the app by doing: 21 | 22 | ```sh 23 | yarn && yarn start 24 | ``` 25 | 26 | - The LDClient will not be initialized on initial render until a context is set. This is done when the login button is clicked. 27 | - Toggle the killswitch for `dev-test-flag` in the dashboard and the 28 | app should respond without a browser refresh. 29 | 30 | # Getting Started with Create React App 31 | 32 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 33 | 34 | ## Available Scripts 35 | 36 | In the project directory, you can run: 37 | 38 | ### `npm start` 39 | 40 | Runs the app in the development mode.\ 41 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 42 | 43 | The page will reload if you make edits.\ 44 | You will also see any lint errors in the console. 45 | 46 | ### `npm test` 47 | 48 | Launches the test runner in the interactive watch mode.\ 49 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 50 | 51 | ### `npm run build` 52 | 53 | Builds the app for production to the `build` folder.\ 54 | It correctly bundles React in production mode and optimizes the build for the best performance. 55 | 56 | The build is minified and the filenames include the hashes.\ 57 | Your app is ready to be deployed! 58 | 59 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 60 | 61 | ### `npm run eject` 62 | 63 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 64 | 65 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 66 | 67 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 68 | 69 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 70 | 71 | ## Learn More 72 | 73 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 74 | 75 | To learn React, check out the [React documentation](https://reactjs.org/). 76 | -------------------------------------------------------------------------------- /examples/deferred-initialization/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deferred-initialization", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.5", 7 | "@testing-library/react": "^13.4.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "@types/jest": "^27.5.2", 10 | "@types/node": "^16.18.18", 11 | "@types/react": "^18.0.28", 12 | "@types/react-dom": "^18.0.11", 13 | "launchdarkly-react-client-sdk": "^3.0.9", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0", 16 | "react-scripts": "5.0.1", 17 | "typescript": "^4.9.5", 18 | "web-vitals": "^2.1.4" 19 | }, 20 | "scripts": { 21 | "analyze": "source-map-explorer 'build/static/js/*.js'", 22 | "start": "react-scripts start", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test", 25 | "eject": "react-scripts eject", 26 | "postinstall": "cd ../../ && yarn link-dev", 27 | "tsc": "tsc" 28 | }, 29 | "eslintConfig": { 30 | "extends": [ 31 | "react-app", 32 | "react-app/jest" 33 | ] 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | }, 47 | "devDependencies": { 48 | "source-map-explorer": "2.5.3" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/deferred-initialization/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/deferred-initialization/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /examples/deferred-initialization/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/deferred-initialization/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import logo from './logo.svg'; 3 | import Welcome from './welcome'; 4 | import { LDProvider, LDContext } from 'launchdarkly-react-client-sdk'; 5 | 6 | import './App.css'; 7 | 8 | const clientSideID = process.env.REACT_APP_LD_CLIENT_SIDE_ID ?? ''; 9 | 10 | function App() { 11 | const [context, setContext] = useState(); 12 | 13 | function onClickLogin() { 14 | setContext({ kind: 'user', key: 'yus' }); 15 | } 16 | 17 | return ( 18 | 19 |
20 |
21 | logo 22 | 23 | 24 | Learn React 25 | 26 |

27 | 28 |

29 |
30 |
31 |
32 | ); 33 | } 34 | 35 | export default App; 36 | -------------------------------------------------------------------------------- /examples/deferred-initialization/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /examples/deferred-initialization/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App'; 4 | import reportWebVitals from './reportWebVitals'; 5 | 6 | import './index.css'; 7 | 8 | const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); 9 | root.render( 10 | 11 | 12 | , 13 | ); 14 | 15 | // If you want to start measuring performance in your app, pass a function 16 | // to log results (for example: reportWebVitals(console.log)) 17 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 18 | reportWebVitals(); 19 | -------------------------------------------------------------------------------- /examples/deferred-initialization/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/deferred-initialization/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/deferred-initialization/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /examples/deferred-initialization/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /examples/deferred-initialization/src/welcome.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useFlags } from 'launchdarkly-react-client-sdk'; 3 | 4 | import './App.css'; 5 | 6 | function Welcome() { 7 | const { devTestFlag } = useFlags(); 8 | 9 | return

{devTestFlag ? Flag on : Flag off}

; 10 | } 11 | 12 | export default Welcome; 13 | -------------------------------------------------------------------------------- /examples/deferred-initialization/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /examples/hoc/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-proposal-class-properties", 8 | "@babel/plugin-transform-runtime", 9 | "babel-plugin-styled-components" 10 | ] 11 | } -------------------------------------------------------------------------------- /examples/hoc/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "parserOptions": { 4 | "allowImportExportEverywhere": true 5 | }, 6 | "extends": [ 7 | "airbnb", 8 | "eslint:recommended", 9 | "plugin:react/recommended" 10 | ], 11 | "plugins": [ 12 | "babel" 13 | ], 14 | "rules": { 15 | "arrow-parens": 0, 16 | "eol-last": 0, 17 | "global-require": 0, 18 | "arrow-body-style": 0, 19 | "consistent-return": 0, 20 | "no-unneeded-ternary": 0, 21 | "max-len": 0, 22 | "no-param-reassign": 2, 23 | "new-cap": 0, 24 | "no-console": 0, 25 | "object-curly-spacing": 0, 26 | "spaced-comment": 0, 27 | "import/no-extraneous-dependencies": 0, 28 | "import/first": 0, 29 | "import/prefer-default-export": 0, 30 | "import/no-mutable-exports": 0, 31 | "import/no-named-as-default": 0, 32 | "react/display-name": 0, 33 | "react/jsx-filename-extension": 0, 34 | "react/jsx-indent": 0, 35 | "react/jsx-indent-props": 0, 36 | "react/jsx-space-before-closing": 0, 37 | "react/jsx-first-prop-new-line": 0, 38 | "react/prefer-stateless-function": 0, 39 | "react/jsx-closing-bracket-location": 0, 40 | "react/require-extension": 0, 41 | "react/sort-comp": 0, 42 | "react/jsx-wrap-multilines": 0, 43 | "react/jsx-no-bind": 0, 44 | "react/jsx-users-react": 0, 45 | "react/jsx-tag-spacing": 0, 46 | "jsx-a11y/anchor-is-valid": 0, 47 | "jsx-a11y/img-has-alt": 0, 48 | "no-trailing-spaces": 0, 49 | "no-underscore-dangle": 0, 50 | "no-use-before-define": 0, 51 | "no-duplicate-imports": 0, 52 | "import/no-duplicates": 1, 53 | "no-useless-escape": 0, 54 | "no-unused-expressions": [1 , {"allowTernary": true}], 55 | "react/forbid-prop-types": 0 56 | }, 57 | "env": { 58 | "browser": true, 59 | "jest": true, 60 | "node": true 61 | }, 62 | "globals": { 63 | "React": true, 64 | "fetch": true, 65 | "jest": true 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /examples/hoc/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | npm-debug.log 4 | dist 5 | .eslintcache -------------------------------------------------------------------------------- /examples/hoc/README.md: -------------------------------------------------------------------------------- 1 | # LaunchDarkly SDK for React - Example app 2 | 3 | This is a simple SPA demonstrating `launchdarkly-react-client-sdk`. 4 | 5 | ## Installation 6 | 7 | ```sh 8 | yarn 9 | ``` 10 | 11 | ## Running the example 12 | 13 | Follow these steps to run the example app: 14 | 15 | * In app.js, set `clientSideID` to your own Client-side ID. You can find 16 | this in your LaunchDarkly portal under Account settings / Projects. 17 | 18 | * Create a flag called `dev-test-flag` in your project. Make sure you 19 | make the flag available to the client-side SDK. 20 | 21 | * You should now be able to start the app by doing: 22 | 23 | ```sh 24 | yarn start 25 | ``` 26 | 27 | * Toggle the killswitch for `dev-test-flag` in the dashboard and the 28 | app should respond without a browser refresh. 29 | -------------------------------------------------------------------------------- /examples/hoc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "launchdarkly-react-client-sdk-example", 3 | "version": "1.1.0", 4 | "description": "Example usage of launchdarkly-react-client-sdk", 5 | "main": "src/server/index.js", 6 | "scripts": { 7 | "start": "NODE_OPTIONS=--openssl-legacy-provider node src/server/index.js", 8 | "lint": "eslint ./src", 9 | "serve": "webpack-serve webpack.config.server", 10 | "postinstall": "cd ../../ && yarn link-dev" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/launchdarkly/js-client-sdk.git" 15 | }, 16 | "keywords": [ 17 | "launchdarkly", 18 | "launch", 19 | "darkly", 20 | "react", 21 | "sdk", 22 | "bindings" 23 | ], 24 | "author": "LaunchDarkly ", 25 | "license": "Apache-2.0", 26 | "homepage": "https://github.com/launchdarkly/js-client-sdk/tree/main/packages/launchdarkly-react-client-sdk", 27 | "dependencies": { 28 | "@babel/polyfill": "^7.2.5", 29 | "express": "^4.17.3", 30 | "launchdarkly-react-client-sdk": "^3.0.10", 31 | "lodash": "^4.17.21", 32 | "prop-types": "^15.7.2", 33 | "react": "18.2.0", 34 | "react-dom": "18.2.0", 35 | "react-router-dom": "^5.1.2", 36 | "styled-components": "^4.1.3" 37 | }, 38 | "devDependencies": { 39 | "@babel/cli": "^7.2.3", 40 | "@babel/core": "^7.2.2", 41 | "@babel/plugin-proposal-class-properties": "^7.2.3", 42 | "@babel/plugin-transform-runtime": "^7.2.0", 43 | "@babel/preset-env": "^7.2.3", 44 | "@babel/preset-react": "^7.0.0", 45 | "babel-eslint": "^10.0.1", 46 | "babel-loader": "^8.0.4", 47 | "babel-plugin-styled-components": "^1.10.0", 48 | "eslint": "^5.10.0", 49 | "eslint-config-airbnb": "^17.1.0", 50 | "eslint-config-prettier": "^3.3.0", 51 | "eslint-plugin-babel": "^5.3.0", 52 | "eslint-plugin-import": "^2.14.0", 53 | "eslint-plugin-jsx-a11y": "^6.1.2", 54 | "eslint-plugin-react": "^7.11.1", 55 | "universal-hot-reload": "^3.3.4", 56 | "webpack": "^4.27.1", 57 | "webpack-cli": "^3.1.2", 58 | "webpack-node-externals": "^1.7.2", 59 | "webpack-serve": "^2.0.3" 60 | }, 61 | "resolutions": { 62 | "acorn": "npm:acorn-with-stage3" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /examples/hoc/src/client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import App from '../universal/app'; 5 | 6 | createRoot(document.getElementById('reactDiv')).render( 7 | 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /examples/hoc/src/server/index.js: -------------------------------------------------------------------------------- 1 | const UniversalHotReload = require('universal-hot-reload').default; 2 | 3 | // supply your own webpack configs 4 | const serverConfig = require('../../webpack.config.server.js'); 5 | const clientConfig = require('../../webpack.config.client.js'); 6 | 7 | // the configs are optional, you can supply either one or both. 8 | // if you omit say the server config, then your server won't hot reload. 9 | UniversalHotReload({ serverConfig, clientConfig }); 10 | -------------------------------------------------------------------------------- /examples/hoc/src/server/server.js: -------------------------------------------------------------------------------- 1 | import Express from 'express'; 2 | import React from 'react'; 3 | import { renderToString } from 'react-dom/server'; 4 | import { StaticRouter } from 'react-router-dom'; 5 | import App from '../universal/app'; 6 | 7 | const PORT = 3000; 8 | const app = Express(); 9 | 10 | app.use('/dist', Express.static('dist', { maxAge: '1d' })); 11 | 12 | app.use((req, res) => { 13 | const html = ` 14 | 15 | 16 | 17 | 18 | ld-react example 19 | 20 | 21 |
${renderToString( 22 | 23 | 24 | , 25 | )}
26 | 27 | 28 | `; 29 | 30 | res.end(html); 31 | }); 32 | 33 | export default app.listen(PORT, () => { 34 | console.log(`Example app listening at ${PORT}...`); 35 | }); 36 | -------------------------------------------------------------------------------- /examples/hoc/src/universal/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Route, Redirect } from 'react-router-dom'; 3 | import { withLDProvider } from 'launchdarkly-react-client-sdk'; 4 | import SiteNav from './siteNav'; 5 | import Home from './home'; 6 | import HooksDemo from './hooksDemo'; 7 | 8 | const App = () => ( 9 |
10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 | ); 22 | 23 | // Set clientSideID to your own Client-side ID. You can find this in 24 | // your LaunchDarkly portal under Account settings / Projects 25 | export default withLDProvider({ 26 | clientSideID: '', 27 | })(App); 28 | -------------------------------------------------------------------------------- /examples/hoc/src/universal/home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | import { withLDConsumer } from 'launchdarkly-react-client-sdk'; 5 | 6 | const Root = styled.div` 7 | color: #001b44; 8 | `; 9 | const Heading = styled.h1` 10 | color: #00449e; 11 | `; 12 | const ListItem = styled.li` 13 | margin-top: 10px; 14 | `; 15 | const FlagDisplay = styled.div` 16 | font-size: 20px; 17 | font-weight: bold; 18 | `; 19 | const FlagOn = styled.span` 20 | color: #96bf01; 21 | `; 22 | const Home = ({ flags }) => ( 23 | 24 | Welcome to launchdarkly-react-client-sdk Example App 25 |
26 | To run this example: 27 |
    28 | 29 | In app.js, set clientSideID to your own Client-side ID. You can find this in your ld portal under Account 30 | settings / Projects. 31 | 32 | 33 | Create a flag called dev-test-flag in your project. Make sure you make it available for the client side js 34 | sdk. 35 | 36 | Turn the flag on and off to see this app respond without a browser refresh. 37 |
38 |
39 | {flags.devTestFlag ? Flag on : Flag off} 40 |
41 | ); 42 | 43 | Home.propTypes = { 44 | flags: PropTypes.object.isRequired, 45 | }; 46 | 47 | export default withLDConsumer()(Home); 48 | -------------------------------------------------------------------------------- /examples/hoc/src/universal/hooksDemo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { useFlags } from 'launchdarkly-react-client-sdk'; 4 | 5 | const Root = styled.div` 6 | color: #001b44; 7 | `; 8 | const Heading = styled.h1` 9 | color: #00449e; 10 | `; 11 | const ListItem = styled.li` 12 | margin-top: 10px; 13 | `; 14 | const FlagDisplay = styled.div` 15 | font-size: 20px; 16 | font-weight: bold; 17 | `; 18 | const FlagOn = styled.span` 19 | color: #96bf01; 20 | `; 21 | const HooksDemo = () => { 22 | const { devTestFlag } = useFlags(); 23 | 24 | return ( 25 | 26 | Hooks Demo 27 |
28 | This is the equivalent demo app using hooks. To run this example: 29 |
    30 | 31 | In app.js, set clientSideID to your own Client-side ID. You can find this in your ld portal under Account 32 | settings / Projects. 33 | 34 | 35 | Create a flag called dev-test-flag in your project. Make sure you make it available for the client side js 36 | sdk. 37 | 38 | Turn the flag on and off to see this app respond without a browser refresh. 39 |
40 |
41 | {devTestFlag ? Flag on : Flag off} 42 |
43 | ); 44 | }; 45 | 46 | export default HooksDemo; 47 | -------------------------------------------------------------------------------- /examples/hoc/src/universal/siteNav.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { css } from 'styled-components'; 3 | import { Link } from 'react-router-dom'; 4 | 5 | export default () => ( 6 |
13 | Home 14 | Hooks Demo 15 |
16 | ); 17 | -------------------------------------------------------------------------------- /examples/hoc/webpack.config.client.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const WebpackServeUrl = 'http://localhost:3002'; 4 | 5 | module.exports = { 6 | mode: 'development', 7 | devtool: 'source-map', 8 | entry: ['@babel/polyfill', './src/client/index'], 9 | output: { 10 | path: path.resolve('dist'), 11 | publicPath: `${WebpackServeUrl}/dist/`, // MUST BE FULL PATH! 12 | filename: 'bundle.js', 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.jsx?$/, 18 | include: path.resolve('src'), 19 | exclude: /node_modules/, 20 | loader: 'babel-loader', 21 | options: { 22 | cacheDirectory: true, 23 | }, 24 | }, 25 | ], 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /examples/hoc/webpack.config.server.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const nodeExternals = require('webpack-node-externals'); 3 | 4 | module.exports = { 5 | mode: 'development', 6 | devtool: 'source-map', 7 | entry: ['@babel/polyfill', './src/server/server.js'], // set this to your server entry point. This should be where you start your express server with .listen() 8 | target: 'node', // tell webpack this bundle will be used in nodejs environment. 9 | externals: [nodeExternals()], // Omit node_modules code from the bundle. You don't want and don't need them in the bundle. 10 | output: { 11 | path: path.resolve('dist'), 12 | filename: 'serverBundle.js', 13 | libraryTarget: 'commonjs2', // IMPORTANT! Add module.exports to the beginning of the bundle, so universal-hot-reload can access your app. 14 | }, 15 | // The rest of the config is pretty standard and can contain other webpack stuff you need. 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.jsx?$/, 20 | include: path.resolve('src'), 21 | exclude: /node_modules/, 22 | loader: 'babel-loader', 23 | options: { 24 | cacheDirectory: true, 25 | }, 26 | }], 27 | }, 28 | }; -------------------------------------------------------------------------------- /examples/typescript/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /examples/typescript/README.md: -------------------------------------------------------------------------------- 1 | # LaunchDarkly SDK for React - Example typescript app 2 | 3 | This is a CRA typescript app demonstrating `launchdarkly-react-client-sdk`. 4 | 5 | ## Running the typescript example 6 | 7 | Follow these steps to run the app: 8 | 9 | * Create a `.env.local` file and set your clientSideID there like so: 10 | 11 | ```shell 12 | REACT_APP_LD_CLIENT_SIDE_ID=xxx 13 | ``` 14 | 15 | * Create a flag called `dev-test-flag` in your project. Make sure you 16 | make the flag available to the client-side SDK. 17 | 18 | * You should now be able to start the app by doing: 19 | 20 | ```sh 21 | yarn && yarn start 22 | ``` 23 | 24 | * Toggle the killswitch for `dev-test-flag` in the dashboard and the 25 | app should respond without a browser refresh. 26 | 27 | # Getting Started with Create React App 28 | 29 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 30 | 31 | ## Available Scripts 32 | 33 | In the project directory, you can run: 34 | 35 | ### `npm start` 36 | 37 | Runs the app in the development mode.\ 38 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 39 | 40 | The page will reload if you make edits.\ 41 | You will also see any lint errors in the console. 42 | 43 | ### `npm test` 44 | 45 | Launches the test runner in the interactive watch mode.\ 46 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 47 | 48 | ### `npm run build` 49 | 50 | Builds the app for production to the `build` folder.\ 51 | It correctly bundles React in production mode and optimizes the build for the best performance. 52 | 53 | The build is minified and the filenames include the hashes.\ 54 | Your app is ready to be deployed! 55 | 56 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 57 | 58 | ### `npm run eject` 59 | 60 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 61 | 62 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 63 | 64 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 65 | 66 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 67 | 68 | ## Learn More 69 | 70 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 71 | 72 | To learn React, check out the [React documentation](https://reactjs.org/). 73 | -------------------------------------------------------------------------------- /examples/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.5", 7 | "@testing-library/react": "^13.4.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "@types/jest": "^27.5.2", 10 | "@types/node": "^16.18.18", 11 | "@types/react": "^18.0.28", 12 | "@types/react-dom": "^18.0.11", 13 | "launchdarkly-react-client-sdk": "^3.0.8", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0", 16 | "react-scripts": "5.0.1", 17 | "typescript": "^4.9.5", 18 | "web-vitals": "^2.1.4" 19 | }, 20 | "scripts": { 21 | "analyze": "source-map-explorer 'build/static/js/*.js'", 22 | "start": "react-scripts start", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test", 25 | "eject": "react-scripts eject", 26 | "postinstall": "cd ../../ && yarn link-dev", 27 | "tsc": "tsc" 28 | }, 29 | "eslintConfig": { 30 | "extends": [ 31 | "react-app", 32 | "react-app/jest" 33 | ] 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | }, 47 | "devDependencies": { 48 | "source-map-explorer": "2.5.3" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/typescript/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/typescript/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /examples/typescript/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/typescript/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import logo from './logo.svg'; 3 | import './App.css'; 4 | import { useFlags } from 'launchdarkly-react-client-sdk'; 5 | 6 | function App() { 7 | const { devTestFlag } = useFlags(); 8 | 9 | return ( 10 |
11 |
12 | logo 13 |

{devTestFlag ? Flag on : Flag off}

14 | 15 | Learn React 16 | 17 |
18 |
19 | ); 20 | } 21 | 22 | export default App; 23 | -------------------------------------------------------------------------------- /examples/typescript/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /examples/typescript/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | import { asyncWithLDProvider, LDContext } from 'launchdarkly-react-client-sdk'; 7 | 8 | (async () => { 9 | // Set clientSideID to your own Client-side ID. You can find this in 10 | // your LaunchDarkly portal under Account settings / Projects 11 | const context: LDContext = { 12 | kind: 'user', 13 | key: 'test-user-1', 14 | }; 15 | 16 | const LDProvider = await asyncWithLDProvider({ 17 | clientSideID: process.env.REACT_APP_LD_CLIENT_SIDE_ID ?? '', 18 | context, 19 | }); 20 | 21 | const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); 22 | root.render( 23 | 24 | 25 | 26 | 27 | , 28 | ); 29 | 30 | // If you want to start measuring performance in your app, pass a function 31 | // to log results (for example: reportWebVitals(console.log)) 32 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 33 | reportWebVitals(); 34 | })(); 35 | -------------------------------------------------------------------------------- /examples/typescript/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/typescript/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/typescript/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /examples/typescript/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /examples/typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reporters: ['default'], 3 | moduleFileExtensions: ['ts', 'tsx', 'js'], 4 | transform: { 5 | '\\.(ts|tsx)$': 'ts-jest', 6 | }, 7 | testRegex: '.*\\.test\\.(ts|tsx)$', 8 | testPathIgnorePatterns: ['/node_modules/', '/examples/'], 9 | testEnvironment: 'jest-environment-jsdom-global', 10 | }; 11 | -------------------------------------------------------------------------------- /link-dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "===== Installing all dependencies..." 4 | npm install --force 5 | 6 | echo "===== Building react sdk" 7 | npm run build 8 | 9 | echo "===== Install prod dependencies" 10 | rm -rf node_modules 11 | npm install --force --omit=dev 12 | 13 | echo "===== Linking to examples" 14 | declare -a examples=(async-provider hoc typescript deferred-initialization) 15 | 16 | for example in "${examples[@]}" 17 | do 18 | echo "===== Linking to $example example" 19 | mkdir -p examples/${example}/node_modules 20 | rm -rf examples/${example}/node_modules/launchdarkly-react-client-sdk 21 | mkdir -p examples/${example}/node_modules/launchdarkly-react-client-sdk/node_modules 22 | mkdir -p examples/${example}/node_modules/launchdarkly-react-client-sdk/lib 23 | cp package.json examples/${example}/node_modules/launchdarkly-react-client-sdk/package.json 24 | cp -r node_modules/* examples/${example}/node_modules/launchdarkly-react-client-sdk/node_modules/ 25 | cp -r lib/* examples/${example}/node_modules/launchdarkly-react-client-sdk/lib/ 26 | done 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "launchdarkly-react-client-sdk", 3 | "version": "3.8.1", 4 | "description": "LaunchDarkly SDK for React", 5 | "author": "LaunchDarkly ", 6 | "license": "Apache-2.0", 7 | "keywords": [ 8 | "launchdarkly", 9 | "launch", 10 | "darkly", 11 | "react", 12 | "sdk", 13 | "bindings" 14 | ], 15 | "exports": { 16 | "types": "./lib/index.d.ts", 17 | "require": "./lib/cjs/index.js", 18 | "import": "./lib/esm/index.js" 19 | }, 20 | "main": "./lib/cjs/index.js", 21 | "types": "./lib/index.d.ts", 22 | "files": [ 23 | "lib", 24 | "src", 25 | "!**/*.test.*", 26 | "!**/__snapshots__" 27 | ], 28 | "scripts": { 29 | "test": "jest", 30 | "test:junit": "jest --ci --reporters=default", 31 | "clean": "rimraf lib/*", 32 | "rb": "rollup -c --configPlugin typescript", 33 | "rbw": "npm run rb --watch", 34 | "build": "npm run clean && npm run rb", 35 | "lint": "eslint ./src", 36 | "check-typescript": "tsc", 37 | "prepublishOnly": "npm run build", 38 | "prettier": "prettier --write 'src/*.@(js|ts|tsx|json|css)'", 39 | "link-dev": "./link-dev.sh", 40 | "check": "npm i && npm run prettier && npm run lint && tsc && npm run test", 41 | "doc": "typedoc" 42 | }, 43 | "repository": { 44 | "type": "git", 45 | "url": "git://github.com/launchdarkly/react-client-sdk.git" 46 | }, 47 | "homepage": "https://github.com/launchdarkly/react-client-sdk", 48 | "devDependencies": { 49 | "@eslint/js": "^9.7.0", 50 | "@rollup/plugin-json": "^6.0.0", 51 | "@rollup/plugin-node-resolve": "^15.1.0", 52 | "@rollup/plugin-terser": "^0.4.3", 53 | "@rollup/plugin-typescript": "^12.1.1", 54 | "@testing-library/dom": "^10.4.0", 55 | "@testing-library/jest-dom": "^6.4.8", 56 | "@testing-library/react": "^16.0.0", 57 | "@types/eslint__js": "^8.42.3", 58 | "@types/hoist-non-react-statics": "^3.3.1", 59 | "@types/jest": "^29.5.12", 60 | "@types/lodash.camelcase": "^4.3.6", 61 | "@types/node": "^22.1.0", 62 | "@types/prop-types": "^15.7.4", 63 | "@types/react": "^18.0.3", 64 | "@types/react-dom": "^18.0.0", 65 | "@types/react-test-renderer": "^18.0.0", 66 | "esbuild": "^0.24.0", 67 | "eslint": "^9.8.0", 68 | "jest": "^29.7.0", 69 | "jest-environment-jsdom": "^29.7.0", 70 | "jest-environment-jsdom-global": "^4.0.0", 71 | "prettier": "^3.3.3", 72 | "prop-types": "^15.7.2", 73 | "react": "^18.0.0", 74 | "react-dom": "^18.0.0", 75 | "react-test-renderer": "^18.0.0", 76 | "rimraf": "^6.0.1", 77 | "rollup": "^4.19.0", 78 | "rollup-plugin-dts": "^6.1.0", 79 | "rollup-plugin-esbuild": "^6.1.1", 80 | "ts-jest": "^29.2.2", 81 | "tslib": "^2.8.1", 82 | "typedoc": "^0.26.5", 83 | "typescript": "^5.5.4", 84 | "typescript-eslint": "^8.0.0", 85 | "parse5": "7.2.1" 86 | }, 87 | "dependencies": { 88 | "hoist-non-react-statics": "^3.3.2", 89 | "launchdarkly-js-client-sdk": "^3.8.1", 90 | "lodash.camelcase": "^4.3.0" 91 | }, 92 | "peerDependencies": { 93 | "react": "^16.6.3 || ^17.0.0 || ^18.0.0 || ^19.0.0", 94 | "react-dom": "^16.8.4 || ^17.0.0 || ^18.0.0 || ^19.0.0" 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bootstrap-sha": "26eef1a9bb65f94489eff6ff7d2cd2abc969c952", 3 | "packages": { 4 | ".": { 5 | "release-type": "node" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /rollup.config.ts: -------------------------------------------------------------------------------- 1 | import dts from 'rollup-plugin-dts'; 2 | import esbuild from 'rollup-plugin-esbuild'; 3 | import json from '@rollup/plugin-json'; 4 | import resolve from '@rollup/plugin-node-resolve'; 5 | import terser from '@rollup/plugin-terser'; 6 | 7 | const plugins = [resolve(), esbuild({ target: 'es6' }), json(), terser()]; 8 | 9 | const external = /node_modules/; 10 | 11 | export default [ 12 | { 13 | input: 'src/index.ts', 14 | output: [ 15 | { 16 | file: 'lib/cjs/index.js', 17 | format: 'cjs', 18 | sourcemap: true, 19 | }, 20 | ], 21 | plugins, 22 | external, 23 | }, 24 | { 25 | input: 'src/index.ts', 26 | output: [ 27 | { 28 | file: 'lib/esm/index.js', 29 | format: 'esm', 30 | sourcemap: true, 31 | }, 32 | ], 33 | plugins, 34 | external, 35 | }, 36 | { 37 | input: 'src/index.ts', 38 | plugins: [dts(), json()], 39 | output: { 40 | file: 'lib/index.d.ts', 41 | format: 'es', 42 | }, 43 | }, 44 | ]; 45 | -------------------------------------------------------------------------------- /scripts/better-audit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script processes the output of "npm audit" to make it more useful, as follows: 4 | # - For each flagged vulnerability, it looks at the "path" field and extracts both the flagged 5 | # package (the last element in the path) and the topmost dependency that led to it (the first 6 | # element in the path). 7 | # - It sorts these and eliminates duplicates. 8 | # - It then compares each of the topmost dependencies to package.json to see if it is from 9 | # "dependencies", "peerDependencies", or "devDependencies". If it is either of the first two 10 | # then this is a real runtime vulnerability, and must be fixed by updating the topmost 11 | # dependency. If it is from devDependencies, then it can be safely fixed with "npm audit fix". 12 | 13 | set -e 14 | 15 | function readPackages() { 16 | inCategory=$1 17 | jq -r ".${inCategory} | keys | .[]" package.json 2>/dev/null || true 18 | } 19 | 20 | function isInList() { 21 | item=$1 22 | shift 23 | for x in $@; do 24 | if [ "$item" == "$x" ]; then 25 | true 26 | return 27 | fi 28 | done 29 | false 30 | } 31 | 32 | dependencies=$(readPackages dependencies) 33 | devDependencies=$(readPackages devDependencies) 34 | peerDependencies=$(readPackages peerDependencies) 35 | 36 | function processItems() { 37 | flaggedRuntime=0 38 | flaggedDev=0 39 | while read -r badPackage topLevelDep; do 40 | echo -n "flagged package \"$badPackage\", referenced via \"$topLevelDep\" " 41 | for category in dependencies peerDependencies devDependencies; do 42 | if isInList $topLevelDep ${!category}; then 43 | if [ "$category" == "devDependencies" ]; then 44 | echo "-- from \"$category\"" 45 | flaggedDev=1 46 | else 47 | echo "-- from \"$category\" (RUNTIME) ***" 48 | flaggedRuntime=1 49 | fi 50 | break 51 | fi 52 | done 53 | done 54 | echo 55 | if [ "$flaggedRuntime" == "1" ]; then 56 | echo "*** At least one runtime dependency was flagged. These must be fixed by updating package.json." 57 | echo "Do not use 'npm audit fix'." 58 | exit 1 # return an error, causing the build to fail 59 | elif [ "$flaggedDev" == "1" ]; then 60 | echo "Only development dependencies were flagged. You may safely run 'npm audit fix', which will" 61 | echo "fix these by adding overrides to package-lock.json." 62 | else 63 | echo "Congratulations! No dependencies were flagged by 'npm audit'." 64 | fi 65 | } 66 | 67 | echo "Running npm audit..." 68 | echo 69 | 70 | npm audit --json \ 71 | | grep '"path":' \ 72 | | sort | uniq \ 73 | | sed -n -e 's#.*"path": "\([^"]*\)".*#\1#p' \ 74 | | awk -F '>' '{ print $NF,$1 }' \ 75 | | sort | uniq \ 76 | | processItems 77 | -------------------------------------------------------------------------------- /scripts/packaging-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | npm pack 6 | TARBALL=$(ls *.tgz) 7 | trap "rm ${TARBALL}" EXIT 8 | 9 | tar tfz ${TARBALL} | grep '^package/lib/.*\.js$' || (echo "tarball contained no .js files"; exit 1) 10 | tar tfz ${TARBALL} | grep '^package/lib/.*\.d\.ts$' || (echo "tarball contained no .d.ts files"; exit 1) 11 | 12 | echo "tarball contained .js and .d.ts files in package/lib/ - OK" 13 | -------------------------------------------------------------------------------- /scripts/publish-npm.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | if $LD_RELEASE_IS_DRYRUN ; then 3 | echo "Doing a dry run of publishing." 4 | else 5 | if $LD_RELEASE_IS_PRERELEASE ; then 6 | echo "Publishing with prerelease tag." 7 | npm publish --tag prerelease --provenance --access public || { echo "npm publish failed" >&2; exit 1; } 8 | else 9 | npm publish --provenance --access public || { echo "npm publish failed" >&2; exit 1; } 10 | fi 11 | fi 12 | -------------------------------------------------------------------------------- /src/__snapshots__/asyncWithLDProvider.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`asyncWithLDProvider provider renders app correctly 1`] = ` 4 |
5 | My App 6 |
7 | `; 8 | -------------------------------------------------------------------------------- /src/__snapshots__/provider.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`LDProvider render app 1`] = ` 4 | 14 |
15 | My App 16 |
17 |
18 | `; 19 | -------------------------------------------------------------------------------- /src/__snapshots__/withLDConsumer.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`withLDConsumer flags are passed down through context api 1`] = ` 4 |
5 | testFlag detected 6 |
7 | `; 8 | 9 | exports[`withLDConsumer ldClient is passed down through context api 1`] = ` 10 |
11 | ldClient detected 12 |
13 | `; 14 | 15 | exports[`withLDConsumer only ldClient is passed down through context api 1`] = ` 16 |
17 | Negative, no flag 18 | ldClient detected 19 |
20 | `; 21 | -------------------------------------------------------------------------------- /src/__snapshots__/withLDProvider.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`withLDProvider render app 1`] = ` 4 | 14 |
15 | My App 16 |
17 |
18 | `; 19 | -------------------------------------------------------------------------------- /src/asyncWithLDProvider.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '@testing-library/dom'; 3 | import '@testing-library/jest-dom'; 4 | import { render } from '@testing-library/react'; 5 | import { initialize, LDClient, LDContext, LDFlagChangeset, LDOptions } from 'launchdarkly-js-client-sdk'; 6 | import { AsyncProviderConfig, LDReactOptions } from './types'; 7 | import { Consumer, reactSdkContextFactory } from './context'; 8 | import asyncWithLDProvider from './asyncWithLDProvider'; 9 | import wrapperOptions from './wrapperOptions'; 10 | import { fetchFlags } from './utils'; 11 | 12 | jest.mock('launchdarkly-js-client-sdk', () => { 13 | const actual = jest.requireActual('launchdarkly-js-client-sdk'); 14 | 15 | return { 16 | ...actual, 17 | initialize: jest.fn(), 18 | }; 19 | }); 20 | jest.mock('./utils', () => { 21 | const originalModule = jest.requireActual('./utils'); 22 | 23 | return { 24 | ...originalModule, 25 | fetchFlags: jest.fn(), 26 | }; 27 | }); 28 | 29 | const clientSideID = 'test-client-side-id'; 30 | const context: LDContext = { key: 'yus', kind: 'user', name: 'yus ng' }; 31 | const rawFlags = { 'test-flag': true, 'another-test-flag': true }; 32 | 33 | const App = () => <>My App; 34 | const mockInitialize = initialize as jest.Mock; 35 | const mockFetchFlags = fetchFlags as jest.Mock; 36 | let mockLDClient: { on: jest.Mock; off: jest.Mock; variation: jest.Mock; waitForInitialization: jest.Mock }; 37 | 38 | const renderWithConfig = async (config: AsyncProviderConfig) => { 39 | const LDProvider = await asyncWithLDProvider(config); 40 | 41 | const { getByText } = render( 42 | 43 | 44 | {(value) => ( 45 | 46 | Received:{' '} 47 | {`Flags: ${JSON.stringify(value.flags)}. 48 | Error: ${value.error?.message}. 49 | ldClient: ${value.ldClient ? 'initialized' : 'undefined'}.`} 50 | 51 | )} 52 | 53 | , 54 | ); 55 | 56 | return getByText(/^Received:/); 57 | }; 58 | 59 | describe('asyncWithLDProvider', () => { 60 | let options: LDOptions; 61 | let rejectWaitForInitialization: () => void; 62 | 63 | beforeEach(() => { 64 | mockLDClient = { 65 | on: jest.fn((_e: string, cb: () => void) => { 66 | cb(); 67 | }), 68 | off: jest.fn(), 69 | variation: jest.fn((_: string, v) => v), 70 | waitForInitialization: jest.fn(), 71 | }; 72 | mockInitialize.mockImplementation(() => mockLDClient); 73 | mockFetchFlags.mockImplementation(() => rawFlags); 74 | rejectWaitForInitialization = () => { 75 | const timeoutError = new Error('waitForInitialization timed out'); 76 | timeoutError.name = 'TimeoutError'; 77 | mockLDClient.waitForInitialization.mockRejectedValue(timeoutError); 78 | }; 79 | options = { bootstrap: {}, ...wrapperOptions }; 80 | }); 81 | 82 | afterEach(() => { 83 | jest.resetAllMocks(); 84 | }); 85 | 86 | test('provider renders app correctly', async () => { 87 | const LDProvider = await asyncWithLDProvider({ clientSideID }); 88 | const { container } = render( 89 | 90 | 91 | , 92 | ); 93 | 94 | expect(container).toMatchSnapshot(); 95 | }); 96 | 97 | test('provider unmounts and unsubscribes correctly', async () => { 98 | const LDProvider = await asyncWithLDProvider({ clientSideID }); 99 | const { unmount } = render( 100 | 101 | 102 | , 103 | ); 104 | unmount(); 105 | 106 | expect(mockLDClient.off).toHaveBeenCalledWith('change', expect.any(Function)); 107 | expect(mockLDClient.off).toHaveBeenCalledWith('failed', expect.any(Function)); 108 | expect(mockLDClient.off).toHaveBeenCalledWith('ready', expect.any(Function)); 109 | }); 110 | 111 | test('timeout error; provider unmounts and unsubscribes correctly', async () => { 112 | rejectWaitForInitialization(); 113 | const LDProvider = await asyncWithLDProvider({ clientSideID }); 114 | const { unmount } = render( 115 | 116 | 117 | , 118 | ); 119 | unmount(); 120 | 121 | expect(mockLDClient.off).toHaveBeenCalledWith('change', expect.any(Function)); 122 | expect(mockLDClient.off).toHaveBeenCalledWith('failed', expect.any(Function)); 123 | expect(mockLDClient.off).toHaveBeenCalledWith('ready', expect.any(Function)); 124 | }); 125 | 126 | test('waitForInitialization error (not timeout)', async () => { 127 | mockLDClient.waitForInitialization.mockRejectedValue(new Error('TestError')); 128 | const receivedNode = await renderWithConfig({ clientSideID }); 129 | 130 | expect(receivedNode).toHaveTextContent('TestError'); 131 | expect(mockLDClient.on).not.toHaveBeenCalledWith('ready', expect.any(Function)); 132 | expect(mockLDClient.on).not.toHaveBeenCalledWith('failed', expect.any(Function)); 133 | }); 134 | 135 | test('subscribe to ready and failed events if waitForInitialization timed out', async () => { 136 | rejectWaitForInitialization(); 137 | const LDProvider = await asyncWithLDProvider({ clientSideID }); 138 | render( 139 | 140 | 141 | , 142 | ); 143 | 144 | expect(mockLDClient.on).toHaveBeenCalledWith('ready', expect.any(Function)); 145 | expect(mockLDClient.on).toHaveBeenCalledWith('failed', expect.any(Function)); 146 | }); 147 | 148 | test('ready handler should update flags', async () => { 149 | mockLDClient.on.mockImplementation((e: string, cb: () => void) => { 150 | // focus only on the ready handler and ignore other change and failed. 151 | if (e === 'ready') { 152 | cb(); 153 | } 154 | }); 155 | rejectWaitForInitialization(); 156 | const receivedNode = await renderWithConfig({ clientSideID }); 157 | 158 | expect(mockLDClient.on).toHaveBeenCalledWith('ready', expect.any(Function)); 159 | expect(receivedNode).toHaveTextContent('{"testFlag":true,"anotherTestFlag":true}'); 160 | }); 161 | 162 | test('failed handler should update error', async () => { 163 | mockLDClient.on.mockImplementation((e: string, cb: (e: Error) => void) => { 164 | // focus only on the ready handler and ignore other change and failed. 165 | if (e === 'failed') { 166 | cb(new Error('Test sdk failure')); 167 | } 168 | }); 169 | rejectWaitForInitialization(); 170 | const receivedNode = await renderWithConfig({ clientSideID }); 171 | 172 | expect(mockLDClient.on).toHaveBeenCalledWith('ready', expect.any(Function)); 173 | expect(receivedNode).toHaveTextContent('{}'); 174 | expect(receivedNode).toHaveTextContent('Error: Test sdk failure'); 175 | }); 176 | 177 | test('ldClient is initialised correctly', async () => { 178 | const reactOptions: LDReactOptions = { useCamelCaseFlagKeys: false }; 179 | await asyncWithLDProvider({ clientSideID, context, options, reactOptions }); 180 | 181 | expect(mockInitialize).toHaveBeenCalledWith(clientSideID, context, options); 182 | }); 183 | 184 | test('ld client is initialised correctly with deprecated user object', async () => { 185 | const user: LDContext = { key: 'deprecatedUser' }; 186 | const reactOptions: LDReactOptions = { useCamelCaseFlagKeys: false }; 187 | await asyncWithLDProvider({ clientSideID, user, options, reactOptions }); 188 | 189 | expect(mockInitialize).toHaveBeenCalledWith(clientSideID, user, options); 190 | }); 191 | 192 | test('use context ignore user at init if both are present', async () => { 193 | const user: LDContext = { key: 'deprecatedUser' }; 194 | const reactOptions: LDReactOptions = { useCamelCaseFlagKeys: false }; 195 | 196 | // this should not happen in real usage. Only one of context or user should be specified. 197 | // if both are specified, context will be used and user ignored. 198 | await asyncWithLDProvider({ clientSideID, context, user, options, reactOptions }); 199 | 200 | expect(mockInitialize).toHaveBeenCalledWith(clientSideID, context, options); 201 | }); 202 | 203 | test('subscribe to changes on mount', async () => { 204 | const LDProvider = await asyncWithLDProvider({ clientSideID }); 205 | render( 206 | 207 | 208 | , 209 | ); 210 | expect(mockLDClient.on).toHaveBeenNthCalledWith(1, 'change', expect.any(Function)); 211 | }); 212 | 213 | test('subscribe to changes with camelCase', async () => { 214 | mockLDClient.on.mockImplementation((_e: string, cb: (c: LDFlagChangeset) => void) => { 215 | cb({ 'test-flag': { current: false, previous: true } }); 216 | }); 217 | 218 | const receivedNode = await renderWithConfig({ clientSideID }); 219 | 220 | expect(mockLDClient.on).toHaveBeenNthCalledWith(1, 'change', expect.any(Function)); 221 | expect(receivedNode).toHaveTextContent('{"testFlag":false,"anotherTestFlag":true}'); 222 | expect(receivedNode).toHaveTextContent('Error: undefined'); 223 | }); 224 | 225 | test('subscribe to changes with kebab-case', async () => { 226 | mockLDClient.on.mockImplementation((_e: string, cb: (c: LDFlagChangeset) => void) => { 227 | cb({ 'another-test-flag': { current: false, previous: true }, 'test-flag': { current: false, previous: true } }); 228 | }); 229 | const receivedNode = await renderWithConfig({ clientSideID, reactOptions: { useCamelCaseFlagKeys: false } }); 230 | 231 | expect(mockLDClient.on).toHaveBeenNthCalledWith(1, 'change', expect.any(Function)); 232 | expect(receivedNode).toHaveTextContent('{"test-flag":false,"another-test-flag":false}'); 233 | }); 234 | 235 | test('consecutive flag changes gets stored in context correctly', async () => { 236 | mockLDClient.on.mockImplementationOnce((_e: string, cb: (c: LDFlagChangeset) => void) => { 237 | cb({ 'another-test-flag': { current: false, previous: true } }); 238 | 239 | // simulate second update 240 | cb({ 'test-flag': { current: false, previous: true } }); 241 | }); 242 | 243 | const receivedNode = await renderWithConfig({ clientSideID }); 244 | 245 | expect(mockLDClient.on).toHaveBeenNthCalledWith(1, 'change', expect.any(Function)); 246 | expect(receivedNode).toHaveTextContent('{"testFlag":false,"anotherTestFlag":false}'); 247 | }); 248 | 249 | test('ldClient bootstraps correctly', async () => { 250 | // don't subscribe to changes to test bootstrap 251 | mockLDClient.on.mockImplementation((_e: string, _cb: (c: LDFlagChangeset) => void) => { 252 | return; 253 | }); 254 | options = { 255 | bootstrap: { 256 | 'another-test-flag': false, 257 | 'test-flag': true, 258 | }, 259 | }; 260 | const receivedNode = await renderWithConfig({ clientSideID, context, options }); 261 | expect(receivedNode).toHaveTextContent('{"anotherTestFlag":false,"testFlag":true}'); 262 | }); 263 | 264 | test('undefined bootstrap', async () => { 265 | mockLDClient.on.mockImplementation((_e: string, _cb: (c: LDFlagChangeset) => void) => { 266 | return; 267 | }); 268 | options = { ...options, bootstrap: undefined }; 269 | mockFetchFlags.mockReturnValueOnce({ aNewFlag: true }); 270 | const receivedNode = await renderWithConfig({ clientSideID, context, options }); 271 | 272 | expect(mockFetchFlags).toHaveBeenCalledTimes(1); 273 | expect(receivedNode).toHaveTextContent('{"aNewFlag":true}'); 274 | }); 275 | 276 | test('bootstrap used if there is a timeout', async () => { 277 | mockLDClient.on.mockImplementation((_e: string, _cb: (c: LDFlagChangeset) => void) => { 278 | return; 279 | }); 280 | rejectWaitForInitialization(); 281 | options = { ...options, bootstrap: { myBootstrap: true } }; 282 | const receivedNode = await renderWithConfig({ clientSideID, context, options }); 283 | 284 | expect(mockFetchFlags).not.toHaveBeenCalled(); 285 | expect(receivedNode).toHaveTextContent('{"myBootstrap":true}'); 286 | expect(receivedNode).toHaveTextContent('timed out'); 287 | }); 288 | 289 | test('ldClient bootstraps with empty flags', async () => { 290 | // don't subscribe to changes to test bootstrap 291 | mockLDClient.on.mockImplementation((_e: string, _cb: (c: LDFlagChangeset) => void) => { 292 | return; 293 | }); 294 | options = { 295 | bootstrap: {}, 296 | }; 297 | const receivedNode = await renderWithConfig({ clientSideID, context, options }); 298 | expect(receivedNode).toHaveTextContent('{}'); 299 | }); 300 | 301 | test('ldClient bootstraps correctly with kebab-case', async () => { 302 | // don't subscribe to changes to test bootstrap 303 | mockLDClient.on.mockImplementation((_e: string, _cb: (c: LDFlagChangeset) => void) => { 304 | return; 305 | }); 306 | options = { 307 | bootstrap: { 308 | 'another-test-flag': false, 309 | 'test-flag': true, 310 | }, 311 | }; 312 | const receivedNode = await renderWithConfig({ 313 | clientSideID, 314 | context, 315 | options, 316 | reactOptions: { useCamelCaseFlagKeys: false }, 317 | }); 318 | expect(receivedNode).toHaveTextContent('{"another-test-flag":false,"test-flag":true}'); 319 | }); 320 | 321 | test('internal flags state should be initialised to all flags', async () => { 322 | options = { 323 | bootstrap: 'localStorage', 324 | }; 325 | const receivedNode = await renderWithConfig({ clientSideID, context, options }); 326 | expect(receivedNode).toHaveTextContent('{"testFlag":true,"anotherTestFlag":true}'); 327 | }); 328 | 329 | test('internal ldClient state should be initialised', async () => { 330 | const receivedNode = await renderWithConfig({ clientSideID, context, options }); 331 | expect(receivedNode).toHaveTextContent('ldClient: initialized'); 332 | }); 333 | 334 | test('ldClient is initialised correctly with target flags', async () => { 335 | options = { ...wrapperOptions }; 336 | const flags = { 'test-flag': false }; 337 | const receivedNode = await renderWithConfig({ clientSideID, context, options, flags }); 338 | 339 | expect(mockInitialize).toHaveBeenCalledWith(clientSideID, context, options); 340 | expect(receivedNode).toHaveTextContent('{"testFlag":true}'); 341 | }); 342 | 343 | test('only updates to subscribed flags are pushed to the Provider', async () => { 344 | mockLDClient.on.mockImplementation((_e: string, cb: (c: LDFlagChangeset) => void) => { 345 | cb({ 'test-flag': { current: false, previous: true }, 'another-test-flag': { current: false, previous: true } }); 346 | }); 347 | options = {}; 348 | const subscribedFlags = { 'test-flag': true }; 349 | const receivedNode = await renderWithConfig({ clientSideID, context, options, flags: subscribedFlags }); 350 | 351 | expect(receivedNode).toHaveTextContent('{"testFlag":false}'); 352 | }); 353 | 354 | test('custom context is provided to consumer', async () => { 355 | const CustomContext = reactSdkContextFactory(); 356 | const customLDClient = { 357 | on: jest.fn((_: string, cb: () => void) => { 358 | cb(); 359 | }), 360 | off: jest.fn(), 361 | allFlags: jest.fn().mockReturnValue({ 'context-test-flag': true }), 362 | variation: jest.fn((_: string, v) => v), 363 | waitForInitialization: jest.fn(), 364 | }; 365 | const config: AsyncProviderConfig = { 366 | clientSideID, 367 | ldClient: customLDClient as unknown as LDClient, 368 | reactOptions: { 369 | reactContext: CustomContext, 370 | }, 371 | }; 372 | const originalUtilsModule = jest.requireActual('./utils'); 373 | mockFetchFlags.mockImplementation(originalUtilsModule.fetchFlags); 374 | 375 | const LDProvider = await asyncWithLDProvider(config); 376 | const LaunchDarklyApp = ( 377 | 378 | 379 | {({ flags }) => { 380 | return ( 381 | 382 | flag is {flags.contextTestFlag === undefined ? 'undefined' : JSON.stringify(flags.contextTestFlag)} 383 | 384 | ); 385 | }} 386 | 387 | 388 | ); 389 | 390 | const { findByText } = render(LaunchDarklyApp); 391 | expect(await findByText('flag is true')).not.toBeNull(); 392 | 393 | const receivedNode = await renderWithConfig({ clientSideID }); 394 | expect(receivedNode).not.toHaveTextContent('{"contextTestFlag":true}'); 395 | }); 396 | 397 | test('multiple providers', async () => { 398 | const customLDClient1 = { 399 | on: jest.fn((_: string, cb: () => void) => { 400 | cb(); 401 | }), 402 | off: jest.fn(), 403 | allFlags: jest.fn().mockReturnValue({ 'context1-test-flag': true }), 404 | variation: jest.fn((_: string, v) => v), 405 | waitForInitialization: jest.fn(), 406 | }; 407 | const customLDClient2 = { 408 | on: jest.fn((_: string, cb: () => void) => { 409 | cb(); 410 | }), 411 | off: jest.fn(), 412 | allFlags: jest.fn().mockReturnValue({ 'context2-test-flag': true }), 413 | variation: jest.fn((_: string, v) => v), 414 | waitForInitialization: jest.fn(), 415 | }; 416 | const originalUtilsModule = jest.requireActual('./utils'); 417 | mockFetchFlags.mockImplementation(originalUtilsModule.fetchFlags); 418 | 419 | const CustomContext1 = reactSdkContextFactory(); 420 | const LDProvider1 = await asyncWithLDProvider({ 421 | clientSideID, 422 | ldClient: customLDClient1 as unknown as LDClient, 423 | reactOptions: { 424 | reactContext: CustomContext1, 425 | }, 426 | }); 427 | const CustomContext2 = reactSdkContextFactory(); 428 | const LDProvider2 = await asyncWithLDProvider({ 429 | clientSideID, 430 | ldClient: customLDClient2 as unknown as LDClient, 431 | reactOptions: { 432 | reactContext: CustomContext2, 433 | }, 434 | }); 435 | const safeValue = (val?: boolean) => (val === undefined ? 'undefined' : JSON.stringify(val)); 436 | const LaunchDarklyApp = ( 437 | 438 | 439 | 440 | {({ flags }) => { 441 | return ( 442 | <> 443 | consumer 1, flag 1 is {safeValue(flags.context1TestFlag)} 444 | consumer 1, flag 2 is {safeValue(flags.context2TestFlag)} 445 | 446 | ); 447 | }} 448 | 449 | 450 | {({ flags }) => { 451 | return ( 452 | <> 453 | consumer 2, flag 1 is {safeValue(flags.context1TestFlag)} 454 | consumer 2, flag 2 is {safeValue(flags.context2TestFlag)} 455 | 456 | ); 457 | }} 458 | 459 | 460 | 461 | ); 462 | 463 | const { findByText } = render(LaunchDarklyApp); 464 | expect(await findByText('consumer 1, flag 1 is true')).not.toBeNull(); 465 | expect(await findByText('consumer 1, flag 2 is undefined')).not.toBeNull(); 466 | expect(await findByText('consumer 2, flag 1 is undefined')).not.toBeNull(); 467 | expect(await findByText('consumer 2, flag 2 is true')).not.toBeNull(); 468 | }); 469 | }); 470 | -------------------------------------------------------------------------------- /src/asyncWithLDProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, ReactNode } from 'react'; 2 | import { initialize, LDFlagChangeset } from 'launchdarkly-js-client-sdk'; 3 | import { AsyncProviderConfig, defaultReactOptions } from './types'; 4 | import { fetchFlags, getContextOrUser, getFlattenedFlagsFromChangeset } from './utils'; 5 | import getFlagsProxy from './getFlagsProxy'; 6 | import wrapperOptions from './wrapperOptions'; 7 | import ProviderState from './providerState'; 8 | 9 | /** 10 | * This is an async function which initializes LaunchDarkly's JS SDK (`launchdarkly-js-client-sdk`) 11 | * and awaits it so all flags and the ldClient are ready before the consumer app is rendered. 12 | * 13 | * The difference between `withLDProvider` and `asyncWithLDProvider` is that `withLDProvider` initializes 14 | * `launchdarkly-js-client-sdk` at componentDidMount. This means your flags and the ldClient are only available after 15 | * your app has mounted. This can result in a flicker due to flag changes at startup time. 16 | * 17 | * `asyncWithLDProvider` initializes `launchdarkly-js-client-sdk` at the entry point of your app prior to render. 18 | * This means that your flags and the ldClient are ready at the beginning of your app. This ensures your app does not 19 | * flicker due to flag changes at startup time. 20 | * 21 | * `asyncWithLDProvider` accepts a config object which is used to initialize `launchdarkly-js-client-sdk`. 22 | * 23 | * `asyncWithLDProvider` does not support the `deferInitialization` config option because `asyncWithLDProvider` needs 24 | * to be initialized at the entry point prior to render to ensure your flags and the ldClient are ready at the beginning 25 | * of your app. 26 | * 27 | * It returns a provider which is a React FunctionComponent which: 28 | * - saves all flags and the ldClient instance in the context API 29 | * - subscribes to flag changes and propagate them through the context API 30 | * 31 | * @param config - The configuration used to initialize LaunchDarkly's JS SDK 32 | */ 33 | export default async function asyncWithLDProvider(config: AsyncProviderConfig) { 34 | const { clientSideID, flags: targetFlags, options, reactOptions: userReactOptions } = config; 35 | const reactOptions = { ...defaultReactOptions, ...userReactOptions }; 36 | const context = getContextOrUser(config) ?? { anonymous: true, kind: 'user' }; 37 | let error: Error; 38 | let fetchedFlags = {}; 39 | 40 | const ldClient = (await config.ldClient) ?? initialize(clientSideID, context, { ...wrapperOptions, ...options }); 41 | try { 42 | await ldClient.waitForInitialization(config.timeout); 43 | fetchedFlags = fetchFlags(ldClient, targetFlags); 44 | } catch (e) { 45 | error = e as Error; 46 | } 47 | 48 | const initialFlags = options?.bootstrap && options.bootstrap !== 'localStorage' ? options.bootstrap : fetchedFlags; 49 | 50 | const LDProvider = ({ children }: { children: ReactNode }) => { 51 | const [ldData, setLDData] = useState(() => ({ 52 | unproxiedFlags: initialFlags, 53 | ...getFlagsProxy(ldClient, initialFlags, reactOptions, targetFlags), 54 | ldClient, 55 | error, 56 | })); 57 | 58 | useEffect(() => { 59 | function onChange(changes: LDFlagChangeset) { 60 | const updates = getFlattenedFlagsFromChangeset(changes, targetFlags); 61 | if (Object.keys(updates).length > 0) { 62 | setLDData((prevState) => { 63 | const updatedUnproxiedFlags = { ...prevState.unproxiedFlags, ...updates }; 64 | 65 | return { 66 | ...prevState, 67 | unproxiedFlags: updatedUnproxiedFlags, 68 | ...getFlagsProxy(ldClient, updatedUnproxiedFlags, reactOptions, targetFlags), 69 | }; 70 | }); 71 | } 72 | } 73 | ldClient.on('change', onChange); 74 | 75 | function onReady() { 76 | const unproxiedFlags = fetchFlags(ldClient, targetFlags); 77 | setLDData((prevState) => ({ 78 | ...prevState, 79 | unproxiedFlags, 80 | ...getFlagsProxy(ldClient, unproxiedFlags, reactOptions, targetFlags), 81 | })); 82 | } 83 | 84 | function onFailed(e: Error) { 85 | setLDData((prevState) => ({ ...prevState, error: e })); 86 | } 87 | 88 | // Only subscribe to ready and failed if waitForInitialization timed out 89 | // because we want the introduction of init timeout to be as minimal and backwards 90 | // compatible as possible. 91 | if (error?.name.toLowerCase().includes('timeout')) { 92 | ldClient.on('failed', onFailed); 93 | ldClient.on('ready', onReady); 94 | } 95 | 96 | return function cleanup() { 97 | ldClient.off('change', onChange); 98 | ldClient.off('failed', onFailed); 99 | ldClient.off('ready', onReady); 100 | }; 101 | }, []); 102 | 103 | // unproxiedFlags is for internal use only. Exclude it from context. 104 | const { unproxiedFlags: _, ...rest } = ldData; 105 | 106 | const { reactContext } = reactOptions; 107 | 108 | return {children}; 109 | }; 110 | 111 | return LDProvider; 112 | } 113 | -------------------------------------------------------------------------------- /src/context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | import { ReactSdkContext } from './types'; 3 | 4 | /** 5 | * `reactSdkContextFactory` is a function useful for creating a React context for use with 6 | * all the providers and consumers in this library. 7 | * 8 | * @return a React Context 9 | */ 10 | const reactSdkContextFactory = () => createContext({ flags: {}, flagKeyMap: {}, ldClient: undefined }); 11 | /** 12 | * @ignore 13 | */ 14 | const context = reactSdkContextFactory(); 15 | const { 16 | /** 17 | * @ignore 18 | */ 19 | Provider, 20 | /** 21 | * @ignore 22 | */ 23 | Consumer, 24 | } = context; 25 | 26 | export { Provider, Consumer, ReactSdkContext, reactSdkContextFactory }; 27 | export default context; 28 | -------------------------------------------------------------------------------- /src/getFlagsProxy.test.ts: -------------------------------------------------------------------------------- 1 | import { LDClient, LDFlagSet } from 'launchdarkly-js-client-sdk'; 2 | import getFlagsProxy from './getFlagsProxy'; 3 | import { defaultReactOptions } from './types'; 4 | 5 | const rawFlags: LDFlagSet = { 6 | 'foo-bar': 'foobar', 7 | 'baz-qux': 'bazqux', 8 | }; 9 | 10 | const camelizedFlags: LDFlagSet = { 11 | fooBar: 'foobar', 12 | bazQux: 'bazqux', 13 | }; 14 | 15 | // cast as unknown first to be able to partially mock ldClient 16 | const ldClient = { variation: jest.fn((flagKey) => rawFlags[flagKey] as string) } as unknown as LDClient; 17 | 18 | beforeEach(jest.clearAllMocks); 19 | 20 | test('native Object functions should be ignored', () => { 21 | const { flags } = getFlagsProxy(ldClient, rawFlags); 22 | Object.prototype.hasOwnProperty.call(flags, 'fooBar'); 23 | Object.prototype.propertyIsEnumerable.call(flags, 'bazQux'); 24 | expect(ldClient.variation).not.toHaveBeenCalled(); 25 | }); 26 | 27 | test('camel cases keys', () => { 28 | const { flags } = getFlagsProxy(ldClient, rawFlags); 29 | expect(flags).toEqual(camelizedFlags); 30 | }); 31 | 32 | test('does not camel cases keys', () => { 33 | const { flags } = getFlagsProxy(ldClient, rawFlags, { ...defaultReactOptions, useCamelCaseFlagKeys: false }); 34 | expect(flags).toEqual(rawFlags); 35 | }); 36 | 37 | test('proxy calls ldClient.variation on flag read when camelCase true', () => { 38 | const { flags } = getFlagsProxy(ldClient, rawFlags); 39 | expect(flags.fooBar).toBe('foobar'); 40 | expect(ldClient.variation).toHaveBeenCalledWith('foo-bar', 'foobar'); 41 | }); 42 | 43 | test('proxy calls ldClient.variation on flag read when camelCase false', () => { 44 | const { flags } = getFlagsProxy(ldClient, rawFlags, { ...defaultReactOptions, useCamelCaseFlagKeys: false }); 45 | expect(flags.fooBar).toBeUndefined(); 46 | expect(flags['foo-bar']).toEqual('foobar'); 47 | expect(ldClient.variation).toHaveBeenCalledWith('foo-bar', 'foobar'); 48 | }); 49 | 50 | test('returns flag key map', () => { 51 | const { flagKeyMap } = getFlagsProxy(ldClient, rawFlags); 52 | expect(flagKeyMap).toEqual({ fooBar: 'foo-bar', bazQux: 'baz-qux' }); 53 | }); 54 | 55 | test('filters to target flags', () => { 56 | const { flags } = getFlagsProxy(ldClient, rawFlags, defaultReactOptions, { 'foo-bar': 'mr-toot' }); 57 | expect(flags).toEqual({ fooBar: 'foobar' }); 58 | }); 59 | 60 | test('does not use proxy if sendEventsOnFlagRead is false', () => { 61 | const { flags } = getFlagsProxy(ldClient, rawFlags, { ...defaultReactOptions, sendEventsOnFlagRead: false }); 62 | expect(flags.fooBar).toBe('foobar'); 63 | expect(ldClient.variation).not.toHaveBeenCalled(); 64 | }); 65 | -------------------------------------------------------------------------------- /src/getFlagsProxy.ts: -------------------------------------------------------------------------------- 1 | import { LDFlagSet, LDClient } from 'launchdarkly-js-client-sdk'; 2 | import camelCase from 'lodash.camelcase'; 3 | import { defaultReactOptions, LDFlagKeyMap, LDReactOptions } from './types'; 4 | 5 | export default function getFlagsProxy( 6 | ldClient: LDClient, 7 | rawFlags: LDFlagSet, 8 | reactOptions: LDReactOptions = defaultReactOptions, 9 | targetFlags?: LDFlagSet, 10 | ): { flags: LDFlagSet; flagKeyMap: LDFlagKeyMap } { 11 | const filteredFlags = filterFlags(rawFlags, targetFlags); 12 | const { useCamelCaseFlagKeys = true } = reactOptions; 13 | const [flags, flagKeyMap = {}] = useCamelCaseFlagKeys ? getCamelizedKeysAndFlagMap(filteredFlags) : [filteredFlags]; 14 | 15 | return { 16 | flags: reactOptions.sendEventsOnFlagRead ? toFlagsProxy(ldClient, flags, flagKeyMap, useCamelCaseFlagKeys) : flags, 17 | flagKeyMap, 18 | }; 19 | } 20 | 21 | function filterFlags(flags: LDFlagSet, targetFlags?: LDFlagSet): LDFlagSet { 22 | if (targetFlags === undefined) { 23 | return flags; 24 | } 25 | 26 | return Object.keys(targetFlags).reduce((acc, key) => { 27 | if (hasFlag(flags, key)) { 28 | acc[key] = flags[key]; 29 | } 30 | 31 | return acc; 32 | }, {}); 33 | } 34 | 35 | function getCamelizedKeysAndFlagMap(rawFlags: LDFlagSet) { 36 | const flags: LDFlagSet = {}; 37 | const flagKeyMap: LDFlagKeyMap = {}; 38 | for (const rawFlag in rawFlags) { 39 | // Exclude system keys 40 | if (rawFlag.indexOf('$') === 0) { 41 | continue; 42 | } 43 | const camelKey = camelCase(rawFlag); 44 | flags[camelKey] = rawFlags[rawFlag]; 45 | flagKeyMap[camelKey] = rawFlag; 46 | } 47 | 48 | return [flags, flagKeyMap]; 49 | } 50 | 51 | function hasFlag(flags: LDFlagSet, flagKey: string) { 52 | return Object.prototype.hasOwnProperty.call(flags, flagKey); 53 | } 54 | 55 | function toFlagsProxy( 56 | ldClient: LDClient, 57 | flags: LDFlagSet, 58 | flagKeyMap: LDFlagKeyMap, 59 | useCamelCaseFlagKeys: boolean, 60 | ): LDFlagSet { 61 | return new Proxy(flags, { 62 | // trap for reading a flag value using `LDClient#variation` to trigger an evaluation event 63 | get(target, prop, receiver) { 64 | const currentValue = Reflect.get(target, prop, receiver); 65 | 66 | // check if flag key exists as camelCase or original case 67 | const validFlagKey = 68 | (useCamelCaseFlagKeys && hasFlag(flagKeyMap, prop as string)) || hasFlag(target, prop as string); 69 | 70 | // only process flag keys and ignore symbols and native Object functions 71 | if (typeof prop === 'symbol' || !validFlagKey) { 72 | return currentValue; 73 | } 74 | 75 | if (currentValue === undefined) { 76 | return; 77 | } 78 | 79 | const pristineFlagKey = useCamelCaseFlagKeys ? flagKeyMap[prop] : prop; 80 | 81 | return ldClient.variation(pristineFlagKey, currentValue); 82 | }, 83 | }); 84 | } 85 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import LDProvider from './provider'; 2 | import withLDProvider from './withLDProvider'; 3 | import asyncWithLDProvider from './asyncWithLDProvider'; 4 | import withLDConsumer from './withLDConsumer'; 5 | import useFlags from './useFlags'; 6 | import useLDClient from './useLDClient'; 7 | import useLDClientError from './useLDClientError'; 8 | import { camelCaseKeys } from './utils'; 9 | import { reactSdkContextFactory } from './context'; 10 | 11 | export * from './types'; 12 | 13 | export { 14 | LDProvider, 15 | asyncWithLDProvider, 16 | camelCaseKeys, 17 | reactSdkContextFactory, 18 | useFlags, 19 | useLDClient, 20 | useLDClientError, 21 | withLDProvider, 22 | withLDConsumer, 23 | }; 24 | -------------------------------------------------------------------------------- /src/provider.test.tsx: -------------------------------------------------------------------------------- 1 | import ProviderState from './providerState'; 2 | 3 | jest.mock('launchdarkly-js-client-sdk', () => { 4 | const actual = jest.requireActual('launchdarkly-js-client-sdk'); 5 | 6 | return { 7 | ...actual, 8 | initialize: jest.fn(), 9 | }; 10 | }); 11 | jest.mock('./utils', () => { 12 | const originalModule = jest.requireActual('./utils'); 13 | 14 | return { 15 | ...originalModule, 16 | fetchFlags: jest.fn(), 17 | }; 18 | }); 19 | jest.mock('./context', () => { 20 | const originalModule = jest.requireActual('./context'); 21 | 22 | return { 23 | ...originalModule, 24 | Provider: 'Provider', 25 | }; 26 | }); 27 | 28 | import React, { Component } from 'react'; 29 | import { render } from '@testing-library/react'; 30 | import { create } from 'react-test-renderer'; 31 | import { initialize, LDClient, LDContext, LDFlagChangeset, LDOptions } from 'launchdarkly-js-client-sdk'; 32 | import { LDReactOptions, EnhancedComponent, ProviderConfig } from './types'; 33 | import { ReactSdkContext as HocState, reactSdkContextFactory } from './context'; 34 | import LDProvider from './provider'; 35 | import { fetchFlags } from './utils'; 36 | import wrapperOptions from './wrapperOptions'; 37 | 38 | const clientSideID = 'test-client-side-id'; 39 | const App = () =>
My App
; 40 | const mockInitialize = initialize as jest.Mock; 41 | const mockFetchFlags = fetchFlags as jest.Mock; 42 | const rawFlags = { 'test-flag': true, 'another-test-flag': true }; 43 | const mockLDClient = { 44 | on: jest.fn((_e: string, cb: () => void) => { 45 | cb(); 46 | }), 47 | off: jest.fn(), 48 | allFlags: jest.fn().mockReturnValue({}), 49 | variation: jest.fn(), 50 | waitForInitialization: jest.fn(), 51 | }; 52 | 53 | describe('LDProvider', () => { 54 | let context: LDContext; 55 | let options: LDOptions; 56 | let previousState: ProviderState; 57 | let timeoutError: Error; 58 | 59 | beforeEach(() => { 60 | mockInitialize.mockImplementation(() => mockLDClient); 61 | mockFetchFlags.mockImplementation(() => rawFlags); 62 | 63 | mockLDClient.variation.mockImplementation((_, v) => v); 64 | options = { ...wrapperOptions }; 65 | previousState = { 66 | unproxiedFlags: {}, 67 | flags: {}, 68 | flagKeyMap: {}, 69 | }; 70 | context = { key: 'yus', kind: 'user', name: 'yus ng' }; 71 | timeoutError = new Error('waitForInitialization timed out'); 72 | timeoutError.name = 'TimeoutError'; 73 | }); 74 | 75 | afterEach(() => { 76 | jest.resetAllMocks(); 77 | }); 78 | 79 | test('render app', () => { 80 | const props: ProviderConfig = { clientSideID }; 81 | const LaunchDarklyApp = ( 82 | 83 | 84 | 85 | ); 86 | const component = create(LaunchDarklyApp); 87 | expect(component).toMatchSnapshot(); 88 | }); 89 | 90 | test('ld client is initialised correctly with deprecated user object', async () => { 91 | const user: LDContext = { key: 'yus' }; 92 | const props: ProviderConfig = { clientSideID, user }; 93 | const LaunchDarklyApp = ( 94 | 95 | 96 | 97 | ); 98 | const instance = create(LaunchDarklyApp).root.findByType(LDProvider).instance as EnhancedComponent; 99 | 100 | await instance.componentDidMount(); 101 | expect(mockInitialize).toHaveBeenCalledWith(clientSideID, user, options); 102 | }); 103 | 104 | test('use context ignore user at init if both are present', async () => { 105 | const user: LDContext = { key: 'deprecatedUser' }; 106 | 107 | // this should not happen in real usage. Only one of context or user should be specified. 108 | // if both are specified, context will be used and user ignored. 109 | const props: ProviderConfig = { clientSideID, context, user }; 110 | const LaunchDarklyApp = ( 111 | 112 | 113 | 114 | ); 115 | const instance = create(LaunchDarklyApp).root.findByType(LDProvider).instance as EnhancedComponent; 116 | 117 | await instance.componentDidMount(); 118 | expect(mockInitialize).toHaveBeenCalledWith(clientSideID, context, options); 119 | }); 120 | 121 | test('ld client is initialised correctly', async () => { 122 | options = { ...options, bootstrap: {} }; 123 | const props: ProviderConfig = { clientSideID, context, options }; 124 | const LaunchDarklyApp = ( 125 | 126 | 127 | 128 | ); 129 | const instance = create(LaunchDarklyApp).root.findByType(LDProvider).instance as EnhancedComponent; 130 | 131 | await instance.componentDidMount(); 132 | expect(mockInitialize).toHaveBeenCalledWith(clientSideID, context, options); 133 | }); 134 | 135 | test('ld client is used if passed in', async () => { 136 | options = { ...options, bootstrap: {} }; 137 | const ldClient = mockLDClient as unknown as LDClient; 138 | mockInitialize.mockClear(); 139 | const props: ProviderConfig = { clientSideID, ldClient }; 140 | const LaunchDarklyApp = ( 141 | 142 | 143 | 144 | ); 145 | const instance = create(LaunchDarklyApp).root.findByType(LDProvider).instance as EnhancedComponent; 146 | 147 | await instance.componentDidMount(); 148 | expect(mockInitialize).not.toHaveBeenCalled(); 149 | }); 150 | 151 | test('ld client is used if passed in as promise', async () => { 152 | const context2: LDContext = { key: 'launch', kind: 'user', name: 'darkly' }; 153 | options = { ...options, bootstrap: {} }; 154 | const ldClient = new Promise((resolve) => { 155 | resolve(mockLDClient as unknown as LDClient); 156 | 157 | return; 158 | }); 159 | const props: ProviderConfig = { clientSideID, ldClient, context: context2 }; 160 | const LaunchDarklyApp = ( 161 | 162 | 163 | 164 | ); 165 | const instance = create(LaunchDarklyApp).root.findByType(LDProvider).instance as EnhancedComponent; 166 | 167 | await instance.componentDidMount(); 168 | expect(mockInitialize).not.toHaveBeenCalled(); 169 | }); 170 | 171 | test('ld client is created if passed in promise resolves as undefined', async () => { 172 | options = { ...options, bootstrap: {} }; 173 | const ldClient = new Promise((resolve) => { 174 | resolve(undefined); 175 | 176 | return; 177 | }); 178 | const props: ProviderConfig = { clientSideID, ldClient, context, options }; 179 | const LaunchDarklyApp = ( 180 | 181 | 182 | 183 | ); 184 | const instance = create(LaunchDarklyApp).root.findByType(LDProvider).instance as EnhancedComponent; 185 | 186 | await instance.componentDidMount(); 187 | expect(mockInitialize).toHaveBeenCalledWith(clientSideID, context, options); 188 | }); 189 | 190 | test('ldClient bootstraps with empty flags', () => { 191 | options = { 192 | bootstrap: {}, 193 | }; 194 | const props: ProviderConfig = { clientSideID, context, options }; 195 | const LaunchDarklyApp = ( 196 | 197 | 198 | 199 | ); 200 | const component = create(LaunchDarklyApp).toTree()?.instance as Component; 201 | const initialState = component.state as HocState; 202 | 203 | expect(initialState.flags).toEqual({}); 204 | }); 205 | 206 | test('ld client keeps bootstrapped flags, even when it failed to initialize', async () => { 207 | mockLDClient.waitForInitialization.mockRejectedValue(new Error('TestError')); 208 | options = { 209 | ...options, 210 | bootstrap: { 211 | 'test-flag': true, 212 | }, 213 | }; 214 | const props: ProviderConfig = { clientSideID, context, options }; 215 | 216 | const LaunchDarklyApp = ( 217 | 218 | 219 | 220 | ); 221 | const instance = create(LaunchDarklyApp).root.findByType(LDProvider).instance as EnhancedComponent; 222 | const mockSetState = jest.spyOn(instance, 'setState'); 223 | 224 | await instance?.componentDidMount(); 225 | const setStateFunction = mockSetState.mock?.lastCall?.[0] as (p: ProviderState) => ProviderState; 226 | 227 | expect(mockInitialize).toHaveBeenCalledWith(clientSideID, context, options); 228 | expect(setStateFunction(previousState)).toEqual({ 229 | error: new Error('TestError'), 230 | flags: { testFlag: true }, 231 | unproxiedFlags: { 'test-flag': true }, 232 | flagKeyMap: { testFlag: 'test-flag' }, 233 | ldClient: mockLDClient, 234 | }); 235 | }); 236 | 237 | test('waitForInitialization timed out', async () => { 238 | mockLDClient.waitForInitialization.mockRejectedValue(timeoutError); 239 | const props: ProviderConfig = { clientSideID, context, options }; 240 | const LaunchDarklyApp = ( 241 | 242 | 243 | 244 | ); 245 | const instance = create(LaunchDarklyApp).root.findByType(LDProvider).instance as EnhancedComponent; 246 | const mockSetState = jest.spyOn(instance, 'setState'); 247 | 248 | await instance?.componentDidMount(); 249 | const setStateFunction = mockSetState.mock?.lastCall?.[0] as (p: ProviderState) => ProviderState; 250 | 251 | expect(mockLDClient.on).toHaveBeenCalledWith('failed', expect.any(Function)); 252 | expect(mockLDClient.on).toHaveBeenCalledWith('ready', expect.any(Function)); 253 | expect(setStateFunction(previousState)).toMatchObject({ 254 | error: timeoutError, 255 | }); 256 | }); 257 | 258 | test('waitForInitialization succeeds', async () => { 259 | const props: ProviderConfig = { clientSideID, context, options }; 260 | const LaunchDarklyApp = ( 261 | 262 | 263 | 264 | ); 265 | const instance = create(LaunchDarklyApp).root.findByType(LDProvider).instance as EnhancedComponent; 266 | const mockSetState = jest.spyOn(instance, 'setState'); 267 | 268 | await instance?.componentDidMount(); 269 | const setStateFunction = mockSetState.mock?.lastCall?.[0] as (p: ProviderState) => ProviderState; 270 | 271 | expect(mockLDClient.on).not.toHaveBeenCalledWith('failed', expect.any(Function)); 272 | expect(mockLDClient.on).not.toHaveBeenCalledWith('ready', expect.any(Function)); 273 | expect(setStateFunction(previousState)).toMatchObject({ 274 | error: undefined, 275 | }); 276 | }); 277 | 278 | test('ld client is bootstrapped correctly and transforms keys to camel case', () => { 279 | options = { 280 | bootstrap: { 281 | 'test-flag': true, 282 | 'another-test-flag': false, 283 | $flagsState: { 284 | 'test-flag': { version: 125, variation: 0, trackEvents: true }, 285 | 'another-test-flag': { version: 18, variation: 1 }, 286 | }, 287 | $valid: true, 288 | }, 289 | }; 290 | const props: ProviderConfig = { clientSideID, context, options }; 291 | const LaunchDarklyApp = ( 292 | 293 | 294 | 295 | ); 296 | const component = create(LaunchDarklyApp).toTree()?.instance as Component; 297 | const initialState = component.state as HocState; 298 | 299 | expect(mockInitialize).not.toHaveBeenCalled(); 300 | expect(initialState.flags).toEqual({ testFlag: true, anotherTestFlag: false }); 301 | }); 302 | 303 | test('ld client should not transform keys to camel case if option is disabled', () => { 304 | options = { 305 | bootstrap: { 306 | 'test-flag': true, 307 | 'another-test-flag': false, 308 | }, 309 | }; 310 | const reactOptions: LDReactOptions = { 311 | useCamelCaseFlagKeys: false, 312 | }; 313 | const props: ProviderConfig = { clientSideID, context, options, reactOptions }; 314 | const LaunchDarklyApp = ( 315 | 316 | 317 | 318 | ); 319 | const component = create(LaunchDarklyApp).toTree()?.instance as Component; 320 | const initialState = component.state as HocState; 321 | 322 | expect(mockInitialize).not.toHaveBeenCalled(); 323 | expect(initialState.flags).toEqual({ 'test-flag': true, 'another-test-flag': false }); 324 | }); 325 | 326 | test('ld client should transform keys to camel case if transform option is absent', () => { 327 | options = { 328 | bootstrap: { 329 | 'test-flag': true, 330 | 'another-test-flag': false, 331 | }, 332 | }; 333 | const reactOptions: LDReactOptions = {}; 334 | const props: ProviderConfig = { clientSideID, context, options, reactOptions }; 335 | const LaunchDarklyApp = ( 336 | 337 | 338 | 339 | ); 340 | const component = create(LaunchDarklyApp).toTree()?.instance as Component; 341 | const initialState = component.state as HocState; 342 | 343 | expect(mockInitialize).not.toHaveBeenCalled(); 344 | expect(initialState.flags).toEqual({ testFlag: true, anotherTestFlag: false }); 345 | }); 346 | 347 | test('ld client should transform keys to camel case if react options object is absent', () => { 348 | options = { 349 | bootstrap: { 350 | 'test-flag': true, 351 | 'another-test-flag': false, 352 | }, 353 | }; 354 | const props: ProviderConfig = { clientSideID, context, options }; 355 | const LaunchDarklyApp = ( 356 | 357 | 358 | 359 | ); 360 | const component = create(LaunchDarklyApp).toTree()?.instance as Component; 361 | const initialState = component.state as HocState; 362 | 363 | expect(mockInitialize).not.toHaveBeenCalled(); 364 | expect(initialState.flags).toEqual({ testFlag: true, anotherTestFlag: false }); 365 | }); 366 | 367 | test('state.flags should be initialised to empty when bootstrapping from localStorage', () => { 368 | options = { 369 | bootstrap: 'localStorage', 370 | }; 371 | const props: ProviderConfig = { clientSideID, context, options }; 372 | const LaunchDarklyApp = ( 373 | 374 | 375 | 376 | ); 377 | const component = create(LaunchDarklyApp).toTree()?.instance as Component; 378 | const initialState = component.state as HocState; 379 | 380 | expect(mockInitialize).not.toHaveBeenCalled(); 381 | expect(initialState.flags).toEqual({}); 382 | }); 383 | 384 | test('ld client is initialised correctly with target flags', async () => { 385 | mockFetchFlags.mockImplementation(() => ({ 'dev-test-flag': false, 'launch-doggly': false })); 386 | 387 | options = { ...options, bootstrap: {} }; 388 | const flags = { 'dev-test-flag': false, 'launch-doggly': false }; 389 | const props: ProviderConfig = { clientSideID, context, options, flags }; 390 | const LaunchDarklyApp = ( 391 | 392 | 393 | 394 | ); 395 | const instance = create(LaunchDarklyApp).root.findByType(LDProvider).instance as EnhancedComponent; 396 | const mockSetState = jest.spyOn(instance, 'setState'); 397 | 398 | await instance.componentDidMount(); 399 | const setStateFunction = mockSetState.mock?.lastCall?.[0] as (p: ProviderState) => ProviderState; 400 | 401 | expect(mockInitialize).toHaveBeenCalledWith(clientSideID, context, options); 402 | expect(setStateFunction(previousState)).toEqual({ 403 | flags: { devTestFlag: false, launchDoggly: false }, 404 | unproxiedFlags: { 'dev-test-flag': false, 'launch-doggly': false }, 405 | flagKeyMap: { devTestFlag: 'dev-test-flag', launchDoggly: 'launch-doggly' }, 406 | ldClient: mockLDClient, 407 | }); 408 | }); 409 | 410 | test('state is saved on mount', async () => { 411 | const props: ProviderConfig = { clientSideID }; 412 | const LaunchDarklyApp = ( 413 | 414 | 415 | 416 | ); 417 | const instance = create(LaunchDarklyApp).root.findByType(LDProvider).instance as EnhancedComponent; 418 | const mockSetState = jest.spyOn(instance, 'setState'); 419 | 420 | await instance.componentDidMount(); 421 | const setStateFunction = mockSetState.mock?.lastCall?.[0] as (p: ProviderState) => ProviderState; 422 | 423 | expect(setStateFunction(previousState)).toEqual({ 424 | flags: { testFlag: true, anotherTestFlag: true }, 425 | unproxiedFlags: { 'test-flag': true, 'another-test-flag': true }, 426 | flagKeyMap: { testFlag: 'test-flag', anotherTestFlag: 'another-test-flag' }, 427 | ldClient: mockLDClient, 428 | }); 429 | }); 430 | 431 | test('subscribeToChanges is called on mount', async () => { 432 | const props: ProviderConfig = { clientSideID }; 433 | const LaunchDarklyApp = ( 434 | 435 | 436 | 437 | ); 438 | const instance = create(LaunchDarklyApp).root.findByType(LDProvider).instance as EnhancedComponent; 439 | instance.subscribeToChanges = jest.fn(); 440 | 441 | await instance.componentDidMount(); 442 | expect(instance.subscribeToChanges).toHaveBeenCalled(); 443 | }); 444 | 445 | test('subscribe to changes with camelCase', async () => { 446 | mockLDClient.on.mockImplementation((_e: string, cb: (c: LDFlagChangeset) => void) => { 447 | cb({ 'test-flag': { current: false, previous: true } }); 448 | }); 449 | const props: ProviderConfig = { clientSideID }; 450 | const LaunchDarklyApp = ( 451 | 452 | 453 | 454 | ); 455 | const instance = create(LaunchDarklyApp).root.findByType(LDProvider).instance as EnhancedComponent; 456 | const mockSetState = jest.spyOn(instance, 'setState'); 457 | 458 | await instance.componentDidMount(); 459 | const setStateFunction = mockSetState.mock?.lastCall?.[0] as (p: ProviderState) => ProviderState; 460 | 461 | expect(mockLDClient.on).toHaveBeenCalledWith('change', expect.any(Function)); 462 | expect(setStateFunction(previousState)).toEqual({ 463 | flags: { anotherTestFlag: true, testFlag: false }, 464 | unproxiedFlags: { 'another-test-flag': true, 'test-flag': false }, 465 | flagKeyMap: { anotherTestFlag: 'another-test-flag', testFlag: 'test-flag' }, 466 | }); 467 | }); 468 | 469 | test('subscribe to changes with kebab-case', async () => { 470 | mockLDClient.on.mockImplementation((_e: string, cb: (c: LDFlagChangeset) => void) => { 471 | cb({ 'another-test-flag': { current: false, previous: true }, 'test-flag': { current: false, previous: true } }); 472 | }); 473 | const props: ProviderConfig = { clientSideID, reactOptions: { useCamelCaseFlagKeys: false } }; 474 | const LaunchDarklyApp = ( 475 | 476 | 477 | 478 | ); 479 | const instance = create(LaunchDarklyApp).root.findByType(LDProvider).instance as EnhancedComponent; 480 | const mockSetState = jest.spyOn(instance, 'setState'); 481 | 482 | await instance.componentDidMount(); 483 | const setStateFunction = mockSetState.mock?.lastCall?.[0] as (p: ProviderState) => ProviderState; 484 | 485 | expect(mockLDClient.on).toHaveBeenCalledWith('change', expect.any(Function)); 486 | expect(setStateFunction(previousState)).toEqual({ 487 | flagKeyMap: {}, 488 | unproxiedFlags: { 'another-test-flag': false, 'test-flag': false }, 489 | flags: { 'another-test-flag': false, 'test-flag': false }, 490 | }); 491 | }); 492 | 493 | test(`if props.deferInitialization is true, ld client will only initialize once props.user is defined`, async () => { 494 | options = { ...options, bootstrap: {} }; 495 | const props: ProviderConfig = { clientSideID, deferInitialization: true, options }; 496 | const LaunchDarklyApp = ( 497 | 498 | 499 | 500 | ); 501 | const renderer = create(LaunchDarklyApp); 502 | const instance = renderer.root.findByType(LDProvider).instance as EnhancedComponent; 503 | 504 | await instance.componentDidMount(); 505 | 506 | expect(mockInitialize).toHaveBeenCalledTimes(0); 507 | 508 | const newProps = { ...props, context }; 509 | const UpdatedLaunchDarklyApp = ( 510 | 511 | 512 | 513 | ); 514 | renderer.update(UpdatedLaunchDarklyApp); 515 | if (instance.componentDidUpdate) { 516 | await instance.componentDidUpdate(props); 517 | } 518 | 519 | expect(mockInitialize).toHaveBeenCalledWith(clientSideID, context, options); 520 | }); 521 | 522 | test('only updates to subscribed flags are pushed to the Provider', async () => { 523 | mockFetchFlags.mockImplementation(() => ({ 'test-flag': 2 })); 524 | mockLDClient.on.mockImplementation((_e: string, cb: (c: LDFlagChangeset) => void) => { 525 | cb({ 'test-flag': { current: 3, previous: 2 }, 'another-test-flag': { current: false, previous: true } }); 526 | }); 527 | options = {}; 528 | 529 | const subscribedFlags = { 'test-flag': 1 }; 530 | const props: ProviderConfig = { clientSideID, context, options, flags: subscribedFlags }; 531 | const LaunchDarklyApp = ( 532 | 533 | 534 | 535 | ); 536 | const instance = create(LaunchDarklyApp).root.findByType(LDProvider).instance as EnhancedComponent; 537 | const mockSetState = jest.spyOn(instance, 'setState'); 538 | 539 | await instance.componentDidMount(); 540 | const setStateFunction = mockSetState.mock?.lastCall?.[0] as (p: ProviderState) => ProviderState; 541 | 542 | expect(setStateFunction(previousState)).toEqual({ 543 | flags: { testFlag: 3 }, 544 | unproxiedFlags: { 'test-flag': 3 }, 545 | flagKeyMap: { testFlag: 'test-flag' }, 546 | }); 547 | }); 548 | 549 | test('custom context is provided to consumer', async () => { 550 | const CustomContext = reactSdkContextFactory(); 551 | const customLDClient = { 552 | on: jest.fn((_: string, cb: () => void) => { 553 | cb(); 554 | }), 555 | off: jest.fn(), 556 | allFlags: jest.fn().mockReturnValue({ 'context-test-flag': true }), 557 | variation: jest.fn((_: string, v) => v), 558 | waitForInitialization: jest.fn(), 559 | }; 560 | const props: ProviderConfig = { 561 | clientSideID, 562 | ldClient: customLDClient as unknown as LDClient, 563 | reactOptions: { 564 | reactContext: CustomContext, 565 | }, 566 | }; 567 | const originalUtilsModule = jest.requireActual('./utils'); 568 | mockFetchFlags.mockImplementation(originalUtilsModule.fetchFlags); 569 | 570 | const LaunchDarklyApp = ( 571 | 572 | 573 | {({ flags }) => { 574 | return ( 575 | 576 | flag is {flags.contextTestFlag === undefined ? 'undefined' : JSON.stringify(flags.contextTestFlag)} 577 | 578 | ); 579 | }} 580 | 581 | 582 | ); 583 | 584 | const { findByText } = render(LaunchDarklyApp); 585 | expect(await findByText('flag is true')).not.toBeNull(); 586 | }); 587 | }); 588 | -------------------------------------------------------------------------------- /src/provider.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropsWithChildren } from 'react'; 2 | import { initialize, LDClient, LDFlagChangeset, LDFlagSet } from 'launchdarkly-js-client-sdk'; 3 | import { EnhancedComponent, ProviderConfig, defaultReactOptions, LDReactOptions } from './types'; 4 | import { camelCaseKeys, fetchFlags, getContextOrUser, getFlattenedFlagsFromChangeset } from './utils'; 5 | import getFlagsProxy from './getFlagsProxy'; 6 | import wrapperOptions from './wrapperOptions'; 7 | import ProviderState from './providerState'; 8 | 9 | /** 10 | * The `LDProvider` is a component which accepts a config object which is used to 11 | * initialize `launchdarkly-js-client-sdk`. 12 | * 13 | * This Provider does three things: 14 | * - It initializes the ldClient instance by calling `launchdarkly-js-client-sdk` initialize on `componentDidMount` 15 | * - It saves all flags and the ldClient instance in the context API 16 | * - It subscribes to flag changes and propagate them through the context API 17 | * 18 | * Because the `launchdarkly-js-client-sdk` in only initialized on `componentDidMount`, your flags and the 19 | * ldClient are only available after your app has mounted. This can result in a flicker due to flag changes at 20 | * startup time. 21 | * 22 | * This component can be used as a standalone provider. However, be mindful to only include the component once 23 | * within your application. This provider is used inside the `withLDProviderHOC` and can be used instead to initialize 24 | * the `launchdarkly-js-client-sdk`. For async initialization, check out the `asyncWithLDProvider` function 25 | */ 26 | class LDProvider extends Component, ProviderState> implements EnhancedComponent { 27 | readonly state: Readonly; 28 | 29 | constructor(props: ProviderConfig) { 30 | super(props); 31 | 32 | const { options } = props; 33 | 34 | this.state = { 35 | flags: {}, 36 | unproxiedFlags: {}, 37 | flagKeyMap: {}, 38 | }; 39 | 40 | if (options) { 41 | const { bootstrap } = options; 42 | if (bootstrap && bootstrap !== 'localStorage') { 43 | const { useCamelCaseFlagKeys } = this.getReactOptions(); 44 | this.state = { 45 | flags: useCamelCaseFlagKeys ? camelCaseKeys(bootstrap) : bootstrap, 46 | unproxiedFlags: bootstrap, 47 | flagKeyMap: {}, 48 | }; 49 | } 50 | } 51 | } 52 | 53 | getReactOptions = () => ({ ...defaultReactOptions, ...this.props.reactOptions }); 54 | 55 | subscribeToChanges = (ldClient: LDClient) => { 56 | const { flags: targetFlags } = this.props; 57 | ldClient.on('change', (changes: LDFlagChangeset) => { 58 | const reactOptions = this.getReactOptions(); 59 | const updates = getFlattenedFlagsFromChangeset(changes, targetFlags); 60 | const unproxiedFlags = { 61 | ...this.state.unproxiedFlags, 62 | ...updates, 63 | }; 64 | if (Object.keys(updates).length > 0) { 65 | this.setState((prevState) => ({ 66 | ...prevState, 67 | unproxiedFlags, 68 | ...getFlagsProxy(ldClient, unproxiedFlags, reactOptions, targetFlags), 69 | })); 70 | } 71 | }); 72 | }; 73 | 74 | onFailed = (_ldClient: LDClient, e: Error) => { 75 | this.setState((prevState) => ({ ...prevState, error: e })); 76 | }; 77 | 78 | onReady = (ldClient: LDClient, reactOptions: LDReactOptions, targetFlags?: LDFlagSet) => { 79 | const unproxiedFlags = fetchFlags(ldClient, targetFlags); 80 | this.setState((prevState) => ({ 81 | ...prevState, 82 | unproxiedFlags, 83 | ...getFlagsProxy(ldClient, unproxiedFlags, reactOptions, targetFlags), 84 | })); 85 | }; 86 | 87 | prepareLDClient = async () => { 88 | const { clientSideID, flags: targetFlags, options } = this.props; 89 | let ldClient = await this.props.ldClient; 90 | const reactOptions = this.getReactOptions(); 91 | let unproxiedFlags = this.state.unproxiedFlags; 92 | let error: Error; 93 | 94 | if (ldClient) { 95 | unproxiedFlags = fetchFlags(ldClient, targetFlags); 96 | } else { 97 | const context = getContextOrUser(this.props) ?? { anonymous: true, kind: 'user' }; 98 | ldClient = initialize(clientSideID, context, { ...wrapperOptions, ...options }); 99 | 100 | try { 101 | await ldClient.waitForInitialization(this.props.timeout); 102 | unproxiedFlags = fetchFlags(ldClient, targetFlags); 103 | } catch (e) { 104 | error = e as Error; 105 | 106 | if (error?.name.toLowerCase().includes('timeout')) { 107 | ldClient.on('failed', this.onFailed); 108 | ldClient.on('ready', () => { 109 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 110 | this.onReady(ldClient!, reactOptions, targetFlags); 111 | }); 112 | } 113 | } 114 | } 115 | this.setState((prevState) => ({ 116 | ...prevState, 117 | unproxiedFlags, 118 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 119 | ...getFlagsProxy(ldClient!, unproxiedFlags, reactOptions, targetFlags), 120 | ldClient, 121 | error, 122 | })); 123 | this.subscribeToChanges(ldClient); 124 | }; 125 | 126 | async componentDidMount() { 127 | const { deferInitialization } = this.props; 128 | if (deferInitialization && !getContextOrUser(this.props)) { 129 | return; 130 | } 131 | 132 | await this.prepareLDClient(); 133 | } 134 | 135 | async componentDidUpdate(prevProps: ProviderConfig) { 136 | const { deferInitialization } = this.props; 137 | const contextJustLoaded = !getContextOrUser(prevProps) && getContextOrUser(this.props); 138 | if (deferInitialization && contextJustLoaded) { 139 | await this.prepareLDClient(); 140 | } 141 | } 142 | 143 | render() { 144 | const { flags, flagKeyMap, ldClient, error } = this.state; 145 | 146 | const { reactContext } = this.getReactOptions(); 147 | 148 | return ( 149 | 150 | {this.props.children} 151 | 152 | ); 153 | } 154 | } 155 | 156 | export default LDProvider; 157 | -------------------------------------------------------------------------------- /src/providerState.ts: -------------------------------------------------------------------------------- 1 | import { LDClient, LDFlagSet } from 'launchdarkly-js-client-sdk'; 2 | import { LDFlagKeyMap } from './types'; 3 | 4 | interface ProviderState { 5 | error?: Error; 6 | flagKeyMap: LDFlagKeyMap; 7 | flags: LDFlagSet; 8 | ldClient?: LDClient; 9 | unproxiedFlags: LDFlagSet; 10 | } 11 | 12 | export default ProviderState; 13 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { LDClient, LDContext, LDFlagSet, LDOptions } from 'launchdarkly-js-client-sdk'; 2 | import * as React from 'react'; 3 | import defaultReactContext from './context'; 4 | 5 | /** 6 | * Initialization options for the LaunchDarkly React SDK. These are in addition to the options exposed 7 | * by [[LDOptions]] which are common to both the JavaScript and React SDKs. 8 | */ 9 | export interface LDReactOptions { 10 | /** 11 | * Whether the React SDK should transform flag keys into camel-cased format. 12 | * Using camel-cased flag keys allow for easier use as prop values, however, 13 | * these keys won't directly match the flag keys as known to LaunchDarkly. 14 | * Consequently, flag key collisions may be possible and the Code References feature 15 | * will not function properly. 16 | * 17 | * This is true by default, meaning that keys will automatically be converted to camel-case. 18 | * 19 | * For more information, see the React SDK Reference Guide on 20 | * [flag keys](https://docs.launchdarkly.com/sdk/client-side/react/react-web#flag-keys). 21 | * 22 | * @see https://docs.launchdarkly.com/sdk/client-side/react/react-web#flag-keys 23 | */ 24 | useCamelCaseFlagKeys?: boolean; 25 | 26 | /** 27 | * Whether to send flag evaluation events when a flag is read from the `flags` object 28 | * returned by the `useFlags` hook. This is true by default, meaning flag evaluation 29 | * events will be sent by default. 30 | */ 31 | sendEventsOnFlagRead?: boolean; 32 | 33 | /** 34 | * The react context to use within the provider objects. 35 | */ 36 | reactContext?: React.Context; 37 | } 38 | 39 | /** 40 | * Contains default values for the `reactOptions` object. 41 | */ 42 | export const defaultReactOptions = { 43 | useCamelCaseFlagKeys: true, 44 | sendEventsOnFlagRead: true, 45 | reactContext: defaultReactContext, 46 | }; 47 | 48 | /** 49 | * Configuration object used to initialise LaunchDarkly's JS client. 50 | */ 51 | export interface ProviderConfig { 52 | /** 53 | * Your project and environment specific client side ID. You can find 54 | * this in your LaunchDarkly portal under Account settings. This is 55 | * the only mandatory property required to use the React SDK. 56 | */ 57 | clientSideID: string; 58 | 59 | /** 60 | * A LaunchDarkly context object. If unspecified, an anonymous context 61 | * with kind: 'user' will be created and used. 62 | */ 63 | context?: LDContext; 64 | 65 | /** 66 | * @deprecated The `user` property will be removed in a future version, 67 | * please update your code to use context instead. 68 | */ 69 | user?: LDContext; 70 | 71 | /** 72 | * If set to true, the ldClient will not be initialized until the context prop has been defined. 73 | */ 74 | deferInitialization?: boolean; 75 | 76 | /** 77 | * LaunchDarkly initialization options. These options are common between LaunchDarkly's JavaScript and React SDKs. 78 | * 79 | * @see https://docs.launchdarkly.com/sdk/features/config#javascript 80 | */ 81 | options?: LDOptions; 82 | 83 | /** 84 | * Additional initialization options specific to the React SDK. 85 | * 86 | * @see options 87 | */ 88 | reactOptions?: LDReactOptions; 89 | 90 | /** 91 | * If specified, `launchdarkly-react-client-sdk` will only listen for changes to these flags. 92 | * Otherwise, all flags will be requested and listened to. 93 | * Flag keys must be in their original form as known to LaunchDarkly rather than in their camel-cased form. 94 | */ 95 | flags?: LDFlagSet; 96 | 97 | /** 98 | * Optionally, the ldClient can be initialized outside of the provider 99 | * and passed in, instead of being initialized by the provider. 100 | * 101 | * Note: it should only be passed in when it has emitted the 'ready' 102 | * event when using withLDProvider, to ensure that the flags are properly set. 103 | * If using with asyncWithLDProvider, then it will wait internally, so 104 | * it is not required that the client have emitted the 'ready' event. 105 | */ 106 | ldClient?: LDClient | Promise; 107 | 108 | /** 109 | * The amount of time, in seconds, to wait for initialization before rejecting the promise. 110 | * Using a large timeout is not recommended. If you use a large timeout and await it, then 111 | * any network delays will cause your application to wait a long time before continuing 112 | * execution. This gets passed to the underlying Javascript SDK `waitForInitialization` 113 | * function. 114 | */ 115 | timeout?: number; 116 | } 117 | 118 | /** 119 | * Configuration object used to initialize LaunchDarkly's JS client asynchronously. 120 | */ 121 | export type AsyncProviderConfig = Omit & { 122 | /** 123 | * @deprecated - `asyncWithLDProvider` does not support the `deferInitialization` config option because 124 | * `asyncWithLDProvider` needs to be initialized at the app entry point prior to render to ensure flags and the 125 | * ldClient are ready at the beginning of the app. 126 | */ 127 | deferInitialization?: boolean; 128 | }; 129 | 130 | /** 131 | * The return type of withLDProvider HOC. Exported for testing purposes only. 132 | * 133 | * @ignore 134 | */ 135 | export interface EnhancedComponent extends React.Component { 136 | subscribeToChanges(ldClient: LDClient): void; 137 | componentDidMount(): Promise; 138 | componentDidUpdate(prevProps: ProviderConfig): Promise; 139 | } 140 | 141 | /** 142 | * Return type of `initLDClient`. 143 | */ 144 | export interface AllFlagsLDClient { 145 | /** 146 | * Contains all flags from LaunchDarkly. 147 | */ 148 | flags: LDFlagSet; 149 | 150 | /** 151 | * An instance of `LDClient` from the LaunchDarkly JS SDK (`launchdarkly-js-client-sdk`). 152 | * 153 | * @see https://docs.launchdarkly.com/sdk/client-side/javascript 154 | */ 155 | ldClient: LDClient; 156 | 157 | /** 158 | * LaunchDarkly client initialization error, if there was one. 159 | */ 160 | error?: Error; 161 | } 162 | 163 | /** 164 | * Map of camelized flag keys to original unmodified flag keys. 165 | */ 166 | export type LDFlagKeyMap = Record; 167 | 168 | export { type LDProps } from './withLDConsumer'; 169 | 170 | /** 171 | * The sdk context stored in the Provider state and passed to consumers. 172 | */ 173 | export interface ReactSdkContext { 174 | /** 175 | * JavaScript proxy that will trigger a LDClient#variation call on flag read in order 176 | * to register a flag evaluation event in LaunchDarkly. Empty {} initially 177 | * until flags are fetched from the LaunchDarkly servers. 178 | */ 179 | flags: LDFlagSet; 180 | 181 | /** 182 | * Map of camelized flag keys to their original unmodified form. Empty if useCamelCaseFlagKeys option is false. 183 | */ 184 | flagKeyMap: LDFlagKeyMap; 185 | 186 | /** 187 | * An instance of `LDClient` from the LaunchDarkly JS SDK (`launchdarkly-js-client-sdk`). 188 | * This will be be undefined initially until initialization is complete. 189 | * 190 | * @see https://docs.launchdarkly.com/sdk/client-side/javascript 191 | */ 192 | ldClient?: LDClient; 193 | 194 | /** 195 | * LaunchDarkly client initialization error, if there was one. 196 | */ 197 | error?: Error; 198 | } 199 | 200 | export * from 'launchdarkly-js-client-sdk'; 201 | -------------------------------------------------------------------------------- /src/useFlags.ts: -------------------------------------------------------------------------------- 1 | import { LDFlagSet } from 'launchdarkly-js-client-sdk'; 2 | import React, { useContext } from 'react'; 3 | import { defaultReactOptions, ReactSdkContext } from './types'; 4 | 5 | /** 6 | * `useFlags` is a custom hook which returns all feature flags. It uses the `useContext` primitive 7 | * to access the LaunchDarkly context set up by `withLDProvider`. As such you will still need to 8 | * use the `withLDProvider` HOC at the root of your app to initialize the React SDK and populate the 9 | * context with `ldClient` and your flags. 10 | * 11 | * @param reactContext If specified, the provided React context will be used. 12 | * 13 | * @return All the feature flags configured in your LaunchDarkly project 14 | */ 15 | const useFlags = (reactContext?: React.Context): T => { 16 | const { flags } = useContext(reactContext ?? defaultReactOptions.reactContext); 17 | 18 | return flags as T; 19 | }; 20 | export default useFlags; 21 | -------------------------------------------------------------------------------- /src/useLDClient.ts: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { defaultReactOptions, ReactSdkContext } from './types'; 3 | 4 | // eslint:disable:max-line-length 5 | /** 6 | * `useLDClient` is a custom hook which returns the underlying [LaunchDarkly JavaScript SDK client object](https://launchdarkly.github.io/js-client-sdk/interfaces/LDClient.html). 7 | * Like the `useFlags` custom hook, `useLDClient` also uses the `useContext` primitive to access the LaunchDarkly 8 | * context set up by `withLDProvider`. You will still need to use the `withLDProvider` HOC 9 | * to initialise the react sdk to use this custom hook. 10 | * 11 | * @param reactContext If specified, the custom React context will be used. 12 | * 13 | * @return The `launchdarkly-js-client-sdk` `LDClient` object 14 | */ 15 | const useLDClient = (reactContext?: React.Context) => { 16 | const { ldClient } = useContext(reactContext ?? defaultReactOptions.reactContext); 17 | 18 | return ldClient; 19 | }; 20 | 21 | export default useLDClient; 22 | -------------------------------------------------------------------------------- /src/useLDClientError.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { defaultReactOptions, ReactSdkContext } from './types'; 3 | 4 | /** 5 | * Provides the LaunchDarkly client initialization error, if there was one. 6 | * 7 | * @param reactContext If specified, the custom React context will be used. 8 | * 9 | * @return The `launchdarkly-js-client-sdk` `LDClient` initialization error 10 | */ 11 | export default function useLDClientError(reactContext?: React.Context) { 12 | const { error } = useContext(reactContext ?? defaultReactOptions.reactContext); 13 | 14 | return error; 15 | } 16 | -------------------------------------------------------------------------------- /src/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { camelCaseKeys, fetchFlags, getContextOrUser, getFlattenedFlagsFromChangeset } from './utils'; 2 | import { LDClient, LDContext, LDFlagChangeset, LDFlagSet } from 'launchdarkly-js-client-sdk'; 3 | 4 | const caseTestCases = [ 5 | ['camelCase', 'camelCase'], 6 | ['PascalCase', 'pascalCase'], 7 | ['kebab-case', 'kebabCase'], 8 | ['SCREAMING-KEBAB-CASE', 'screamingKebabCase'], 9 | ['snake_case', 'snakeCase'], 10 | ['SCREAMING_SNAKE_CASE', 'screamingSnakeCase'], 11 | ['camel_Snake_Case', 'camelSnakeCase'], 12 | ['Pascal_Snake_Case', 'pascalSnakeCase'], 13 | ['Train-Case', 'trainCase'], 14 | // we can possibly drop support for these as they are unlikely used in practice 15 | ['snake_kebab-case', 'snakeKebabCase'], 16 | ['dragon.case', 'dragonCase'], 17 | ['SCREAMING.DRAGON.CASE', 'screamingDragonCase'], 18 | ['PascalDragon.Snake_Kebab-Case', 'pascalDragonSnakeKebabCase'], 19 | ['SCREAMING.DRAGON_SNAKE_KEBAB-CASE', 'screamingDragonSnakeKebabCase'], 20 | ]; 21 | 22 | describe('Utils', () => { 23 | describe('camelCaseKeys', () => { 24 | test('should ignore system keys', () => { 25 | const bootstrap = { 26 | 'test-flag': true, 27 | 'another-test-flag': false, 28 | $flagsState: { 29 | 'test-flag': { version: 125, variation: 0, trackEvents: true }, 30 | 'another-test-flag': { version: 18, variation: 1 }, 31 | }, 32 | $valid: true, 33 | }; 34 | 35 | const result = camelCaseKeys(bootstrap); 36 | expect(result).toEqual({ testFlag: true, anotherTestFlag: false }); 37 | }); 38 | 39 | test.each(caseTestCases)('should handle %s', (key, camelKey) => { 40 | expect(camelCaseKeys({ [key]: false })).toEqual({ [camelKey]: false }); 41 | }); 42 | }); 43 | 44 | test('getFlattenedFlagsFromChangeset should return current values of all flags when no targetFlags specified', () => { 45 | const targetFlags: LDFlagSet | undefined = undefined; 46 | const flagChanges: LDFlagChangeset = { 47 | 'test-flag': { current: true, previous: false }, 48 | 'another-test-flag': { current: false, previous: true }, 49 | }; 50 | const flattened = getFlattenedFlagsFromChangeset(flagChanges, targetFlags); 51 | 52 | expect(flattened).toEqual({ 'another-test-flag': false, 'test-flag': true }); 53 | }); 54 | 55 | test('getFlattenedFlagsFromChangeset should return current values only of targetFlags when specified', () => { 56 | const targetFlags: LDFlagSet | undefined = { 'test-flag': false }; 57 | const flagChanges: LDFlagChangeset = { 58 | 'test-flag': { current: true, previous: false }, 59 | 'another-test-flag': { current: false, previous: true }, 60 | }; 61 | const flattened = getFlattenedFlagsFromChangeset(flagChanges, targetFlags); 62 | 63 | expect(flattened).toEqual({ 'test-flag': true }); 64 | }); 65 | 66 | test('getFlattenedFlagsFromChangeset should return empty LDFlagSet when no targetFlags are changed ', () => { 67 | const targetFlags: LDFlagSet | undefined = { 'test-flag': false }; 68 | const flagChanges: LDFlagChangeset = { 69 | 'another-test-flag': { current: false, previous: true }, 70 | }; 71 | const flattened = getFlattenedFlagsFromChangeset(flagChanges, targetFlags); 72 | 73 | expect(Object.keys(flattened)).toHaveLength(0); 74 | }); 75 | 76 | describe('fetchFlags', () => { 77 | const allFlags: LDFlagSet = { 'example-flag': true, 'test-example': false }; 78 | 79 | let mockLDClient: jest.Mocked>; 80 | 81 | beforeEach(() => { 82 | mockLDClient = { 83 | allFlags: jest.fn().mockReturnValue(allFlags), 84 | variation: jest.fn((_, defaultVal: boolean | string | number) => defaultVal), 85 | }; 86 | }); 87 | 88 | test('should return only the target flags', () => { 89 | const targetFlags = { 'target-one': true, 'target-two': true, 'target-three': false }; 90 | const flagSet = fetchFlags(mockLDClient as LDClient, targetFlags); 91 | 92 | expect(flagSet).toEqual({ 'target-one': true, 'target-three': false, 'target-two': true }); 93 | }); 94 | 95 | test('should return all flags when target flags is not defined', () => { 96 | const flagSet = fetchFlags(mockLDClient as LDClient, undefined); 97 | 98 | expect(mockLDClient.allFlags).toBeCalledTimes(1); 99 | expect(flagSet).toEqual({ 'example-flag': true, 'test-example': false }); 100 | }); 101 | }); 102 | 103 | describe('getContextOrUser', () => { 104 | test('returns context if both context and user are provided', () => { 105 | const clientSideID = 'test-id'; 106 | const context: LDContext = { key: 'yus', kind: 'user', name: 'yus ng' }; 107 | const user: LDContext = { key: 'deprecatedUser' }; 108 | const result = getContextOrUser({ clientSideID, context, user }); 109 | expect(result).toEqual(context); 110 | }); 111 | 112 | test('returns user if no context is provided', () => { 113 | const clientSideID = 'test-id'; 114 | const user: LDContext = { key: 'deprecatedUser' }; 115 | const result = getContextOrUser({ clientSideID, user }); 116 | expect(result).toEqual(user); 117 | }); 118 | 119 | test('returns context if only context is provided', () => { 120 | const clientSideID = 'test-id'; 121 | const context: LDContext = { key: 'yus', kind: 'user', name: 'yus ng' }; 122 | const result = getContextOrUser({ clientSideID, context }); 123 | expect(result).toEqual(context); 124 | }); 125 | 126 | test('returns undefined if no context or user is provided', () => { 127 | const clientSideID = 'test-id'; 128 | const result = getContextOrUser({ clientSideID }); 129 | expect(result).toBeUndefined(); 130 | }); 131 | }); 132 | }); 133 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { LDClient, LDContext, LDFlagChangeset, LDFlagSet } from 'launchdarkly-js-client-sdk'; 2 | import camelCase from 'lodash.camelcase'; 3 | import { ProviderConfig } from './types'; 4 | 5 | /** 6 | * Helper function to get the context or fallback to classic user. 7 | * Safe to remove when the user property is deprecated. 8 | */ 9 | export const getContextOrUser = (config: ProviderConfig): LDContext | undefined => config.context ?? config.user; 10 | 11 | /** 12 | * Transforms a set of flags so that their keys are camelCased. This function ignores 13 | * flag keys which start with `$`. 14 | * 15 | * @param rawFlags A mapping of flag keys and their values 16 | * @return A transformed `LDFlagSet` with camelCased flag keys 17 | */ 18 | export const camelCaseKeys = (rawFlags: LDFlagSet) => { 19 | const flags: LDFlagSet = {}; 20 | for (const rawFlag in rawFlags) { 21 | // Exclude system keys 22 | if (rawFlag.indexOf('$') !== 0) { 23 | flags[camelCase(rawFlag)] = rawFlags[rawFlag]; 24 | } 25 | } 26 | 27 | return flags; 28 | }; 29 | 30 | /** 31 | * Gets the flags to pass to the provider from the changeset. 32 | * 33 | * @param changes the `LDFlagChangeset` from the ldClient onchange handler. 34 | * @param targetFlags if targetFlags are specified, changes to other flags are ignored and not returned in the 35 | * flattened `LDFlagSet` 36 | * @return an `LDFlagSet` with the current flag values from the LDFlagChangeset filtered by `targetFlags`. The returned 37 | * object may be empty `{}` if none of the targetFlags were changed. 38 | */ 39 | export const getFlattenedFlagsFromChangeset = ( 40 | changes: LDFlagChangeset, 41 | targetFlags: LDFlagSet | undefined, 42 | ): LDFlagSet => { 43 | const flattened: LDFlagSet = {}; 44 | for (const key in changes) { 45 | if (!targetFlags || targetFlags[key] !== undefined) { 46 | flattened[key] = changes[key].current; 47 | } 48 | } 49 | 50 | return flattened; 51 | }; 52 | 53 | /** 54 | * Retrieves flag values. 55 | * 56 | * @param ldClient LaunchDarkly client 57 | * @param targetFlags If specified, `launchdarkly-react-client-sdk` will only listen for changes to these flags. 58 | * Flag keys must be in their original form as known to LaunchDarkly rather than in their camel-cased form. 59 | * 60 | * @returns an `LDFlagSet` with the current flag values from LaunchDarkly filtered by `targetFlags`. 61 | */ 62 | export const fetchFlags = (ldClient: LDClient, targetFlags?: LDFlagSet) => { 63 | const allFlags = ldClient.allFlags(); 64 | if (!targetFlags) { 65 | return allFlags; 66 | } 67 | 68 | return Object.keys(targetFlags).reduce((acc, key) => { 69 | acc[key] = Object.prototype.hasOwnProperty.call(allFlags, key) ? allFlags[key] : targetFlags[key]; 70 | 71 | return acc; 72 | }, {}); 73 | }; 74 | 75 | /** 76 | * @deprecated The `camelCaseKeys.camelCaseKeys` property will be removed in a future version, 77 | * please update your code to use the `camelCaseKeys` function directly. 78 | */ 79 | camelCaseKeys.camelCaseKeys = camelCaseKeys; 80 | 81 | export default { camelCaseKeys, getFlattenedFlagsFromChangeset, fetchFlags }; 82 | -------------------------------------------------------------------------------- /src/withLDConsumer.test.tsx: -------------------------------------------------------------------------------- 1 | import { LDFlagSet } from 'launchdarkly-js-client-sdk'; 2 | 3 | interface HocProps { 4 | flags?: LDFlagSet; 5 | ldClient?: { track: jest.Mock }; 6 | } 7 | 8 | jest.mock('./context', () => { 9 | interface ConsumerChildren { 10 | children(props: HocProps): React.ReactNode; 11 | } 12 | 13 | return { 14 | Consumer(props: ConsumerChildren) { 15 | return props.children({ flags: { testFlag: true }, ldClient: { track: jest.fn() } }); 16 | }, 17 | }; 18 | }); 19 | 20 | import * as React from 'react'; 21 | import { create } from 'react-test-renderer'; 22 | import withLDConsumer from './withLDConsumer'; 23 | 24 | describe('withLDConsumer', () => { 25 | test('flags are passed down through context api', () => { 26 | const Home = (props: HocProps) => ( 27 |
{props.flags && props.flags.testFlag ? 'testFlag detected' : 'Negative, no flag'}
28 | ); 29 | const HomeWithFlags = withLDConsumer()(Home); 30 | const component = create(); 31 | expect(component).toMatchSnapshot(); 32 | }); 33 | 34 | test('ldClient is passed down through context api', () => { 35 | const Home = (props: HocProps) =>
{props.ldClient ? 'ldClient detected' : 'Negative, no ldClient'}
; 36 | const HomeWithFlags = withLDConsumer()(Home); 37 | const component = create(); 38 | expect(component).toMatchSnapshot(); 39 | }); 40 | 41 | test('only ldClient is passed down through context api', () => { 42 | const Home = (props: HocProps) => ( 43 |
44 | {props.flags ? 'flags detected' : 'Negative, no flag'} 45 | {props.ldClient ? 'ldClient detected' : 'Negative, no ldClient'} 46 |
47 | ); 48 | const HomeWithFlags = withLDConsumer({ clientOnly: true })(Home); 49 | const component = create(); 50 | expect(component).toMatchSnapshot(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/withLDConsumer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { LDClient, LDFlagSet } from 'launchdarkly-js-client-sdk'; 3 | import { defaultReactOptions, ReactSdkContext } from './types'; 4 | 5 | /** 6 | * Controls the props the wrapped component receives from the `LDConsumer` HOC. 7 | */ 8 | export interface ConsumerOptions { 9 | /** 10 | * If true then the wrapped component only receives the `ldClient` instance 11 | * and nothing else. 12 | */ 13 | clientOnly: boolean; 14 | 15 | reactContext?: React.Context; 16 | } 17 | 18 | /** 19 | * The possible props the wrapped component can receive from the `LDConsumer` HOC. 20 | */ 21 | export interface LDProps { 22 | /** 23 | * A map of feature flags from their keys to their values. 24 | * Keys are camelCased using `lodash.camelcase`. 25 | */ 26 | flags?: LDFlagSet; 27 | 28 | /** 29 | * An instance of `LDClient` from the LaunchDarkly JS SDK (`launchdarkly-js-client-sdk`) 30 | * 31 | * @see https://docs.launchdarkly.com/sdk/client-side/javascript 32 | */ 33 | ldClient?: LDClient; 34 | } 35 | 36 | /** 37 | * withLDConsumer is a function which accepts an optional options object and returns a function 38 | * which accepts your React component. This function returns a HOC with flags 39 | * and the ldClient instance injected via props. 40 | * 41 | * @param options - If you need only the `ldClient` instance and not flags, then set `{ clientOnly: true }` 42 | * to only pass the ldClient prop to your component. Defaults to `{ clientOnly: false }`. 43 | * @return A HOC with flags and the `ldClient` instance injected via props 44 | */ 45 | function withLDConsumer(options: ConsumerOptions = { clientOnly: false }) { 46 | return function withLDConsumerHoc

(WrappedComponent: React.ComponentType

) { 47 | const ReactContext = options.reactContext ?? defaultReactOptions.reactContext; 48 | 49 | return (props: P) => ( 50 | 51 | {({ flags, ldClient }: ReactSdkContext) => { 52 | if (options.clientOnly) { 53 | return ; 54 | } 55 | 56 | return ; 57 | }} 58 | 59 | ); 60 | }; 61 | } 62 | 63 | export default withLDConsumer; 64 | -------------------------------------------------------------------------------- /src/withLDProvider.test.tsx: -------------------------------------------------------------------------------- 1 | import wrapperOptions from './wrapperOptions'; 2 | 3 | jest.mock('launchdarkly-js-client-sdk', () => { 4 | const actual = jest.requireActual('launchdarkly-js-client-sdk'); 5 | 6 | return { 7 | ...actual, 8 | initialize: jest.fn(), 9 | }; 10 | }); 11 | jest.mock('./utils', () => { 12 | const originalModule = jest.requireActual('./utils'); 13 | 14 | return { 15 | ...originalModule, 16 | fetchFlags: jest.fn(), 17 | }; 18 | }); 19 | jest.mock('./context', () => ({ Provider: 'Provider' })); 20 | 21 | import * as React from 'react'; 22 | import { create } from 'react-test-renderer'; 23 | import { initialize, LDContext, LDFlagChangeset, LDOptions } from 'launchdarkly-js-client-sdk'; 24 | import withLDProvider from './withLDProvider'; 25 | import { EnhancedComponent } from './types'; 26 | import LDProvider from './provider'; 27 | import { fetchFlags } from './utils'; 28 | import ProviderState from './providerState'; 29 | 30 | const clientSideID = 'test-client-side-id'; 31 | const App = () =>

My App
; 32 | const mockInitialize = initialize as jest.Mock; 33 | const mockFetchFlags = fetchFlags as jest.Mock; 34 | const rawFlags = { 'test-flag': true, 'another-test-flag': true }; 35 | const mockLDClient = { 36 | on: jest.fn((_e: string, cb: () => void) => { 37 | cb(); 38 | }), 39 | allFlags: jest.fn().mockReturnValue({}), 40 | variation: jest.fn(), 41 | waitForInitialization: jest.fn(), 42 | }; 43 | 44 | describe('withLDProvider', () => { 45 | let options: LDOptions; 46 | let previousState: ProviderState; 47 | 48 | beforeEach(() => { 49 | mockInitialize.mockImplementation(() => mockLDClient); 50 | mockFetchFlags.mockImplementation(() => rawFlags); 51 | mockLDClient.variation.mockImplementation((_, v) => v); 52 | options = { bootstrap: {}, ...wrapperOptions }; 53 | previousState = { 54 | unproxiedFlags: {}, 55 | flags: {}, 56 | flagKeyMap: {}, 57 | }; 58 | }); 59 | 60 | afterEach(() => { 61 | jest.resetAllMocks(); 62 | }); 63 | 64 | test('render app', () => { 65 | const LaunchDarklyApp = withLDProvider({ clientSideID })(App); 66 | const component = create(); 67 | expect(component).toMatchSnapshot(); 68 | }); 69 | 70 | test('ld client is initialised correctly', async () => { 71 | const context: LDContext = { key: 'yus', kind: 'user', name: 'yus ng' }; 72 | const LaunchDarklyApp = withLDProvider({ clientSideID, context, options })(App); 73 | const instance = create().root.findByType(LDProvider).instance as EnhancedComponent; 74 | 75 | await instance.componentDidMount(); 76 | expect(mockInitialize).toHaveBeenCalledWith(clientSideID, context, options); 77 | }); 78 | 79 | test('ld client is initialised correctly with target flags', async () => { 80 | mockFetchFlags.mockImplementation(() => ({ 'dev-test-flag': true, 'launch-doggly': true })); 81 | const context: LDContext = { key: 'yus', kind: 'user', name: 'yus ng' }; 82 | const flags = { 'dev-test-flag': false, 'launch-doggly': false }; 83 | const LaunchDarklyApp = withLDProvider({ clientSideID, context, options, flags })(App); 84 | const instance = create().root.findByType(LDProvider).instance as EnhancedComponent; 85 | const mockSetState = jest.spyOn(instance, 'setState'); 86 | 87 | await instance.componentDidMount(); 88 | const setStateFunction = mockSetState.mock?.lastCall?.[0] as (p: ProviderState) => ProviderState; 89 | 90 | expect(mockInitialize).toHaveBeenCalledWith(clientSideID, context, options); 91 | expect(setStateFunction(previousState)).toEqual({ 92 | flags: { devTestFlag: true, launchDoggly: true }, 93 | unproxiedFlags: { 'dev-test-flag': true, 'launch-doggly': true }, 94 | flagKeyMap: { devTestFlag: 'dev-test-flag', launchDoggly: 'launch-doggly' }, 95 | ldClient: mockLDClient, 96 | }); 97 | }); 98 | 99 | test('flags and ldClient are saved in state on mount', async () => { 100 | const LaunchDarklyApp = withLDProvider({ clientSideID })(App); 101 | const instance = create().root.findByType(LDProvider).instance as EnhancedComponent; 102 | const mockSetState = jest.spyOn(instance, 'setState'); 103 | 104 | await instance.componentDidMount(); 105 | const setStateFunction = mockSetState.mock?.lastCall?.[0] as (p: ProviderState) => ProviderState; 106 | 107 | expect(setStateFunction(previousState)).toEqual({ 108 | flags: { testFlag: true, anotherTestFlag: true }, 109 | unproxiedFlags: rawFlags, 110 | flagKeyMap: { testFlag: 'test-flag', anotherTestFlag: 'another-test-flag' }, 111 | ldClient: mockLDClient, 112 | }); 113 | }); 114 | 115 | test('subscribeToChanges is called on mount', async () => { 116 | const LaunchDarklyApp = withLDProvider({ clientSideID })(App); 117 | const instance = create().root.findByType(LDProvider).instance as EnhancedComponent; 118 | instance.subscribeToChanges = jest.fn(); 119 | 120 | await instance.componentDidMount(); 121 | expect(instance.subscribeToChanges).toHaveBeenCalled(); 122 | }); 123 | 124 | test('subscribe to changes with camelCase', async () => { 125 | mockLDClient.on.mockImplementation((_e: string, cb: (c: LDFlagChangeset) => void) => { 126 | cb({ 'test-flag': { current: false, previous: true } }); 127 | }); 128 | const LaunchDarklyApp = withLDProvider({ clientSideID })(App); 129 | const instance = create().root.findByType(LDProvider).instance as EnhancedComponent; 130 | const mockSetState = jest.spyOn(instance, 'setState'); 131 | 132 | await instance.componentDidMount(); 133 | const setStateFunction = mockSetState.mock?.lastCall?.[0] as (p: ProviderState) => ProviderState; 134 | 135 | expect(mockLDClient.on).toHaveBeenCalledWith('change', expect.any(Function)); 136 | expect(setStateFunction(previousState)).toEqual({ 137 | flags: { anotherTestFlag: true, testFlag: false }, 138 | unproxiedFlags: { 'test-flag': false, 'another-test-flag': true }, 139 | flagKeyMap: { testFlag: 'test-flag', anotherTestFlag: 'another-test-flag' }, 140 | }); 141 | }); 142 | 143 | test('subscribe to changes with kebab-case', async () => { 144 | mockLDClient.on.mockImplementation((_e: string, cb: (c: LDFlagChangeset) => void) => { 145 | cb({ 'another-test-flag': { current: false, previous: true }, 'test-flag': { current: false, previous: true } }); 146 | }); 147 | const LaunchDarklyApp = withLDProvider({ clientSideID, reactOptions: { useCamelCaseFlagKeys: false } })(App); 148 | const instance = create().root.findByType(LDProvider).instance as EnhancedComponent; 149 | const mockSetState = jest.spyOn(instance, 'setState'); 150 | 151 | await instance.componentDidMount(); 152 | const setStateFunction = mockSetState.mock?.lastCall?.[0] as (p: ProviderState) => ProviderState; 153 | 154 | expect(mockLDClient.on).toHaveBeenCalledWith('change', expect.any(Function)); 155 | expect(setStateFunction(previousState)).toEqual({ 156 | flags: { 'test-flag': false, 'another-test-flag': false }, 157 | unproxiedFlags: { 'test-flag': false, 'another-test-flag': false }, 158 | flagKeyMap: {}, 159 | }); 160 | }); 161 | 162 | test('hoist non react statics', () => { 163 | interface ComponentWithStaticFn extends React.FC { 164 | getInitialProps(): void; 165 | } 166 | const WrappedComponent: ComponentWithStaticFn = () => <>; 167 | WrappedComponent.getInitialProps = () => ''; 168 | 169 | const LaunchDarklyApp = withLDProvider({ clientSideID, reactOptions: { useCamelCaseFlagKeys: false } })( 170 | WrappedComponent, 171 | ) as ComponentWithStaticFn; 172 | expect(LaunchDarklyApp.getInitialProps).toBeDefined(); 173 | }); 174 | }); 175 | -------------------------------------------------------------------------------- /src/withLDProvider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { defaultReactOptions, ProviderConfig } from './types'; 3 | import LDProvider from './provider'; 4 | import hoistNonReactStatics from 'hoist-non-react-statics'; 5 | 6 | /** 7 | * `withLDProvider` is a function which accepts a config object which is used to 8 | * initialize `launchdarkly-js-client-sdk`. 9 | * 10 | * This HOC handles passing configuration to the `LDProvider`, which does the following: 11 | * - It initializes the ldClient instance by calling `launchdarkly-js-client-sdk` initialize on `componentDidMount` 12 | * - It saves all flags and the ldClient instance in the context API 13 | * - It subscribes to flag changes and propagate them through the context API 14 | * 15 | * The difference between `withLDProvider` and `asyncWithLDProvider` is that `withLDProvider` initializes 16 | * `launchdarkly-js-client-sdk` at `componentDidMount`. This means your flags and the ldClient are only available after 17 | * your app has mounted. This can result in a flicker due to flag changes at startup time. 18 | * 19 | * `asyncWithLDProvider` initializes `launchdarkly-js-client-sdk` at the entry point of your app prior to render. 20 | * This means that your flags and the ldClient are ready at the beginning of your app. This ensures your app does not 21 | * flicker due to flag changes at startup time. 22 | * 23 | * @param config - The configuration used to initialize LaunchDarkly's JS SDK 24 | * @return A function which accepts your root React component and returns a HOC 25 | */ 26 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type 27 | export function withLDProvider( 28 | config: ProviderConfig, 29 | ): (WrappedComponent: React.ComponentType) => React.ComponentType { 30 | return function withLDProviderHoc(WrappedComponent: React.ComponentType): React.ComponentType { 31 | const { reactOptions: userReactOptions } = config; 32 | const reactOptions = { ...defaultReactOptions, ...userReactOptions }; 33 | const providerProps = { ...config, reactOptions }; 34 | 35 | function HoistedComponent(props: T) { 36 | return ( 37 | 38 | 39 | 40 | ); 41 | } 42 | 43 | hoistNonReactStatics(HoistedComponent, WrappedComponent); 44 | 45 | return HoistedComponent; 46 | }; 47 | } 48 | 49 | export default withLDProvider; 50 | -------------------------------------------------------------------------------- /src/wrapperOptions.ts: -------------------------------------------------------------------------------- 1 | import { LDOptions } from 'launchdarkly-js-client-sdk'; 2 | import * as packageInfo from '../package.json'; 3 | 4 | const wrapperOptions: LDOptions = { 5 | wrapperName: 'react-client-sdk', 6 | wrapperVersion: packageInfo.version, 7 | sendEventsOnlyForVariation: true, 8 | }; 9 | 10 | export default wrapperOptions; 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "lib", 4 | "target": "es6", 5 | "lib": ["es6", "esnext", "dom"], 6 | "module": "commonjs", 7 | "declaration": true, 8 | "sourceMap": true, 9 | "sourceRoot": "../", 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "jsx": "react", 14 | "esModuleInterop": true, 15 | "resolveJsonModule": true 16 | }, 17 | "include": ["src"], 18 | "exclude": ["node_modules", "lib", "dist", "example"] 19 | } 20 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://typedoc.org/schema.json", 3 | "name": "launchdarkly-react-client-sdk", 4 | "includeVersion": true, 5 | "entryPoints": [ 6 | "src/index.ts", 7 | ] 8 | } 9 | --------------------------------------------------------------------------------