splitio / react-client

React JS SDK client for Split Software
https://split.io
Other
27 stars 9 forks source link

Users are getting null is not an object (evaluating 'n.state.client.lastUpdate') #198

Open neriyarden opened 1 month ago

neriyarden commented 1 month ago

Hey, I don't know if this is a widespread issue or just an issue on my end. I'm seeing a big amount of errors in Sentry of this issue:

TypeError _this.update(@splitsoftware+splitio-react@1.12.0_react@18.3.1/node_modules/@splitsoftware/splitio-react/es/SplitClient) Unhandled: null is not an object (evaluating 'n.state.client.lastUpdate')

After searching the web and finding nothing about this issue, I dove into the code and found where I think the error occurs:

  // Attach listeners for SDK events, to update state if client status change.
  // The listeners take into account the value of `updateOnSdk***` props.
  subscribeToEvents(client: SplitIO.IBrowserClient | null) {
    if (client) {
      const statusOnEffect = getStatus(client);
      const status = this.state;

      if (this.props.updateOnSdkReady) {
        if (!statusOnEffect.isReady) client.once(client.Event.SDK_READY, this.update);
        else if (!status.isReady) this.update();
      }
      if (this.props.updateOnSdkReadyFromCache) {
        if (!statusOnEffect.isReadyFromCache) client.once(client.Event.SDK_READY_FROM_CACHE, this.update);
        else if (!status.isReadyFromCache) this.update();
      }
      if (this.props.updateOnSdkTimedout) {
        if (!statusOnEffect.hasTimedout) client.once(client.Event.SDK_READY_TIMED_OUT, this.update);
        else if (!status.hasTimedout) this.update();
      }
      if (this.props.updateOnSdkUpdate) client.on(client.Event.SDK_UPDATE, this.update);
    }
  }

This is my setup of the SplitClient:

import { useEffect, useState } from "react";
import {
  SplitClient,
  SplitFactoryProvider,
} from "@splitsoftware/splitio-react";

import { analytics } from "@flare/analytics";

const DUMMY_SPLIT_KEY = "anonymous";

export const SplitProvider = ({ children }: { children: React.ReactNode }) => {
  const [anonymousId, setAnonymousId] = useState<string | null | undefined>();

  useEffect(() => {
    async function getAnonymousId() {
      const aId = await analytics.getAnonymousId();
      setAnonymousId(aId);
    }

    getAnonymousId();
  }, []);

  const splitConfig: SplitIO.IBrowserSettings = {
    core: {
      authorizationKey: process.env.NEXT_PUBLIC_SPLIT_CLIENT_KEY as string,
      key: DUMMY_SPLIT_KEY,
      trafficType: "anonymous",
    },
  };

  if (anonymousId) {
    splitConfig.core.key = anonymousId;
  }

  return (
    <SplitFactoryProvider config={splitConfig}>
      <SplitClient splitKey={splitConfig.core.key}>{children}</SplitClient>
    </SplitFactoryProvider>
  );
};

When I log the isReady and client props i see that client is null when isReady is still false. Does this mean i have to lazy load the SplitClient and my entire component tree that passes as children (render only when isReady === true)?

Or am I missing something in my setup?

EmilianoSanchez commented 1 month ago

Hi @neriyarden,

Yes, that behavior (client is null when isReady is still false) is expected for the SplitFactoryProvider component. This is a breaking change compared to the now deprecated SplitFactory component.

In summary, the client property is null during the first render to avoid side-effects and follow the "rules" of React components. You can read more about it in the following references:

You don't necessarily have to lazy load the SplitClient or your component, but you should conditionally render based on the value of the isReady property (or isReadyFromCache). For example:

import { useSplitClient } from "@splitsoftware/splitio-react";

const MySplitClientChildrenComponent = (props) => {
  const { isReady, isReadyFromCache, client } = useSplitClient();

  return isReady || isReadyFromCache ?
    <p>`My treatment is ${client.getTreatment(MY_FEATURE_FLAG}`</p> : // client is available
    <Loading /> // client is null
}

But you can directly avoid using the client at all, for example:

import { useSplitTreatments } from "@splitsoftware/splitio-react";

const MySplitClientChildrenComponent = (props) => {
  const { isReady, isReadyFromCache, treatments } = useSplitTreatments({ names: [MY_FEATURE_FLAG] });

  return isReady || isReadyFromCache ?
    <p>`My treatment is ${treatments[MY_FEATURE_FLAG].treatment}`</p> :
    <Loading /> 
}

Hope this helps!