TrueCar / react-launch-darkly

Simple component helpers to support LaunchDarkly in your React app.
MIT License
76 stars 20 forks source link

Bootstrapping flags returns empty on hard reload #73

Closed janhartmann closed 6 years ago

janhartmann commented 6 years ago

First of all, thanks for this great integration

I am in a situation where I do a hard reload of a page with has a React Router FeatureFlag, the getAllFeatureFlags returns an empty object and therefore not rendering the renderFeatureCallback. After debugging this the userKey is set correctly on the getAllFeatureFlags request, but the FeatureFlag returns the renderDefaultCallback response.

If I click around the routes of the application (even after a hard reload), to pages with FeatureFlag's it all works fine. I am unsure whether or not I am using this library wrong, or I have some issues with my routing. Any suggestions is much appreciated. I know there is many moving parts.

Hard reloading on routes without a <FeatureFlag /> wrapper, works just fine.

My setup is that I wrap the <LaunchDarkly ... /> in a wrapper:

import { LaunchDarkly, getAllFeatureFlags } from "react-launch-darkly";

export class LaunchDarklyProvider extends React.Component<
  ILaunchDarklyProviderProps,
  ILaunchDarklyProviderState
> {
  constructor(props: ILaunchDarklyProviderProps) {
    super(props);

    this.state = {
      flags: {}
    };
  }

  public async componentDidMount() {
    await this.bootstrapFlags();
  }

  public async componentDidUpdate(prevProps: ILaunchDarklyProviderProps) {
    const { authContext } = this.props;
    if (prevProps.authContext.userId !== authContext.userId) {
      await this.bootstrapFlags();
    }
  }

  public render() {
    const { flags } = this.state;
    const { children } = this.props;

    return (
      <LaunchDarkly
        clientId={ApplicationConfiguration.launchDarklyClientId}
        user={{ key: this.getUserKey() }}
        clientOptions={{
          bootstrap: flags
        }}
      >
        {children}
      </LaunchDarkly>
    );
  }

  private async bootstrapFlags() {
    const { authContext } = this.props;

    const flags = await getAllFeatureFlags(
      ApplicationConfiguration.launchDarklyClientId,
      {
         key: this.getUserKey()
      }
    );
    this.setState({
      flags
    });
    }
  }

  private getUserKey(): string {
    const { authContext } = this.props;
    if (authContext.customerId && authContext.userId) {
      return `${authContext.customerId}_${authContext.userId}`;
    }
    return null;
  }
}

This setup is then wired into the application, and it functions fine. Now I toggle my React Router routes like so:

const RouteWithSubRoutes: React.SFC = route => (
  <AuthContextConsumer>
    {auth => (
      <Route
        path={route.path}
        exact={route.exact}
        render={props => {
          if (auth.isAuthenticated() || !route.protected) {
            if (route.requireFlag) { // This is the flag key.
              return (
                <FeatureFlag
                  flagKey={route.requireFlag}
                  renderFeatureCallback={() => (
                    <route.component {...props} {...route} />
                  )}
                  renderDefaultCallback={() => <p>No Access</p>}
                />
              );
            }
            return <route.component {...props} {...route} />;
          }
          return <Redirect to="/auth/login" />;
        }}
      />
    )}
  </AuthContextConsumer>
);

My composition root of the applications is like

<AuthProvider>
  <Router>
    <LaunchDarklyProvider>
      <React.Fragment>
        <Route
          exact={true}
          path="/"
          render={() => <Redirect to="/dashboard" />}
        />
        <App />
      </React.Fragment>
    </LaunchDarklyProvider>
  </Router>
</AuthProvider>

With the App component looking like:

class App extends React.Component {
  public render() {
    const { classes } = this.props;
    return (
      <div className={classes.root}>
        {routes.map((route, index) => (
          <RouteWithSubRoutes key={index} {...route} />
        ))}
      </div>
    );
  }
}
janhartmann commented 6 years ago

Holy. I got it working, the issue was I needed to update the bootstrap state prop and not a flags (nested) state prop. So changing to:

 this.state = {
      boostrap: {}
    };
<LaunchDarkly
  clientId={ApplicationConfiguration.launchDarklyClientId}
  user={{ key: this.getUserKey() }}
  clientOptions={{ boostrap }}
>
  {children}
</LaunchDarkly>

And fetch like:

const flags = await getAllFeatureFlags(
  ApplicationConfiguration.launchDarklyClientId,
  {
    key: this.getUserKey()
  }
);
this.setState({
  boostrap: {
    flags
  }
});

Guess I was updating a nested prop and not triggering an update.