Skip to content

Feedback + Suggestions for React Interface #631

Closed as not planned
Closed as not planned
@leggomuhgreggo

Description

@leggomuhgreggo

Overview

Initially, I intended to suggest aligning the SDK more closely with the existing React SDK's API. However, while drafting this issue, I realized there might not be enough commonality for there to be a clear case for this.

So instead I will just offer some general feedback, as someone who's been using LD on a cross-platform expo project for a few years, and who is also excited for the recent TS-first + Universal React direction y'all are taking.

I should say up front that, while I have thought a bunch about this topic, I come from a place of ignorance, and it's possible I am overlooking tradeoffs. I figure y'all are already on top of things, but hope it's relevant enough to be useful to some degree.

Thanks!

For reference

Current React SDK Hooks

The launchdarkly-react-client-sdk exports the following hooks:

useFlags
useLDClient
useLDClientError
Current React Native SDK Hooks

The @launchdarkly/react-native-client-sdk exports:

useBoolVariation
useBoolVariationDetail
useNumberVariation
useNumberVariationDetail
useStringVariation
useStringVariationDetail
useJsonVariation
useJsonVariationDetail
useTypedVariation
useTypedVariationDetail
useLDClient

Single Flag vs All Flags Hooks

Recap

  • The useFlags hook from the React SDK returns all flags at once.
  • The useTypedVariation hook (and its derivatives) from the React Native SDK returns a single flag.

Preference for Single-Flag Approach

My preference is the single-flag approach, as it offers more granular control and helps avoid unnecessary re-renders. However, there are scenarios where the "all-flags" approach has its benefits—particularly when dealing with feature overrides.

Use Case for All-Flags: Feature Overrides

We use overrides extensively for various purposes, including:

  • e2e testing
  • local development
  • demoing
  • manual validation

If you have multiple "layers" of overrides, corresponding to different "sources," and you need to manage them as a group, having an "all-flags" hook becomes very useful. This is difficult to achieve with a single-flag interface alone.

Example: Flag Source Layer Priority
  1. overrides > url parameters — e.g., for e2e testing (also possibly cookies, headers)
  2. overrides > app dev menu — for demoing and manual validation within the app
  3. overrides > config file — a convenient .env-like file for local development
  4. remote > evaluation — the variation resolved from LD ⭐
  5. default > flag fallback — using defaultValue as a fallback argument
  6. defaults > config — global defaults for all flags, to ensure consistency across references

This "maximalist" approach isn't unrealistic

Why Not Just Return All Flags?

An argument could be made that returning all flags simplifies things. However, doing so introduces patterns that can lead to re-render issues. Therefore, I believe the atomic, single-flag approach is more sensible as the primary usage pattern.

That said, having a way to access all flags is a legitimate secondary pattern, especially in scenarios involving overrides.

Type-Specific Hooks?

My impression is that the type-specific hooks are sugar around the useTypedVariation hook — perhaps vestiges of conventions drawn from the more "type-first" native android/ios ecosystems?

I'd suggest that it is more idiomatic to control types via TS, eg

const myFlag = useVariation<boolean>(my-flag-key)

Where the return type, if not specified via generic arg, defaults to a union of the possible options

type FlagReturnType = boolean | number | string | JSON | undefined

I think this simplifies the coupling to default value in resolving type [code]

And it has the advantage of reducing the surface area and making adoption a friendlier proposition.

Consider: Standard React Hooks

If we consider the standard react hooks, they enable types through generics/overloads — but still leave this optional.

This makes it easy to have strong typing, but still leaves it accessible for teams that aren't as strict about TS observances.

Here's the TS definition for useState as an example.

    /**
     * Returns a stateful value, and a function to update it.
     *
     * @version 16.8.0
     * @see {@link https://react.dev/reference/react/useState}
     */
    function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];
    // convenience overload when first argument is omitted
    /**
     * Returns a stateful value, and a function to update it.
     *
     * @version 16.8.0
     * @see {@link https://react.dev/reference/react/useState}
     */
    function useState<S = undefined>(): [S | undefined, Dispatch<SetStateAction<S | undefined>>];

Default Value Usage?

This is a nuanced question — but let's consider the differences between these approaches:

const myFlag = useVariation<boolean>(my-flag-key) ?? false

vs

const myFlag = useVariation<boolean>(my-flag-key, false)

The fist delegates defaultValue to user-land

The second makes the defaultValue a part of the standard API*
*even a requirement, in the current implementation

Global Defaults?

Another option is to provide a "flag defaults" config to the LD init — so that the default for a given flag is consistent across usages.

Suggestion

I'd suggest that this is probably the ideal usage pattern / interface ⭐

useFlag(key: FlagKeys | string, defaultValue?: FlagTypes) : FlagTypes | undefined

Personally, I would not use the defaultValue argument — preferring instead to have global defaults — but it's there for those who prefer it.

Async vs Sync

There are certain situations where it's essential to resolve an up-to-date value from LD, and not use the cached/default version.

Currently the way to handle this is something like:

const useMyFeatureFlag = () => {
  const { isReady } = useLDClient();
  
  if !isReady return undefined; // or whatever default value
  
  const myKeyValue = useFlag('my-key');
  
  return myKeyValue
}

const MyComponent = () => {
  const myFeatureFlag = useMyFeatureFlag
  
  if (!myFeatureFlag) return LoadingView
  
  return <FeatureComponent flagValue={myFeatureFlag} />
}

However, this approach can be cumbersome.

I'd suggest this usage indicates adding support for a sort of async style interface, similar to modern API client libraries like react-query and urql

const MyComponent = () => {
  const [ myFeatureFlag, reQueryVariation ] = useFlagAsync('my-key'); // alternatively useFlagQuery?
  
  if (myFeatureFlag.fetching) return LoadingView
  if (myFeatureFlag.error) return ErrorView
  
  return <FeatureComponent flagValue={myFeatureFlag.value} />
}

Indeed it may even be worth adding support for "request policy" eg (from urql docs)

  • cache-first (the default) prefers cached results and falls back to sending an API request when no prior result is cached.
  • cache-and-network returns cached results but also always sends an API request, which is perfect for displaying data quickly while keeping it up-to-date.
  • network-only will always send an API request and will ignore cached results.
  • cache-only will always return cached results or null.

Future Consideration: Codegen?

It would be great to generate the type definitions for flags, as part of the dev/ci workflow.

Of course, there's nothing stopping users from doing this currently, but it would be great if there were official feature support. (perhaps it's already on the roadmap!)

I mention this because I think this possible eventuality might inform design choices around type handling — and also possibly generated default values.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions