diff --git a/package.json b/package.json index 4660b1f42..442cd6c7a 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ "packages/sdk/electron", "packages/sdk/fastly", "packages/sdk/fastly/example", + "packages/sdk/react", + "packages/sdk/react/contract-tests", "packages/sdk/react-native", "packages/sdk/react-native/example", "packages/sdk/react-universal", diff --git a/packages/sdk/react/LICENSE b/packages/sdk/react/LICENSE new file mode 100644 index 000000000..badc81e16 --- /dev/null +++ b/packages/sdk/react/LICENSE @@ -0,0 +1,13 @@ +Copyright 2026 Catamorphic, Co. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/packages/sdk/react/README.md b/packages/sdk/react/README.md new file mode 100644 index 000000000..74761c551 --- /dev/null +++ b/packages/sdk/react/README.md @@ -0,0 +1,21 @@ +# LaunchDarkly React SDK + +> [!CAUTION] +> This [SDK|feature] is experimental and should NOT be considered ready for production use. +> It may change or be removed without notice and is not subject to backwards +> compatibility guarantees. + +## About LaunchDarkly + +- 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: + - 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. + - 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?). + - 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. + - 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). + - Disable parts of your application to facilitate maintenance, without taking everything offline. +- 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. +- Explore LaunchDarkly + - [launchdarkly.com](https://www.launchdarkly.com/ 'LaunchDarkly Main Website') for more information + - [docs.launchdarkly.com](https://docs.launchdarkly.com/ 'LaunchDarkly Documentation') for our documentation and SDK reference guides + - [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/ 'LaunchDarkly API Documentation') for our API documentation + - [blog.launchdarkly.com](https://blog.launchdarkly.com/ 'LaunchDarkly Blog Documentation') for the latest product updates diff --git a/packages/sdk/react/__tests__/index.test.ts b/packages/sdk/react/__tests__/index.test.ts new file mode 100644 index 000000000..88b76d01e --- /dev/null +++ b/packages/sdk/react/__tests__/index.test.ts @@ -0,0 +1,3 @@ +describe('react-sdk', () => { + test.todo('Add react sdk tests'); +}); diff --git a/packages/sdk/react/contract-tests/README.md b/packages/sdk/react/contract-tests/README.md new file mode 100644 index 000000000..1d1e10469 --- /dev/null +++ b/packages/sdk/react/contract-tests/README.md @@ -0,0 +1,2 @@ +# React SDK contract-tests + diff --git a/packages/sdk/react/contract-tests/package.json b/packages/sdk/react/contract-tests/package.json new file mode 100644 index 000000000..1c9b62bfb --- /dev/null +++ b/packages/sdk/react/contract-tests/package.json @@ -0,0 +1,4 @@ +{ + "name": "@internal/react-sdk-contract-tests", + "packageManager": "yarn@3.4.1" +} diff --git a/packages/sdk/react/package.json b/packages/sdk/react/package.json new file mode 100644 index 000000000..5a8dd7dbd --- /dev/null +++ b/packages/sdk/react/package.json @@ -0,0 +1,49 @@ +{ + "name": "@launchdarkly/react-sdk", + "version": "0.0.1", + "description": "LaunchDarkly SDK for React frameworks", + "homepage": "https://github.com/launchdarkly/js-core/tree/main/packages/sdk/react", + "repository": { + "type": "git", + "url": "https://github.com/launchdarkly/js-core.git" + }, + "license": "Apache-2.0", + "packageManager": "yarn@4.2.2", + "keywords": [ + "launchdarkly", + "react", + "isomorphic", + "nextjs", + "remix" + ], + "type": "module", + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "default": "./dist/src/index.js" + }, + "./client": { + "types": "./dist/src/client/index.d.ts", + "default": "./dist/src/client/index.js" + }, + "./server": { + "types": "./dist/src/server/index.d.ts", + "default": "./dist/src/server/index.js" + } + }, + "files": [ + "dist" + ], + "devDependencies": { + "typedoc": "0.25.0", + "typescript": "5.1.6" + }, + "dependencies": { + "@launchdarkly/js-client-sdk": "workspace:^", + "@launchdarkly/js-client-sdk-common": "workspace:^", + "@launchdarkly/js-server-sdk-common": "workspace:^" + }, + "peerDependencies": { + "react": ">=18.0.0" + } +} diff --git a/packages/sdk/react/src/LDIsomorphicClient.ts b/packages/sdk/react/src/LDIsomorphicClient.ts new file mode 100644 index 000000000..5045fe590 --- /dev/null +++ b/packages/sdk/react/src/LDIsomorphicClient.ts @@ -0,0 +1,30 @@ +import { LDReactClient } from './client/LDClient'; +import { LDReactServerClient } from './server/LDClient'; + +/** + * The LaunchDarkly isomorphic client interface. + * + * This is a common interface that can be used to create a client + * that can be used on the server and client sides. + * + * @privateRemarks + * NOTE: This interface might be replaced shared functions in the future which + * maybe better for tree shaking. + */ +export interface LDIsomorphicClient extends Omit< + LDReactClient, + 'waitForInitialization' | 'start' | 'addHook' +> { + + /** + * A builder function that will federate the current client with a server component. + * RSC components will ONLY be available if this function is called. + * + * @remarks + * By default, the react client will only be doing client side rendering. + * + * @param LDServerClient A LaunchDarkly server client + * @returns + */ + useServerClient: (LDServerClient: LDReactServerClient) => this; +} diff --git a/packages/sdk/react/src/LDIsomorphicOptions.ts b/packages/sdk/react/src/LDIsomorphicOptions.ts new file mode 100644 index 000000000..9b6204005 --- /dev/null +++ b/packages/sdk/react/src/LDIsomorphicOptions.ts @@ -0,0 +1,7 @@ +import { LDReactClientOptions } from "./client/LDOptions"; + +/** + * Options for creating an isomorphic client. + */ +export interface LDIsomorphicOptions extends LDReactClientOptions { +} \ No newline at end of file diff --git a/packages/sdk/react/src/client/LDClient.ts b/packages/sdk/react/src/client/LDClient.ts new file mode 100644 index 000000000..8d6515016 --- /dev/null +++ b/packages/sdk/react/src/client/LDClient.ts @@ -0,0 +1,58 @@ +import { LDClient, LDContext, LDWaitForInitializationResult } from '@launchdarkly/js-client-sdk'; + +/** + * Initialization state of the client. This type should be consistent with + * the `status` field of the `LDWaitForInitializationResult` type. + */ +export type IntializedState = LDWaitForInitializationResult['status'] | 'initializing' | 'unknown'; + +/** + * The LaunchDarkly client interface for React. + * + * @privateRemarks + * We will provide 2 ways to create instances of LDClient: + * 1. A `createClient` function that is similar to the js-client-sdk's `createClient` function. + * 2. A `createLDProvider` function that creates a React Context + * + */ +export interface LDReactClient extends LDClient { + /** + * Returns the initialization state of the client. This function is helpful to determine + * whether LDClient can be used to evaluate flags on intial component render. + * + * @see {@link LDWaitForInitializationResult} for the possible values and their meaning + * + * @returns {IntializedState} The initialization state of the client. + */ + getInitializationState(): IntializedState; +} + +/** + * The react context interface for the launchdarkly client. This will be the type that is + * used in the `createContext` function. + */ +export interface LDReactClientContextValue { + /** + * The LaunchDarkly client. + */ + client: LDReactClient; + + /** + * The LaunchDarkly context. + */ + context: LDContext; + + /** + * The initialization state of the client. + */ + intializedState: IntializedState; +} + +/** + * The LaunchDarkly client context provider interface for React. + * This will be the type that is returned from our createContext function. + */ +export interface LDReactClientContextProvider { + Context: React.Context; +} + diff --git a/packages/sdk/react/src/client/LDOptions.ts b/packages/sdk/react/src/client/LDOptions.ts new file mode 100644 index 000000000..a618c18fd --- /dev/null +++ b/packages/sdk/react/src/client/LDOptions.ts @@ -0,0 +1,67 @@ +import { LDContext, LDOptions as LDOptionsBase, LDStartOptions } from '@launchdarkly/js-client-sdk'; + +/** + * Initialization options for the LaunchDarkly React SDK. + */ +export interface LDReactClientOptions extends LDOptionsBase { + /** + * Whether the React SDK should transform flag keys into camel-cased format. + * Using camel-cased flag keys allow for easier use as prop values, however, + * these keys won't directly match the flag keys as known to LaunchDarkly. + * Consequently, flag key collisions may be possible and the Code References feature + * will not function properly. + * + * This is true by default, meaning that keys will automatically be converted to camel-case. + * + * For more information, see the React SDK Reference Guide on + * [flag keys](https://docs.launchdarkly.com/sdk/client-side/react/react-web#flag-keys). + * + * @see https://docs.launchdarkly.com/sdk/client-side/react/react-web#flag-keys + */ + useCamelCaseFlagKeys?: boolean; +} + +/** + * Options for creating a React Provider. + */ +export interface LDReactProviderOptions { + /** + * LaunchDarkly initialization options. These options are common between LaunchDarkly's JavaScript and React SDKs. + * + * @see {@link LDReactClientOptions} for the possible options + */ + options?: LDReactClientOptions; + + /** + * Your project and environment specific client side ID. You can find + * this in your LaunchDarkly portal under Account settings. + */ + clientSideID: string; + + /** + * A LaunchDarkly context object. This will be used as the initial + * context for the client. + */ + context: LDContext; + + /** + * Options for starting the LaunchDarkly client. + * + * @remarks + * This option is especially useful if you choose to not defer + * initialization and want to start the client immediately. + * + * @see {@link LDStartOptions} for the possible options + */ + startOptions?: LDStartOptions; + + /** + * If set to false, the LDClient will initialize immediately. + * + * @default true + * + * If intiailization is deferred, then the LDClient can be initialized manually + * by calling the `start` function. + */ + deferInitialization?: boolean; +} diff --git a/packages/sdk/react/src/client/index.ts b/packages/sdk/react/src/client/index.ts new file mode 100644 index 000000000..82a804ad6 --- /dev/null +++ b/packages/sdk/react/src/client/index.ts @@ -0,0 +1,108 @@ +import { LDReactClient, LDReactClientContextProvider, LDReactClientContextValue } from './LDClient'; +import { createContext as createReactContext } from 'react'; +import { LDReactClientOptions } from './LDOptions'; +import { LDContext } from '@launchdarkly/js-client-sdk'; + +export type * from '@launchdarkly/js-client-sdk'; +export * from './LDClient'; +export * from './LDOptions'; + +/** + * Creates a new instance of the LaunchDarkly client. + * + * @remarks + * This function is exported so that developers can have more flexibility in client creation. + * More so this is to preserve previous behavior of app developers managing their own client + * instance. + * + * we DO NOT recommend using this client creation method. + * + * @example + * ```tsx + * import { createClient } from '@launchdarkly/react'; + * const client = createClient(clientSideID, context, options); + * ``` + * + * @param clientSideID + * @param context + * @param options + * @returns + */ +// ts-expect-error - TODO: implement this +export function createClient(clientSideID: string, context: LDContext, options?: LDReactClientOptions): LDReactClient { + // TODO: implement this + return null as any; +} + +/** + * Creates a new context provider from a LaunchDarkly client instance. + * + * @remarks + * We export this function so that developers can more easily manage their own client instance. + * This was how we supported multiple client instances in the past. + * + * we DO NOT recommend using this client creation method. + * + * @param client A LaunchDarkly client instance. + * @returns A LaunchDarkly client context provider. + */ +// ts-expect-error - TODO: implement this +export function createContextFromClient(client: LDReactClient): LDReactClientContextProvider { + // TODO: implement this + return null as any; +} + +/** + * Creates a new context provider for the LaunchDarkly client. + * + * TODO: this is unfinished and is only here to provide some reminders + * of what I was thinking. + * + * @example + * + * ```tsx + * const { Context, useContext } = createContext(); + * + * function MyComponent() { + * const { client, context, intializedState } = useContext(); + * return
Client: {client.toString()}, Context: {context.toString()}, InitializedState: {intializedState}
; + * } + * ``` + * + * Typically, in React applications, you might want to export the context so that it can be used in other + * components. + * + * ```tsx + * export createContext(); + * ``` + * + * ```tsx + * import { Context } from './path/to/context'; + * + * function MyComponent() { + * const { client, context, intializedState } = useContext(Context); + * return
Client: {client.toString()}, Context: {context.toString()}, InitializedState: {intializedState}
; + * } + * + * function MyOtherComponent() { + * return + * + * ; + * } + * ``` + * + * @returns The LaunchDarkly client context provider. + */ +export function createClientContext(clientSideID: string, context: LDContext, options?: LDReactClientOptions): LDReactClientContextProvider { + const client = createClient(clientSideID, context, options); + const Context = createReactContext({ + client, + context, + intializedState: 'unknown', + }); + + return { + Context, + }; +} + diff --git a/packages/sdk/react/src/index.ts b/packages/sdk/react/src/index.ts new file mode 100644 index 000000000..c88cfba69 --- /dev/null +++ b/packages/sdk/react/src/index.ts @@ -0,0 +1,11 @@ +import { LDIsomorphicClient } from "./LDIsomorphicClient"; +import { LDContext } from "@launchdarkly/js-client-sdk"; +import { LDIsomorphicOptions } from "./LDIsomorphicOptions"; + +/** + * Creates a new instance of the launchdarkly client. + */ +// @ts-expect-error - TODO: implement this +export function createClient(clientSideID: string, context: LDContext, options?: LDIsomorphicOptions): LDIsomorphicClient { + +} diff --git a/packages/sdk/react/src/server/LDClient.ts b/packages/sdk/react/src/server/LDClient.ts new file mode 100644 index 000000000..7aed562aa --- /dev/null +++ b/packages/sdk/react/src/server/LDClient.ts @@ -0,0 +1,279 @@ +import { + LDEvaluationDetail, + LDEvaluationDetailTyped, + LDFlagsState, + LDFlagsStateOptions, + LDFlagValue, + LDMigrationStage, + LDMigrationVariation, +} from '@launchdarkly/js-server-sdk-common'; + +/** + * The LaunchDarkly server client interface for React. + * + * @remarks + * This is a restrictive version of the LDClient interface. + * The main reason for this is to ensure we leverage client side + * rendering appropriately for more dynamic content. + * + * @privateRemarks + * We are basing this off the common server client interface so that we + * can potentially support edge sdk rendering. The main difference between this + * interface and the common server interface is that we do not have a context parameter. + * + * This is because the context is determined by the context provider and will be different + * for each request. + * + * @see {@link LDReactServerOptions} for the possible options + * + */ +export interface LDReactServerClient { + /** + * Tests whether the client has completed initialization. + * + * If this returns false, it means that the client has not yet successfully connected to + * LaunchDarkly. It might still be in the process of starting up, or it might be attempting to + * reconnect after an unsuccessful attempt, or it might have received an unrecoverable error (such + * as an invalid SDK key) and given up. + * + * @returns + * True if the client has successfully initialized. + */ + initialized(): boolean; + + /** + * Determines the variation of a feature flag for a context. + * + * @param key The unique key of the feature flag. + * @param defaultValue The default value of the flag, to be used if the value is not available + * from LaunchDarkly. + * @param callback A Node-style callback to receive the result value. If omitted, you will receive + * a Promise instead. + * @returns + * If you provided a callback, then nothing. Otherwise, a Promise which will be resolved with + * the result value. + */ + variation( + key: string, + defaultValue: LDFlagValue, + callback?: (err: any, res: LDFlagValue) => void, + ): Promise; + + /** + * Determines the variation of a feature flag for a context, along with information about how it + * was calculated. + * + * The `reason` property of the result will also be included in analytics events, if you are + * capturing detailed event data for this flag. + * + * For more information, see the [SDK reference + * guide](https://docs.launchdarkly.com/sdk/features/evaluation-reasons#nodejs-server-side). + * + * @param key The unique key of the feature flag. + * @param defaultValue The default value of the flag, to be used if the value is not available + * from LaunchDarkly. + * @param callback A Node-style callback to receive the result (as an {@link LDEvaluationDetail}). + * If omitted, you will receive a Promise instead. + * @returns + * If you provided a callback, then nothing. Otherwise, a Promise which will be resolved with + * the result (as an{@link LDEvaluationDetail}). + */ + variationDetail( + key: string, + defaultValue: LDFlagValue, + callback?: (err: any, res: LDEvaluationDetail) => void, + ): Promise; + + /** + * Returns the migration stage of the migration feature flag for the given + * evaluation context. + * + * If the evaluated value of the flag cannot be converted to an LDMigrationStage, then the default + * value will be returned and error will be logged. + * + * @param key The unique key of the feature flag. + * @param defaultValue The default value of the flag, to be used if the value is not available + * from LaunchDarkly. + * @returns + * A Promise which will be resolved with the result (as an{@link LDMigrationVariation}). + */ + migrationVariation(key: string, defaultValue: LDMigrationStage): Promise; + + /** + * Determines the boolean variation of a feature flag for a context. + * + * If the flag variation does not have a boolean value, defaultValue is returned. + * + * @param key The unique key of the feature flag. + * @param defaultValue The default value of the flag, to be used if the value is not available + * from LaunchDarkly. + * @returns + * A Promise which will be resolved with the result value. + */ + boolVariation(key: string, defaultValue: boolean): Promise; + + /** + * Determines the numeric variation of a feature flag for a context. + * + * If the flag variation does not have a numeric value, defaultValue is returned. + * + * @param key The unique key of the feature flag. + * @param defaultValue The default value of the flag, to be used if the value is not available + * from LaunchDarkly. + * @returns + * A Promise which will be resolved with the result value. + */ + numberVariation(key: string, defaultValue: number): Promise; + + /** + * Determines the string variation of a feature flag for a context. + * + * If the flag variation does not have a string value, defaultValue is returned. + * + * @param key The unique key of the feature flag. + * @param defaultValue The default value of the flag, to be used if the value is not available + * from LaunchDarkly. + * @returns + * A Promise which will be resolved with the result value. + */ + stringVariation(key: string, defaultValue: string): Promise; + + /** + * Determines the variation of a feature flag for a context. + * + * This version may be favored in TypeScript versus `variation` because it returns + * an `unknown` type instead of `any`. `unknown` will require a cast before usage. + * + * @param key The unique key of the feature flag. + * @param defaultValue The default value of the flag, to be used if the value is not available + * from LaunchDarkly. + * @returns + * A Promise which will be resolved with the result value. + */ + jsonVariation(key: string, defaultValue: unknown): Promise; + + /** + * Determines the boolean variation of a feature flag for a context, along with information about + * how it was calculated. + * + * The `reason` property of the result will also be included in analytics events, if you are + * capturing detailed event data for this flag. + * + * If the flag variation does not have a boolean value, defaultValue is returned. The reason will + * indicate an error of the type `WRONG_KIND` in this case. + * + * For more information, see the [SDK reference + * guide](https://docs.launchdarkly.com/sdk/features/evaluation-reasons#nodejs-server-side). + * + * @param key The unique key of the feature flag. + * @param defaultValue The default value of the flag, to be used if the value is not available + * from LaunchDarkly. + * @returns + * A Promise which will be resolved with the result + * (as an {@link LDEvaluationDetailTyped}). + */ + boolVariationDetail( + key: string, + defaultValue: boolean, + ): Promise>; + + /** + * Determines the numeric variation of a feature flag for a context, along with information about + * how it was calculated. + * + * The `reason` property of the result will also be included in analytics events, if you are + * capturing detailed event data for this flag. + * + * If the flag variation does not have a numeric value, defaultValue is returned. The reason will + * indicate an error of the type `WRONG_KIND` in this case. + * + * For more information, see the [SDK reference + * guide](https://docs.launchdarkly.com/sdk/features/evaluation-reasons#nodejs-server-side). + * + * @param key The unique key of the feature flag. + * @param defaultValue The default value of the flag, to be used if the value is not available + * from LaunchDarkly. + * @returns + * A Promise which will be resolved with the result + * (as an {@link LDEvaluationDetailTyped}). + */ + numberVariationDetail( + key: string, + defaultValue: number, + ): Promise>; + + /** + * Determines the string variation of a feature flag for a context, along with information about + * how it was calculated. + * + * The `reason` property of the result will also be included in analytics events, if you are + * capturing detailed event data for this flag. + * + * If the flag variation does not have a string value, defaultValue is returned. The reason will + * indicate an error of the type `WRONG_KIND` in this case. + * + * For more information, see the [SDK reference + * guide](https://docs.launchdarkly.com/sdk/features/evaluation-reasons#nodejs-server-side). + * + * @param key The unique key of the feature flag. + * @param defaultValue The default value of the flag, to be used if the value is not available + * from LaunchDarkly. + * @returns + * A Promise which will be resolved with the result + * (as an {@link LDEvaluationDetailTyped}). + */ + stringVariationDetail( + key: string, + defaultValue: string, + ): Promise>; + + /** + * Determines the variation of a feature flag for a context, along with information about how it + * was calculated. + * + * The `reason` property of the result will also be included in analytics events, if you are + * capturing detailed event data for this flag. + * + * This version may be favored in TypeScript versus `variation` because it returns + * an `unknown` type instead of `any`. `unknown` will require a cast before usage. + * + * For more information, see the [SDK reference + * guide](https://docs.launchdarkly.com/sdk/features/evaluation-reasons#nodejs-server-side). + * + * @param key The unique key of the feature flag. + * @param defaultValue The default value of the flag, to be used if the value is not available + * from LaunchDarkly. + * @param callback A Node-style callback to receive the result (as an {@link LDEvaluationDetail}). + * If omitted, you will receive a Promise instead. + * @returns + * If you provided a callback, then nothing. Otherwise, a Promise which will be resolved with + * the result (as an{@link LDEvaluationDetailTyped}). + */ + jsonVariationDetail( + key: string, + defaultValue: unknown, + ): Promise>; + + /** + * Builds an object that encapsulates the state of all feature flags for a given context. + * This includes the flag values and also metadata that can be used on the front end. This + * method does not send analytics events back to LaunchDarkly. + * + * The most common use case for this method is to bootstrap a set of client-side + * feature flags from a back-end service. Call the `toJSON()` method of the returned object + * to convert it to the data structure used by the client-side SDK. + * + * @param options + * Optional {@link LDFlagsStateOptions} to determine how the state is computed. + * @param callback + * A Node-style callback to receive the result (as an {@link LDFlagsState}). If omitted, you + * will receive a Promise instead. + * @returns + * If you provided a callback, then nothing. Otherwise, a Promise which will be resolved + * with the result as an {@link LDFlagsState}. + */ + allFlagsState( + options?: LDFlagsStateOptions, + callback?: (err: Error | null, res: LDFlagsState | null) => void, + ): Promise; +} diff --git a/packages/sdk/react/src/server/LDContextProvider.ts b/packages/sdk/react/src/server/LDContextProvider.ts new file mode 100644 index 000000000..fc9c1bc96 --- /dev/null +++ b/packages/sdk/react/src/server/LDContextProvider.ts @@ -0,0 +1,38 @@ +import { LDContext } from '@launchdarkly/js-server-sdk-common'; + +/** + * A provider for the LaunchDarkly context that can be used in + * the server components. + * + * @privateRemarks + * This interface is still under consideration and we will need to refine + * this when we start to implement some examples to see how it works in practice. + */ +export interface LDContextProvider { + /** + * getContext is used to determine the context that should be + * used for the request instance. This function will be called once + * per request. + * + * @remarks + * The reasons for this interface is that different frameworks may have + * different ways to determine the context from a request. + * + * @returns The LDContext for the request. + */ + getContext: () => LDContext; + + /** + * setContext is used to set the context that should be + * used for the request instance. This function will be called once + * per request. + * + * @remarks + * This is used to update the context that is associated with a + * browser session. This is optional and if not provided, then we will + * assume that the context is updated elsewhere. + * + * @param context The LDContext to set for the request. + */ + setContext?: (context: LDContext) => void; +} diff --git a/packages/sdk/react/src/server/LDOptions.ts b/packages/sdk/react/src/server/LDOptions.ts new file mode 100644 index 000000000..3c5a936d4 --- /dev/null +++ b/packages/sdk/react/src/server/LDOptions.ts @@ -0,0 +1,15 @@ +import { LDOptions as LDOptionsCommon } from '@launchdarkly/js-server-sdk-common'; + +import { LDContextProvider } from './LDContextProvider'; + +export interface LDReactServerOptions extends LDOptionsCommon { + /** + * A provider for the Launchdarkly context. + * + * @remarks + * This is left up to the developer to implement and will be used + * to do server side flag evalations. LDClient will not initialize + * if this is not provided. + */ + contextProvider: LDContextProvider; +} diff --git a/packages/sdk/react/src/server/index.ts b/packages/sdk/react/src/server/index.ts new file mode 100644 index 000000000..01ed2cc1e --- /dev/null +++ b/packages/sdk/react/src/server/index.ts @@ -0,0 +1,80 @@ +import { LDClient, LDFlagsStateOptions } from '@launchdarkly/js-server-sdk-common'; + +import { LDReactServerClient } from './LDClient'; +import { LDReactServerOptions } from './LDOptions'; + +export type * from './LDContextProvider'; +export type * from './LDOptions'; +export type * from './LDClient'; + +/** + * @experimental + * This function is experimental and may change in the future. + * + * Creates a restricted version of the common server client that is used for server side rendering. + * + * @param client The LaunchDarkly client. + * @param options The options for the React server client. + * @returns The React server client. The client is a restricted version of the common server client. + */ +export function createReactServerClient( + client: LDClient, + options: LDReactServerOptions, +): LDReactServerClient { + if (!options.contextProvider) { + throw new Error('contextProvider is required'); + } + + const { contextProvider } = options; + return { + variation: (key, defaultValue, callback) => { + const context = contextProvider.getContext(); + return client.variation(key, context, defaultValue, callback); + }, + variationDetail: (key, defaultValue, callback) => { + const context = contextProvider.getContext(); + return client.variationDetail(key, context, defaultValue, callback); + }, + migrationVariation: (key, defaultValue) => { + const context = contextProvider.getContext(); + return client.migrationVariation(key, context, defaultValue); + }, + boolVariation: (key, defaultValue) => { + const context = contextProvider.getContext(); + return client.boolVariation(key, context, defaultValue); + }, + numberVariation: (key, defaultValue) => { + const context = contextProvider.getContext(); + return client.numberVariation(key, context, defaultValue); + }, + stringVariation: (key, defaultValue) => { + const context = contextProvider.getContext(); + return client.stringVariation(key, context, defaultValue); + }, + jsonVariation: (key, defaultValue) => { + const context = contextProvider.getContext(); + return client.jsonVariation(key, context, defaultValue); + }, + boolVariationDetail: (key, defaultValue) => { + const context = contextProvider.getContext(); + return client.boolVariationDetail(key, context, defaultValue); + }, + numberVariationDetail: (key, defaultValue) => { + const context = contextProvider.getContext(); + return client.numberVariationDetail(key, context, defaultValue); + }, + stringVariationDetail: (key, defaultValue) => { + const context = contextProvider.getContext(); + return client.stringVariationDetail(key, context, defaultValue); + }, + jsonVariationDetail: (key, defaultValue) => { + const context = contextProvider.getContext(); + return client.jsonVariationDetail(key, context, defaultValue); + }, + initialized: () => client.initialized(), + allFlagsState: (allFlagsStateOptions: LDFlagsStateOptions, callback) => { + const context = contextProvider.getContext(); + return client.allFlagsState(context, allFlagsStateOptions, callback); + }, + }; +} diff --git a/packages/sdk/react/temp_docs/README.md b/packages/sdk/react/temp_docs/README.md new file mode 100644 index 000000000..2ad9ce83c --- /dev/null +++ b/packages/sdk/react/temp_docs/README.md @@ -0,0 +1,6 @@ +# Temporary documentation + +This documentation directory will exist until this SDK +is GA. + +For official documentation, please see https://launchdarkly.com/docs/home \ No newline at end of file diff --git a/packages/sdk/react/temp_docs/React-support.md b/packages/sdk/react/temp_docs/React-support.md new file mode 100644 index 000000000..cc41bbc9f --- /dev/null +++ b/packages/sdk/react/temp_docs/React-support.md @@ -0,0 +1,4 @@ +# React version support + +With the introduction of server components, we will narrow our official react support +to a minimum major version of 18. diff --git a/packages/sdk/react/temp_docs/creating-client-with-rsc.md b/packages/sdk/react/temp_docs/creating-client-with-rsc.md new file mode 100644 index 000000000..d511c957f --- /dev/null +++ b/packages/sdk/react/temp_docs/creating-client-with-rsc.md @@ -0,0 +1,88 @@ +# Creating a LaunchDarkly client with React Server Components + +> **Status:** The LaunchDarkly React SDK and RSC support are experimental. The APIs described in this document are not fully implemented and may change. This doc reflects intended design and usage. + +## Overview + +To use LaunchDarkly in an app that uses React Server Components (RSC), you create an **isomorphic client** via `createClient` from `@launchdarkly/react-sdk`. That client works in both Client Components and, when federated with a server client, in Server Components. + +The isomorphic client supports two modes: + +- **Client-only (default):** If you never call `useServerClient`, the client evaluates flags only on the client. This is the default and is sufficient for apps that do not need server-side flag evaluation. +- **Client + server:** After you call `useServerClient(serverClient)` on the isomorphic client, the same client can be used in React Server Components; server-side evaluation will use the server client you provided. + +## Why the server client is opt-in + +The RSC-capable server client is kept separate, and developers must explicitly call `useServerClient` in their client creation flow, for two reasons: + +1. **Flexibility:** We want to remain flexible about which server SDK can be used to drive RSC (e.g. Node vs edge, or future runtimes). The React SDK does not bundle or mandate a specific server SDK; it only expects a client that conforms to the `LDReactServerClient` interface. You can use the LaunchDarkly Node SDK, an edge SDK, or another implementation that matches that interface. + +2. **Ownership:** Lifecycle and management of the server SDK—creation, configuration, and disposal—are left to the developer. The React SDK only consumes a server client you provide. This keeps the React SDK focused on the client/isomorphic layer and keeps server-SDK choices in your application’s hands. + +## Entry points and types + +- **Main entry:** `createClient(clientSideID, context, options?)` from `@launchdarkly/react-sdk`. Returns an `LDIsomorphicClient`. Options are `LDIsomorphicOptions` (extends client options, e.g. `useCamelCaseFlagKeys`). + +- **Server entry:** `createReactServerClient(client, options)` from `@launchdarkly/react-sdk/server`. Accepts a standard LaunchDarkly server `LDClient` and `LDReactServerOptions` (which requires `contextProvider: LDContextProvider`). Returns an `LDReactServerClient`. + +## Intended flow (step-by-step) + +1. **Create the isomorphic client.** Call `createClient(clientSideID, initialContext, options)` to get an `LDIsomorphicClient`. This client can be used in Client Components for variations, `allFlags`, `identify`, and other client-side APIs. + +2. **Set up the server.** In your server environment, create a standard LaunchDarkly server client (using your SDK key and any server-SDK options). Implement `LDContextProvider` so that `getContext()` returns the LaunchDarkly context for the current request (e.g. from request, session, or cookies). Then call `createReactServerClient(serverClient, { contextProvider })` to get an `LDReactServerClient`. + +3. **Federate for RSC.** Call `isomorphicClient.useServerClient(reactServerClient)` (e.g. in a root layout or provider) so the same isomorphic client can be used in React Server Components. Without this step, only client-side flag evaluation is available. + +4. **Use the client.** Use the same isomorphic client in both Client Components (client-side evaluation) and Server Components (server-side evaluation via the federated server client). On the server, context is supplied per request by your `LDContextProvider`. + +## Context provider + +`LDContextProvider` is the bridge between your framework (e.g. Next.js App Router) and LaunchDarkly. It has a required `getContext()` that returns the `LDContext` for the current request. Optionally, `setContext(context)` can be used to update the context associated with the request or session. Implementation is application-specific: you might read the user from session, headers, or cookies and build an `LDContext` from that. + +## Code sketch (intended usage) + +The following is conceptual; implementations are not yet complete. + +```ts +// 1. Create the isomorphic client (e.g. in a shared module or root layout) +import { createClient } from '@launchdarkly/react-sdk'; + +const client = createClient(clientSideID, initialContext, options); +// client is an LDIsomorphicClient; use in Client Components as-is. +``` + +```ts +// 2. Server: create your server LDClient and context provider, then create the React server client +import { createReactServerClient } from '@launchdarkly/react-sdk/server'; +// import your server SDK's createClient / LDClient as needed + +const contextProvider = { + getContext: () => { /* return LDContext for this request, e.g. from session */ }, +}; +const reactServerClient = createReactServerClient(serverClient, { contextProvider }); +``` + +```tsx +// 3. Federate the isomorphic client with the server client (e.g. in root layout or provider) +client.useServerClient(reactServerClient); +// Now the same client can be used in Server Components. +``` + +> [!caution] +> Everything below is wildly hypothetical +```tsx +// 4a. Use in a Client Component +'use client'; +function ClientFeature() { + const enabled = client.boolVariation('my-flag', false); + return enabled ? : ; +} +``` + +```tsx +// 4b. Use in a Server Component (when useServerClient was called) +async function ServerFeature() { + const enabled = await client.boolVariation('my-flag', false); + return enabled ? : ; +} +``` diff --git a/packages/sdk/react/tsconfig.eslint.json b/packages/sdk/react/tsconfig.eslint.json new file mode 100644 index 000000000..8241f86c3 --- /dev/null +++ b/packages/sdk/react/tsconfig.eslint.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["/**/*.ts", "/**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/packages/sdk/react/tsconfig.json b/packages/sdk/react/tsconfig.json new file mode 100644 index 000000000..3a8b63285 --- /dev/null +++ b/packages/sdk/react/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "declaration": true, + "declarationMap": true, + "lib": ["es6", "dom"], + "module": "ESNext", + "moduleResolution": "node", + "noImplicitOverride": true, + "outDir": "dist", + "resolveJsonModule": true, + "rootDir": ".", + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "stripInternal": true, + "target": "ES2017", + "types": ["jest", "node", "react/canary"], + "jsx": "react" + }, + "exclude": ["**/*.test.ts", "dist", "node_modules", "__tests__", "examples/**"] +} diff --git a/packages/sdk/react/tsconfig.ref.json b/packages/sdk/react/tsconfig.ref.json new file mode 100644 index 000000000..34a1cb607 --- /dev/null +++ b/packages/sdk/react/tsconfig.ref.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*", "package.json"], + "compilerOptions": { + "composite": true + } +} diff --git a/packages/sdk/react/typedoc.json b/packages/sdk/react/typedoc.json new file mode 100644 index 000000000..7ac616b54 --- /dev/null +++ b/packages/sdk/react/typedoc.json @@ -0,0 +1,5 @@ +{ + "extends": ["../../../typedoc.base.json"], + "entryPoints": ["src/index.ts"], + "out": "docs" +}