@featbit/react-client-sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +435 -0
- package/dist/asyncWithFbProvider.d.ts +29 -0
- package/dist/asyncWithFbProvider.js +129 -0
- package/dist/asyncWithFbProvider.js.map +1 -0
- package/dist/context.d.ts +15 -0
- package/dist/context.js +6 -0
- package/dist/context.js.map +1 -0
- package/dist/getFlagsProxy.d.ts +6 -0
- package/dist/getFlagsProxy.js +56 -0
- package/dist/getFlagsProxy.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/initClient.d.ts +12 -0
- package/dist/initClient.js +84 -0
- package/dist/initClient.js.map +1 -0
- package/dist/provider.d.ts +39 -0
- package/dist/provider.js +201 -0
- package/dist/provider.js.map +1 -0
- package/dist/types.d.ts +90 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/useFbClient.d.ts +11 -0
- package/dist/useFbClient.js +18 -0
- package/dist/useFbClient.js.map +1 -0
- package/dist/useFlags.d.ts +11 -0
- package/dist/useFlags.js +16 -0
- package/dist/useFlags.js.map +1 -0
- package/dist/utils.d.ts +27 -0
- package/dist/utils.js +78 -0
- package/dist/utils.js.map +1 -0
- package/dist/withFbConsumer.d.ts +38 -0
- package/dist/withFbConsumer.js +36 -0
- package/dist/withFbConsumer.js.map +1 -0
- package/dist/withFbProvider.d.ts +25 -0
- package/dist/withFbProvider.js +50 -0
- package/dist/withFbProvider.js.map +1 -0
- package/package.json +57 -0
- package/src/asyncWithFbProvider.tsx +72 -0
- package/src/context.ts +24 -0
- package/src/getFlagsProxy.ts +82 -0
- package/src/index.ts +21 -0
- package/src/initClient.ts +26 -0
- package/src/provider.tsx +136 -0
- package/src/types.ts +103 -0
- package/src/useFbClient.ts +21 -0
- package/src/useFlags.ts +19 -0
- package/src/utils.ts +39 -0
- package/src/withFbConsumer.tsx +58 -0
- package/src/withFbProvider.tsx +49 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { defaultReactOptions, FbReactOptions, FlagKeyMap, IFlagSet } from "./types";
|
|
2
|
+
import { IFbClient } from "@featbit/js-client-sdk";
|
|
3
|
+
import camelCase from "lodash.camelcase";
|
|
4
|
+
|
|
5
|
+
export default function getFlagsProxy(
|
|
6
|
+
fbClient: IFbClient,
|
|
7
|
+
bootstrapFlags: IFlagSet,
|
|
8
|
+
fetchedFlags: IFlagSet,
|
|
9
|
+
reactOptions: FbReactOptions = defaultReactOptions
|
|
10
|
+
): { flags: IFlagSet; flagKeyMap: FlagKeyMap } {
|
|
11
|
+
const { useCamelCaseFlagKeys = false, sendEventsOnFlagRead = true } = reactOptions;
|
|
12
|
+
const [flags, flagKeyMap = {}] = useCamelCaseFlagKeys ? getCamelizedKeysAndFlagMap(fetchedFlags) : [fetchedFlags];
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
flags: toFlagsProxy(fbClient, bootstrapFlags, flags, flagKeyMap, fetchedFlags, useCamelCaseFlagKeys, sendEventsOnFlagRead),
|
|
16
|
+
flagKeyMap,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getCamelizedKeysAndFlagMap(rawFlags: IFlagSet) {
|
|
21
|
+
const flags: IFlagSet = {};
|
|
22
|
+
const flagKeyMap: FlagKeyMap = {};
|
|
23
|
+
for (const rawFlagKey in rawFlags) {
|
|
24
|
+
// Exclude system keys
|
|
25
|
+
if (rawFlagKey.indexOf('$') === 0) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const camelKey = camelCase(rawFlagKey);
|
|
29
|
+
flags[camelKey] = rawFlags[rawFlagKey];
|
|
30
|
+
flagKeyMap[camelKey] = rawFlagKey;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return [flags, flagKeyMap];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function hasFlag(flags: IFlagSet, flagKey: string) {
|
|
37
|
+
return Object.prototype.hasOwnProperty.call(flags, flagKey);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function toFlagsProxy(
|
|
41
|
+
fbClient: IFbClient,
|
|
42
|
+
bootstrapFlags: IFlagSet,
|
|
43
|
+
flags: IFlagSet,
|
|
44
|
+
flagKeyMap: FlagKeyMap,
|
|
45
|
+
flagsWithRawFlagKeys: IFlagSet,
|
|
46
|
+
useCamelCaseFlagKeys: boolean,
|
|
47
|
+
sendEventsOnFlagRead: boolean
|
|
48
|
+
): IFlagSet {
|
|
49
|
+
return new Proxy(flags, {
|
|
50
|
+
get: (target, prop, receiver) => {
|
|
51
|
+
const currentValue = Reflect.get(target, prop, receiver) || flagsWithRawFlagKeys[prop as string]
|
|
52
|
+
|
|
53
|
+
// check if flag key exists as camelCase or original case
|
|
54
|
+
const validFlagKey =
|
|
55
|
+
hasFlag(flagKeyMap, prop as string) || hasFlag(target, prop as string) || hasFlag(flagsWithRawFlagKeys, prop as string);
|
|
56
|
+
|
|
57
|
+
if (!validFlagKey && hasFlag(bootstrapFlags, prop as string)) {
|
|
58
|
+
return bootstrapFlags[prop as string];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// only process flag keys and ignore symbols and native Object functions
|
|
62
|
+
if (typeof prop === 'symbol' || !validFlagKey) {
|
|
63
|
+
return currentValue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (useCamelCaseFlagKeys && prop !== camelCase(prop as string)) {
|
|
67
|
+
console.warn(`You're attempting to access a flag with its original keyId: ${prop as string}, even though useCamelCaseFlagKeys is set to true.`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (currentValue === undefined) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!sendEventsOnFlagRead) {
|
|
75
|
+
return currentValue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const pristineFlagKey = useCamelCaseFlagKeys ? (flagKeyMap[prop] || prop) : prop;
|
|
79
|
+
return fbClient.variation(pristineFlagKey, currentValue);
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import asyncWithFbProvider from './asyncWithFbProvider';
|
|
2
|
+
import context from './context';
|
|
3
|
+
import FbProvider from './provider';
|
|
4
|
+
import useFlags from './useFlags';
|
|
5
|
+
import useFbClient from './useFbClient';
|
|
6
|
+
import { camelCaseKeys } from './utils';
|
|
7
|
+
import withFbConsumer from './withFbConsumer';
|
|
8
|
+
import withFbProvider from './withFbProvider';
|
|
9
|
+
|
|
10
|
+
export * from './types';
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
FbProvider,
|
|
14
|
+
context,
|
|
15
|
+
asyncWithFbProvider,
|
|
16
|
+
camelCaseKeys,
|
|
17
|
+
useFlags,
|
|
18
|
+
useFbClient,
|
|
19
|
+
withFbProvider,
|
|
20
|
+
withFbConsumer
|
|
21
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { AllFlagsFbClient, defaultReactOptions, FbReactOptions } from './types';
|
|
2
|
+
import { FbClientBuilder, IOptions } from '@featbit/js-client-sdk';
|
|
3
|
+
import { fetchFlags } from "./utils";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Internal function to initialize the `@featbit/js-client-sdk`.
|
|
7
|
+
*
|
|
8
|
+
* @param reactOptions Initialization options for the FeatBit React SDK
|
|
9
|
+
* @param options @featbit/js-client-sdk initialization options
|
|
10
|
+
*
|
|
11
|
+
* @see `ProviderConfig` for more details about the parameters
|
|
12
|
+
* @return An initialized client and flags
|
|
13
|
+
*/
|
|
14
|
+
export const initClient = async (
|
|
15
|
+
reactOptions: FbReactOptions = defaultReactOptions,
|
|
16
|
+
options: IOptions = {}
|
|
17
|
+
): Promise<AllFlagsFbClient> => {
|
|
18
|
+
const fbClient = new FbClientBuilder({...options}).build();
|
|
19
|
+
|
|
20
|
+
return new Promise<AllFlagsFbClient>((resolve) => {
|
|
21
|
+
fbClient.on('ready', async () => {
|
|
22
|
+
const flags = await fetchFlags(fbClient);
|
|
23
|
+
resolve({flags, fbClient});
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
};
|
package/src/provider.tsx
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { EnhancedComponent, ProviderConfig, defaultReactOptions, IFlagSet } from './types';
|
|
3
|
+
import { Provider, FbContext } from './context';
|
|
4
|
+
import { camelCaseKeys, fetchFlags } from "./utils";
|
|
5
|
+
import { initClient } from './initClient';
|
|
6
|
+
import { IFbClient } from '@featbit/js-client-sdk';
|
|
7
|
+
import getFlagsProxy from "./getFlagsProxy";
|
|
8
|
+
|
|
9
|
+
interface FbHocState extends FbContext {
|
|
10
|
+
unproxiedFlags: IFlagSet;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The `FbProvider` is a component which accepts a config object which is used to
|
|
15
|
+
* initialize `@featbit/js-client-sdk`.
|
|
16
|
+
*
|
|
17
|
+
* This Provider does three things:
|
|
18
|
+
* - It initializes the FeatBit instance by calling `@featbit/js-client-sdk` init on `componentDidMount`
|
|
19
|
+
* - It saves all flags and the FeatBit instance in the context API
|
|
20
|
+
* - It subscribes to flag changes and propagate them through the context API
|
|
21
|
+
*
|
|
22
|
+
* Because the `@featbit/js-client-sdk` is only initialized on `componentDidMount`, your flags and the
|
|
23
|
+
* FeatBit are only available after your app has mounted. This can result in a flicker due to flag changes at
|
|
24
|
+
* startup time.
|
|
25
|
+
*
|
|
26
|
+
* This component can be used as a standalone provider. However, be mindful to only include the component once
|
|
27
|
+
* within your application. This provider is used inside the `withFbProviderHOC` and can be used instead to initialize
|
|
28
|
+
* the `@featbit/js-client-sdk`. For async initialization, check out the `asyncWithFbProvider` function
|
|
29
|
+
*/
|
|
30
|
+
class FbProvider extends React.Component<ProviderConfig, FbHocState> implements EnhancedComponent {
|
|
31
|
+
readonly state: Readonly<FbHocState>;
|
|
32
|
+
bootstrapFlags: IFlagSet;
|
|
33
|
+
|
|
34
|
+
constructor(props: ProviderConfig) {
|
|
35
|
+
super(props);
|
|
36
|
+
|
|
37
|
+
const {options} = props;
|
|
38
|
+
this.bootstrapFlags = (options?.bootstrap || []).reduce((acc: {[key: string]: string}, flag: any) => {
|
|
39
|
+
acc[flag.id] = flag.variation;
|
|
40
|
+
return acc;
|
|
41
|
+
}, {} as {[key: string]: string});;
|
|
42
|
+
|
|
43
|
+
this.state = {
|
|
44
|
+
flags: {},
|
|
45
|
+
unproxiedFlags: {},
|
|
46
|
+
flagKeyMap: {},
|
|
47
|
+
fbClient: undefined,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
if (options?.bootstrap && options?.bootstrap.length > 0) {
|
|
51
|
+
const {useCamelCaseFlagKeys} = this.getReactOptions();
|
|
52
|
+
const flags = useCamelCaseFlagKeys ? camelCaseKeys(this.bootstrapFlags) : this.bootstrapFlags;
|
|
53
|
+
this.state = {
|
|
54
|
+
flags,
|
|
55
|
+
unproxiedFlags: flags,
|
|
56
|
+
flagKeyMap: {},
|
|
57
|
+
fbClient: undefined,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
getReactOptions = () => ({...defaultReactOptions, ...this.props.reactOptions});
|
|
63
|
+
|
|
64
|
+
subscribeToChanges = (fbClient: IFbClient) => {
|
|
65
|
+
fbClient.on('update', (changedKeys: string[]) => {
|
|
66
|
+
const updates: IFlagSet = changedKeys.reduce((acc, key) => {
|
|
67
|
+
acc[key] = fbClient.variation(key, '');
|
|
68
|
+
return acc;
|
|
69
|
+
}, {} as IFlagSet);
|
|
70
|
+
|
|
71
|
+
const unproxiedFlags = {
|
|
72
|
+
...this.state.unproxiedFlags,
|
|
73
|
+
...updates,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
if (Object.keys(updates).length > 0) {
|
|
77
|
+
this.setState({
|
|
78
|
+
unproxiedFlags,
|
|
79
|
+
...getFlagsProxy(fbClient, this.bootstrapFlags, unproxiedFlags, this.getReactOptions())
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
init = async () => {
|
|
86
|
+
const {options} = this.props;
|
|
87
|
+
let client: IFbClient = this.props.fbClient!;
|
|
88
|
+
const reactOptions = this.getReactOptions();
|
|
89
|
+
let unproxiedFlags;
|
|
90
|
+
if (client) {
|
|
91
|
+
unproxiedFlags = await fetchFlags(client);
|
|
92
|
+
} else {
|
|
93
|
+
const initialisedOutput = await initClient(reactOptions, options);
|
|
94
|
+
unproxiedFlags = initialisedOutput.flags;
|
|
95
|
+
client = initialisedOutput.fbClient!;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.setState({
|
|
99
|
+
unproxiedFlags,
|
|
100
|
+
...getFlagsProxy(client, this.bootstrapFlags, unproxiedFlags, reactOptions),
|
|
101
|
+
fbClient: client
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
this.subscribeToChanges(client);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
async componentDidMount() {
|
|
108
|
+
const {options, deferInitialization} = this.props;
|
|
109
|
+
if (deferInitialization && !options) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
await this.init();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async componentDidUpdate(prevProps: ProviderConfig) {
|
|
117
|
+
const {options, deferInitialization} = this.props;
|
|
118
|
+
const userJustLoaded = !prevProps.options?.user && options?.user;
|
|
119
|
+
if (deferInitialization && userJustLoaded) {
|
|
120
|
+
await this.init();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
render() {
|
|
125
|
+
const {flags, flagKeyMap, fbClient} = this.state;
|
|
126
|
+
|
|
127
|
+
// Conditional rendering when fbClient is null
|
|
128
|
+
if (fbClient === undefined) {
|
|
129
|
+
return null; // or Loading Indicator or any other placeholder
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return <Provider value={{ flags, flagKeyMap, fbClient }}>{ this.props.children }</Provider>;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export default FbProvider;
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { IFbClient, IOptions, FlagValue } from '@featbit/js-client-sdk';
|
|
2
|
+
|
|
3
|
+
export interface IFlagSet {
|
|
4
|
+
[key: string]: FlagValue;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Initialization options for the FeatBit React SDK. These are in addition to the options exposed
|
|
9
|
+
* by [[IOption]] which are common to both the JavaScript and React SDKs.
|
|
10
|
+
*/
|
|
11
|
+
export interface FbReactOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Whether the React SDK should transform flag keys into camel-cased format.
|
|
14
|
+
* Using camel-cased flag keys allow for easier use as prop values, however,
|
|
15
|
+
* these keys won't directly match the flag keys as known to LaunchDarkly.
|
|
16
|
+
* Consequently, flag key collisions may be possible and the Code References feature
|
|
17
|
+
* will not function properly.
|
|
18
|
+
*
|
|
19
|
+
* This is false by default, if set to true, keys will automatically be converted to camel-case.
|
|
20
|
+
*/
|
|
21
|
+
useCamelCaseFlagKeys?: boolean;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Whether to send flag evaluation events when a flag is read from the `flags` object
|
|
25
|
+
* returned by the `useFlags` hook. This is true by default, meaning flag evaluation
|
|
26
|
+
* events will be sent by default.
|
|
27
|
+
*/
|
|
28
|
+
sendEventsOnFlagRead?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Contains default values for the `reactOptions` object.
|
|
33
|
+
*/
|
|
34
|
+
export const defaultReactOptions = {useCamelCaseFlagKeys: false, sendEventsOnFlagRead: true};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Configuration object used to initialise FeatBit's JS client.
|
|
38
|
+
*/
|
|
39
|
+
export interface ProviderConfig {
|
|
40
|
+
/**
|
|
41
|
+
* If set to true, the FeatBit will not be initialized until the option prop has been defined.
|
|
42
|
+
*/
|
|
43
|
+
deferInitialization?: boolean;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* FeatBit initialization options. These options are common between FeatBit's JavaScript and React SDKs.
|
|
47
|
+
*/
|
|
48
|
+
options?: IOptions;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Additional initialization options specific to the React SDK.
|
|
52
|
+
*
|
|
53
|
+
* @see options
|
|
54
|
+
*/
|
|
55
|
+
reactOptions?: FbReactOptions;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Optionally, the FB can be initialised outside the provider
|
|
59
|
+
* and passed in, instead of being initialised by the provider.
|
|
60
|
+
* Note: it should only be passed in when it has emitted the 'ready'
|
|
61
|
+
* event, to ensure that the flags are properly set.
|
|
62
|
+
*/
|
|
63
|
+
fbClient?: IFbClient;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The return type of withFbProvider HOC. Exported for testing purposes only.
|
|
68
|
+
*
|
|
69
|
+
* @ignore
|
|
70
|
+
*/
|
|
71
|
+
export interface EnhancedComponent extends React.Component {
|
|
72
|
+
subscribeToChanges(fbClient: IFbClient): void;
|
|
73
|
+
|
|
74
|
+
// tslint:disable-next-line:invalid-void
|
|
75
|
+
componentDidMount(): Promise<void>;
|
|
76
|
+
|
|
77
|
+
// tslint:disable-next-line:invalid-void
|
|
78
|
+
componentDidUpdate(prevProps: ProviderConfig): Promise<void>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Return type of `initClient`.
|
|
83
|
+
*/
|
|
84
|
+
export interface AllFlagsFbClient {
|
|
85
|
+
/**
|
|
86
|
+
* Contains all flags from FeatBit.
|
|
87
|
+
*/
|
|
88
|
+
flags: IFlagSet;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* An instance of `FB` from the FeatBit JS SDK.
|
|
92
|
+
*/
|
|
93
|
+
fbClient: IFbClient;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Map of camelized flag keys to original unmodified flag keys.
|
|
98
|
+
*/
|
|
99
|
+
export interface FlagKeyMap {
|
|
100
|
+
[camelCasedKey: string]: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export * from '@featbit/js-client-sdk';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import context, { FbContext } from './context';
|
|
3
|
+
import { IFbClient } from "@featbit/js-client-sdk";
|
|
4
|
+
|
|
5
|
+
// tslint:disable:max-line-length
|
|
6
|
+
/**
|
|
7
|
+
* `useFbClient` is a custom hook which returns the underlying [FeatBit JavaScript SDK client object](https://github.com/featbit/featbit-js-client-sdk).
|
|
8
|
+
* Like the `useFlags` custom hook, `useFbClient` also uses the `useContext` primitive to access the FeatBit
|
|
9
|
+
* context set up by `withFbProvider`. You will still need to use the `withFbProvider` HOC
|
|
10
|
+
* to initialise the react sdk to use this custom hook.
|
|
11
|
+
*
|
|
12
|
+
* @return The `@featbit/js-client-sdk` `FB` object
|
|
13
|
+
*/
|
|
14
|
+
// tslint:enable:max-line-length
|
|
15
|
+
const useFbClient = (): IFbClient => {
|
|
16
|
+
const { fbClient} = useContext<FbContext>(context);
|
|
17
|
+
|
|
18
|
+
return fbClient!;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default useFbClient;
|
package/src/useFlags.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import context, { FbContext } from './context';
|
|
3
|
+
import { IFlagSet } from "./types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `useFlags` is a custom hook which returns all feature flags. It uses the `useContext` primitive
|
|
7
|
+
* to access the FeatBit context set up by `withFbProvider`. As such you will still need to
|
|
8
|
+
* use the `withFbProvider` HOC at the root of your app to initialize the React SDK and populate the
|
|
9
|
+
* context with `fbClient` and your flags.
|
|
10
|
+
*
|
|
11
|
+
* @return All the feature flags configured in FeatBit
|
|
12
|
+
*/
|
|
13
|
+
const useFlags = <T extends IFlagSet = IFlagSet>(): T => {
|
|
14
|
+
const {flags} = useContext<FbContext>(context);
|
|
15
|
+
|
|
16
|
+
return flags as T;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default useFlags;
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { IEvalDetail, IFbClient } from '@featbit/js-client-sdk';
|
|
2
|
+
import camelCase from 'lodash.camelcase';
|
|
3
|
+
import { IFlagSet } from "./types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Transforms a set of flags so that their keys are camelCased. This function ignores
|
|
7
|
+
* flag keys which start with `$`.
|
|
8
|
+
*
|
|
9
|
+
* @param rawFlags A mapping of flag keys and their values
|
|
10
|
+
* @return A transformed `IFeatureFlagSet` with camelCased flag keys
|
|
11
|
+
*/
|
|
12
|
+
export const camelCaseKeys = (rawFlags: IFlagSet) => {
|
|
13
|
+
const flags: IFlagSet = {};
|
|
14
|
+
for (const rawFlag in rawFlags) {
|
|
15
|
+
// Exclude system keys
|
|
16
|
+
if (rawFlag.indexOf('$') !== 0) {
|
|
17
|
+
flags[camelCase(rawFlag)] = rawFlags[rawFlag]; // tslint:disable-line:no-unsafe-any
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return flags;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Retrieves flag values.
|
|
26
|
+
*
|
|
27
|
+
* @param fbClient FeatBit client
|
|
28
|
+
*
|
|
29
|
+
* @returns an `IFeatureFlagSet` with the current flag values from FeatBit
|
|
30
|
+
*/
|
|
31
|
+
export const fetchFlags = async (
|
|
32
|
+
fbClient: IFbClient
|
|
33
|
+
) => {
|
|
34
|
+
const evalDetails: IEvalDetail<string>[] = await fbClient.getAllVariations();
|
|
35
|
+
|
|
36
|
+
return evalDetails.map(({flagKey, value}) => ({[flagKey]: value}));
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default {camelCaseKeys, fetchFlags};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Consumer, FbContext } from './context';
|
|
3
|
+
import { IFbClient } from '@featbit/js-client-sdk';
|
|
4
|
+
import { IFlagSet } from "./types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Controls the props the wrapped component receives from the `FbConsumer` HOC.
|
|
8
|
+
*/
|
|
9
|
+
export interface ConsumerOptions {
|
|
10
|
+
/**
|
|
11
|
+
* If true then the wrapped component only receives the `fbClient` instance
|
|
12
|
+
* and nothing else.
|
|
13
|
+
*/
|
|
14
|
+
clientOnly: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The possible props the wrapped component can receive from the `FbConsumer` HOC.
|
|
19
|
+
*/
|
|
20
|
+
export interface FbProps {
|
|
21
|
+
/**
|
|
22
|
+
* A map of feature flags from their keys to their values.
|
|
23
|
+
* Keys are camelCased using `lodash.camelcase`.
|
|
24
|
+
*/
|
|
25
|
+
flags?: IFlagSet;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* An instance of `FB` from the FeatBit JS Client SDK (`@featbit/js-client-sdk`)
|
|
29
|
+
*/
|
|
30
|
+
fbClient?: IFbClient;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* withFbConsumer is a function which accepts an optional options object and returns a function
|
|
35
|
+
* which accepts your React component. This function returns a HOC with flags
|
|
36
|
+
* and the FB instance injected via props.
|
|
37
|
+
*
|
|
38
|
+
* @param options - If you need only the `fbClient` instance and not flags, then set `{ clientOnly: true }`
|
|
39
|
+
* to only pass the fbClient prop to your component. Defaults to `{ clientOnly: false }`.
|
|
40
|
+
* @return A HOC with flags and the `fbClient` instance injected via props
|
|
41
|
+
*/
|
|
42
|
+
function withFbConsumer(options: ConsumerOptions = {clientOnly: false}) {
|
|
43
|
+
return function withFbConsumerHoc<P>(WrappedComponent: React.ComponentType<P & FbProps>) {
|
|
44
|
+
return (props: P) => (
|
|
45
|
+
<Consumer>
|
|
46
|
+
{ ({flags, fbClient}: FbContext) => {
|
|
47
|
+
if (options.clientOnly) {
|
|
48
|
+
return <WrappedComponent fbClient={ fbClient } { ...props } />;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return <WrappedComponent flags={ flags } fbClient={ fbClient } { ...props } />;
|
|
52
|
+
} }
|
|
53
|
+
</Consumer>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export default withFbConsumer;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { defaultReactOptions, ProviderConfig } from './types';
|
|
3
|
+
import FbProvider from './provider';
|
|
4
|
+
import hoistNonReactStatics from 'hoist-non-react-statics';
|
|
5
|
+
import IntrinsicAttributes = React.JSX.IntrinsicAttributes;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `withFbProvider` is a function which accepts a config object which is used to
|
|
9
|
+
* initialize `@featbit/js-client-sdk`.
|
|
10
|
+
*
|
|
11
|
+
* This HOC handles passing configuration to the `FbProvider`, which does the following:
|
|
12
|
+
* - It initializes the fbClient instance by calling `@featbit/js-client-sdk` init on `componentDidMount`
|
|
13
|
+
* - It saves all flags and the fbClient instance in the context API
|
|
14
|
+
* - It subscribes to flag changes and propagate them through the context API
|
|
15
|
+
*
|
|
16
|
+
* The difference between `withFbProvider` and `asyncWithFbProvider` is that `withFbProvider` initializes
|
|
17
|
+
* `@featbit/js-client-sdk` at `componentDidMount`. This means your flags and the fbClient are only available after
|
|
18
|
+
* your app has mounted. This can result in a flicker due to flag changes at startup time.
|
|
19
|
+
*
|
|
20
|
+
* `asyncWithFbProvider` initializes `@featbit/js-client-sdk` at the entry point of your app prior to render.
|
|
21
|
+
* This means that your flags and the fbClient are ready at the beginning of your app. This ensures your app does not
|
|
22
|
+
* flicker due to flag changes at startup time.
|
|
23
|
+
*
|
|
24
|
+
* @param config - The configuration used to initialize FeatBit JS Client SDK
|
|
25
|
+
* @return A function which accepts your root React component and returns a HOC
|
|
26
|
+
*/
|
|
27
|
+
export function withFbProvider<T extends IntrinsicAttributes = {}>(
|
|
28
|
+
config: ProviderConfig,
|
|
29
|
+
): (WrappedComponent: React.ComponentType<T>) => any {
|
|
30
|
+
return function withFbProviderHoc(WrappedComponent: React.ComponentType<T>): any {
|
|
31
|
+
const {reactOptions: userReactOptions} = config;
|
|
32
|
+
const reactOptions = {...defaultReactOptions, ...userReactOptions};
|
|
33
|
+
const providerProps = {...config, reactOptions};
|
|
34
|
+
|
|
35
|
+
function HoistedComponent(props: T) {
|
|
36
|
+
return (
|
|
37
|
+
<FbProvider { ...providerProps }>
|
|
38
|
+
<WrappedComponent { ...props } />
|
|
39
|
+
</FbProvider>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
hoistNonReactStatics(HoistedComponent, WrappedComponent);
|
|
44
|
+
|
|
45
|
+
return HoistedComponent;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default withFbProvider;
|