launchdarkly / react-client-sdk

LaunchDarkly Client-side SDK for React.js
Other
85 stars 68 forks source link

Provide client instance to `asyncWithLDProvider` or expose client instance statically #56

Closed StevenLangbroek closed 2 years ago

StevenLangbroek commented 3 years ago

Is your feature request related to a problem? Please describe.

We have a (React) client application, which needs some configuration to be able to run. We're evaluating using LaunchDarkly for this, as it gives us not just runtime application-wide control, but we can also turn on specific behavior for users facing problems (e.g., increasing log levels or adding debugging tools). Our application has some bootstrapping logic, like so:


(async () => {
  const authentication = new Authentication(window.__COGNITO_CONFIG__);

  await authentication.restoreUser();

  const LDProvider = await asyncWithLDProvider({
    clientSideID: window.__LAUNCHDARKLY_CONFIG__.clientId,
    user: ldUserFromUserData(await authentication.getUser()),
  });

  const history = createBrowserHistory();

  initIntl(window.__PHRASE_CONFIG__);

  // this is a graphql client, `urql`
  const client = createClient({
    history,
    authentication,
    url: window.__GRAPHQL_CONFIG__.url,
  });

  // there's some more configuration inside the `Chrome` component

  render(
    <Chrome
      FeatureFlagProvider={LDProvider}
      authentication={authentication}
      client={client}
      history={history}
    />,
    document.getElementById('root'),
  );
})();

What we'd like to do is strip down the required "environment variables" down to Cognito & LaunchDarkly configuration, then read the rest off of LaunchDarkly, roughly like so:

(async () => {
  const authentication = new Authentication(window.__COGNITO_CONFIG__);

  await authentication.restoreUser();

  const LDProvider = await asyncWithLDProvider({
    clientSideID: window.__LAUNCHDARKLY_CONFIG__.clientId,
    user: ldUserFromUserData(await authentication.getUser()),
  });

  const history = createBrowserHistory();

  initIntl(LDProvider.flags.phraseConfig); // <-- where this would come from LD

  const client = createClient({
    history,
    authentication,
    url: LDProvider.flags.graphQLURL, // <-- also from LD
  });
})();

Describe the solution you'd like

I can see this working in 2 ways:

Would love to hear whether this is something you'd consider, and if so which approach has your preference.

Thanks 🙏

bwoskow-ld commented 3 years ago

Hi @StevenLangbroek,

As you point out the React SDK doesn't exactly support your requirements today.

Your first proposed solution would be a breaking change as the React SDK's asyncWithLDProvider function returns a React component, and you want to evaluate flags prior to React setting up and rendering the components. We'd rather not make this change.

Your second proposed solution may work - I'll discuss this further below.

But first, here's an alternative solution which might work for you. Our React SDK is largely based on our JS SDK -- the React SDK simply wraps the JS SDK to add some React-specific niceties while delegating all of the core LaunchDarkly functionality to the JS implementation. Given your use case, would it make sense for you to utilize the JS SDK instead of the React SDK? The main benefit of doing this is that you would be free to initialize and access LDClient at any time; see our JS SDK documentation for more information. The downside is that you would either lose or need to replicate the React-specific niceties introduced with our React SDK. This includes things like setting up a change listener and auto-camelCased flag keys. If you're primarily using LaunchDarkly for the initialization of your app, or if your app typically uses pageloads when rendering new content, then this solution is likely to be viable for you and can be utilized today.

Alternatively, if the above is not viable, what we can probably do is make some changes to initLDClient.ts to publicly export the LDClient so that you can instantiate and use it earlier. If we were to propose a change, in my opinion, that would be better than exporting just a snapshot of the flags at a specific point in time as LDClient can do more.

kalley commented 3 years ago

@bwoskow-ld Would it be possible to just add another Provider factory? We're in the middle of transitioning an app from regular js to react, but we're going to have multiple react roots as well as some old code that needs to be able to listen to flag changes. I hate to just copy and paste code you guys have already, but we could do it that way. What I'd like to see is something along the lines of:

const { flags, ldClient } = await initLDClient(user, options);
const LDProvider = makeProvider({ flags, ldClient, reactOptions });

Where, internally, it'd look almost exactly like asyncWithLDProvider, except that it wouldn't instantiate the client itself, but would handle the adding the useEffect. It could probably even be used from asyncWithLDProvider so that there isn't any duplicate code. Just thinking out loud. We may just end up doing it this way, but I really don't want us to miss out on good changes you all make in the future.

ghost commented 2 years ago

I'm in a similar boat - we have a legacy application that's using the Launch Darkly JavaScript library and we're introducing compartmentalized React components that will also be utilizing Launch Darkly. Since the legacy site already manages an instance of the LDClient, it would be nice to pass it into the compartmentalized React components to pass along to the initLDClient.ts, maybe via the reactOptions:LDReactOptions? Could that work?

The logic would just check to see if the LDClient is already initialized/ready and continue on as normal. I haven't dived into every nook and cranny in the code, but I think it would function ok. I might try forking the library to see how it goes 😊

XieX commented 2 years ago

Looks as though this was solved by the above PR, included in 2.24.0. Let us know if there's a use case that's not covered by being able to initialize the client and pass it into the React provider yourself.