Is your feature request related to a problem? Please describe.
There are three configcat JS libraries, and the advantage of the SSR one over Node.js or React isn't clear. In my case, I'm using Remix, and the user ID is entirely on the server within the cookie derived session. I need the React.js render to match on both the server and client side.
Describe the solution you'd like
I would like a set of React components that can take the ConfigCat values passed from a server route loader and initialize the context of the react components.
Describe alternatives you've considered
I considered the react library, but like I mentioned, the client-side doesn't have a copy of the user-id. Passing it would expose the app to unnecessary data leakage.
I considered the Node.js library, but I didn't see the differences spelled out. While I'm running Remix on Node.js, it could run on other JavaScript engines like Cloudflare Workers.
Using the SSR library to as documented doesn't provide a clear way to optionally render UI components in a consistent manner between the server and client. I need the server to get the flags for the logged-in user and then both use them when rendering to HTML and distribute the settings to the client so it can also use the values while rendering.
Additional context
I wrote two files to aid me in this:
configcat.server.ts
:
import type { SettingKeyValue } from "configcat-js-ssr";
import {
createConsoleLogger,
getClient,
LogLevel,
PollingMode,
User,
} from "configcat-js-ssr";
import { get_process_env } from "~/utils";
const sdkKey = get_process_env("CONFIGCAT_SDK_KEY");
const configCatClient = getClient(sdkKey, PollingMode.AutoPoll);
export async function getConfigCatValues(
identifier: string
): Promise<SettingKeyValue[]> {
const userObject = new User(identifier);
const settingValues = await configCatClient.getAllValuesAsync(userObject);
return settingValues;
}
and configcat.tsx
:
import type { ReactNode } from "react";
import { createContext, useContext } from "react";
import type { SettingValue } from "configcat-js-ssr";
import { getLogger } from "~/services/logging";
const logger = getLogger("configcat");
/**
* Remix loader compatible type for ConfigCat SettingKeyValue
*/
interface SettingKeyValue {
settingKey: string;
settingValue?: SettingValue;
}
const ConfigCatContext = createContext<SettingKeyValue[]>([]);
export function useConfigCat(): SettingKeyValue[] {
return useContext(ConfigCatContext);
}
export function useFeatureFlag<T extends SettingValue>(
key: string,
defaultValue: T
): T {
const typeofDefaultValue = typeof defaultValue;
if (
defaultValue != null &&
["boolean", "number", "string"].indexOf(typeofDefaultValue) === -1
) {
throw new TypeError(
`Invalid type for 'defaultValue': ${typeofDefaultValue}`
);
}
const context = useContext(ConfigCatContext);
const settingValue = context.find((x) => x.settingKey === key)?.settingValue;
if (settingValue == null) {
return defaultValue;
}
if (typeof settingValue === typeofDefaultValue) {
return settingValue as T;
} else {
console.error(
`typeof of setting value (${typeof settingValue}) for setting '${key}' does not match type of defaultValue (${typeofDefaultValue})`
);
}
return defaultValue;
}
export function ConfigCatProvider({
children,
value,
}: {
children?: ReactNode | undefined;
value: SettingKeyValue[];
}) {
return (
<ConfigCatContext.Provider value={value}>
{children}
</ConfigCatContext.Provider>
);
}
And use like this:
export async function loader({ request }: LoaderArgs) {
const session = await getSession(request.headers.get("Cookie"));
const user = await requireAuthenticatedUser(request, session);
const configCatValues = await getConfigCatValues(user.profile.username);
const data: LoaderData = {
configCatValues,
};
return json(data);
}
export default function Index() {
const { configCatValues } = useLoaderData<typeof loader>();
}
return (
<ConfigCatProvider value={configCatValues}>
<Outlet />
</ConfigCatProvider>
);
}