configcat / js-ssr-sdk

ConfigCat SDK for JavaScript Server Side Rendered applications like NuxtJS. ConfigCat is a hosted feature flag service: https://configcat.com. Manage feature toggles across frontend, backend, mobile, desktop apps. Alternative to LaunchDarkly. Management app + feature flag SDKs.
https://configcat.com/docs/sdk-reference/js-ssr
MIT License
12 stars 6 forks source link

Integrate SSR with React #69

Open ngbrown opened 1 year ago

ngbrown commented 1 year ago

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>
  );
}
laliconfigcat commented 1 year ago

Hello @ngbrown ,

The difference between the SSR, the JS, the React and the Node SDK is basically http request handling and cache handling.

The JS SDK uses XMLHttpRequest to fetch the config.json from our servers and a localstorage+inmemory cache implementation by default. The React SDK is basically the same as the JS SDK - XMLHttpRequest + localstorage+inmemory cache but with React features. The Node SDK uses the built-in http+https+tunnel packages to fetch the config.json and an inmemory cache by default. The SSR SDK uses axios which is a mixture of the above, XMLHttpRequest on the client-side, and http+https+tunnel on the server-side. The cache is localstorage+inmemory for client-side and inmemory for server side by default.

So the SSR SDK can work in client-side and server-side scenarios too, but it wasn't designed to provide a bridge between server-side and client-side. If you only want to use the ConfigCat SDK on the server side, i'd recommend going with the Node SDK for now (if possible), and provide the necessary data to the frontend - e.g. the way you mentioned.

I hope I could help.

Cheers, Lali

ngbrown commented 1 year ago

@laliconfigcat Thanks for the response and the clarification on the differences between SSR and Node.js on the server side.

I hope this provides some insperation for what others might want a bridge between server-side and client-side or future guidance on the ConfigCat libraries. This SSR library seems to be the most reasonable place to handle this situation because that's where developers would most likely want to bridge the two. To avoid a React hydration error on the client side, we can't wait for an Async http call to finish.

laliconfigcat commented 1 year ago

We recently introduced synchronous feature flag evaluation in our SDKS with the help of snapshots. Could that help you in the hydration error on client-side? https://configcat.com/docs/sdk-reference/js-ssr/#snapshots-and-synchronous-feature-flag-evaluation

ngbrown commented 1 year ago

@laliconfigcat I saw the documentation for the snapshot, but I also saw that getting a specific value for a specific user was after the snapshot. I mentioned that the user identification value was on the server-side, so I don't think the snapshot would prove useful in this case.

getAllValuesAsync on the other hand takes the User object as a parameter and outputs all the possible values that the render might need, whether it is server-side or client-side.

github-actions[bot] commented 1 month ago

This issue is marked stale because it has no activity in the last 3 weeks. The issue will be closed in one week. Please remove the stale flag to keep it open.