statsig-io / js-client-monorepo

ISC License
4 stars 2 forks source link

Overrides don't trigger render in React #9

Open Mitelak opened 2 months ago

Mitelak commented 2 months ago

I'd expect that if I use LocalOverrideAdapter and I override the gate dynamically (e.g., due to some UI action) using overrideGate, the useFeatureGate will react to the change of the value. This doesn't happen. Actually, react-bindings rerender only if value_updated is emitted, which happens only on user update async or sync calls.

It'd be good to either start emitting the value_updated event on other actions that can cause the final gate value to change or emit override events and listen for them in the react bindings, too. I can make a PR if this can help.

daniel-statsig commented 2 months ago

Hey @Mitelak, happy to discuss any PR you put up. The way the bindings work today is all driven through "values_updated" as you have said. It was designed this way such that only an initialize or updateUser call could trigger a re-render.

The LocalOverrideAdapter was initially designed to facilitate testing so doesn't really interact with the react bindings.

Can you describe your use case more? If you are showing some UI based on a user interaction, how are you persisting it to future sessions?

I'm trying to determine if you really need local overrides for this, or if you could just have some gate based on a user attribute like: myUser.custom.hasAcceptedTerms

justinjunodev commented 2 months ago

Hey @Mitelak, happy to discuss any PR you put up. The way the bindings work today is all driven through "values_updated" as you have said. It was designed this way such that only an initialize or updateUser call could trigger a re-render.

The LocalOverrideAdapter was initially designed to facilitate testing so doesn't really interact with the react bindings.

Can you describe your use case more? If you are showing some UI based on a user interaction, how are you persisting it to future sessions?

I'm trying to determine if you really need local overrides for this, or if you could just have some gate based on a user attribute like: myUser.custom.hasAcceptedTerms

Thanks @daniel-statsig. @Mitelak feel free to chime in here as well.

In a nutshell, we have a dialog within our application that our internal teams use to toggle some of our feature flags. This is useful for feature-work-in-progress, qa, etc. Since we are providing the user object to our client, I'd assume updating/ appending the custom value to the user would trigger the re-render, but it'd also store that value permanently on that user. This isn't ideal, IMO, and I believe the prior overrides stored the these values locally? 🤔

daniel-statsig commented 2 months ago

Hmm, that sounds similar to what the StatsigClient debugger does (https://docs.statsig.com/sdk/debugging#client-sdk-debugger). This isn't currently supported however, but I bring it as a way to gauge interest. We could build out support for it, possibly saving your teams time building their own debug UI.

All that said, if you want LocalOverrides to show up in the react-bindings, you should just need to call updateUserSync with the same user object.

Something like this:

function Content() {
  const { value, details } = useFeatureGate('my_gate');
  const { renderVersion } = useContext(StatsigContext);

  const { client } = useStatsigClient();
  const applyOverride = () => {
    const context = client.getContext();
    const adapter = context.options.overrideAdapter as LocalOverrideAdapter;
    adapter.overrideGate('my_gate', true);

    client.updateUserSync(context.user);
  };

  return (
    <div style={{ padding: 16 }}>
      <div>Render Version: {renderVersion}</div>
      <div>
        my_gate: {value ? 'Passing' : 'Failing'} ({details.reason})
      </div>
      <button onClick={() => applyOverride()}>Override</button>
    </div>
  );
}

export default function App(): JSX.Element {
  const client = useMemo(() => {
    const client = new StatsigClient(
      DEMO_CLIENT_KEY,
      { userID: 'a-user' },
      {
        logLevel: LogLevel.Debug,
        overrideAdapter: new LocalOverrideAdapter(),
      },
    );
    client.initializeAsync().catch(console.error);
    return client;
  }, []);

  return (
    <StatsigProvider client={client}>
      <Content />
    </StatsigProvider>
  );
}