-
Notifications
You must be signed in to change notification settings - Fork 36
/
Copy pathFlagProvider.tsx
127 lines (112 loc) · 3.81 KB
/
FlagProvider.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
/** @format */
import React, { type FC, type PropsWithChildren, useEffect, useMemo, useState } from 'react';
import { type IConfig, UnleashClient } from 'unleash-proxy-client';
import FlagContext, { type IFlagContextValue } from './FlagContext';
export interface IFlagProvider {
config?: IConfig;
unleashClient?: UnleashClient;
startClient?: boolean;
stopClient?: boolean;
}
const offlineConfig: IConfig = {
bootstrap: [],
disableRefresh: true,
disableMetrics: true,
url: 'http://localhost',
appName: 'offline',
clientKey: 'not-used',
};
// save startTransition as var to avoid webpack analysis (https://github.com/webpack/webpack/issues/14814)
const _startTransition = 'startTransition';
// fallback for React <18 which doesn't support startTransition
const startTransition = React[_startTransition] || (fn => fn());
const FlagProvider: FC<PropsWithChildren<IFlagProvider>> = ({
config: customConfig,
children,
unleashClient,
startClient = true,
stopClient = true,
}) => {
const config = customConfig || offlineConfig;
const client = React.useRef<UnleashClient>(
unleashClient || new UnleashClient(config)
);
const [flagsReady, setFlagsReady] = React.useState(
Boolean(
unleashClient
? (customConfig?.bootstrap && customConfig?.bootstrapOverride !== false) || unleashClient.isReady?.()
: config.bootstrap && config.bootstrapOverride !== false
)
);
const [flagsError, setFlagsError] = useState(client.current.getError?.() || null);
useEffect(() => {
if (!config && !unleashClient) {
console.error(
`You must provide either a config or an unleash client to the flag provider.
If you are initializing the client in useEffect, you can avoid this warning
by checking if the client exists before rendering.`
);
}
const errorCallback = (e: any) => {
startTransition(() => {
setFlagsError((currentError: any) => currentError || e);
});
};
const clearErrorCallback = (e: any) => {
startTransition(() => {
setFlagsError(null);
});
}
let timeout: ReturnType<typeof setTimeout> | null = null;
const readyCallback = () => {
// wait for flags to resolve after useFlag gets the same event
timeout = setTimeout(() => {
startTransition(() => {
setFlagsReady(true);
});
}, 0);
};
client.current.on('ready', readyCallback);
client.current.on('error', errorCallback);
client.current.on('recovered', clearErrorCallback);
if (startClient) {
// defensively stop the client first
client.current.stop();
// start the client
client.current.start();
}
// stop unleash client on unmount
return function cleanup() {
if (client.current) {
client.current.off('error', errorCallback);
client.current.off('ready', readyCallback);
client.current.off('recovered', clearErrorCallback);
if (stopClient) {
client.current.stop();
}
}
if (timeout) {
clearTimeout(timeout);
}
};
}, []);
const context = useMemo<IFlagContextValue>(
() => ({
on: ((event, callback, ctx) => client.current.on(event, callback, ctx)) as IFlagContextValue['on'],
off: ((event, callback) => client.current.off(event, callback)) as IFlagContextValue['off'],
updateContext: async (context) => await client.current.updateContext(context),
isEnabled: (toggleName) => client.current.isEnabled(toggleName),
getVariant: (toggleName) => client.current.getVariant(toggleName),
client: client.current,
flagsReady,
flagsError,
setFlagsReady,
setFlagsError,
}),
[flagsReady, flagsError]
);
return (
<FlagContext.Provider value={context}>{children}</FlagContext.Provider>
);
};
export default FlagProvider;