react-client
react-client copied to clipboard
Additional re-renders on any feature flag changes due to lastUpdate
Hello!
We were previously using version 1.13.0 and updated to 2.1.0. We then started seeing re-renders for any feature flag changes. When diffing the context against a stored version, the only value that's changed is lastUpdate, but this is causing a total re-render because the context has changed.
This is how I'm inspecting the context:
const CustomSplitClient = ({ children }: Props) => {
// Get a user ID
const userId = useMemo(
() => someFunction(),
);
const splitContext = useSplitClient({ splitKey: userId });
const splitContextRef = useRef(splitContext);
const contextValue = useMemo(() => {
const newValue = JSON.stringify(splitContext);
const previousValue = JSON.stringify(splitContextRef.current);
if (newValue !== previousValue) {
console.log("Context Changed");
console.log('New Value-->\n', newValue, '\n<--');
console.log('Previous Value-->\n', previousValue, '\n<--');
splitContextRef.current = splitContext;
}
return splitContextRef.current;
}, [splitContext]);
return <SplitContext.Provider value={contextValue}>{children}</SplitContext.Provider>;
};
Happy to provide any other info that can help. Thank you!
Hi @ewdicus ,
The issue you encountered is likely related to a breaking change introduced in version 2.0.0.
The new useSplitClient and useSplitTreatments hooks, which replace the now removed useClient and useTreatments hooks respectively, accept an updateOnSdkUpdate configuration option that controls whether the hook should re-render (and re-evaluate flags in the case of the useSplitTreatments hook) when an SDK_UPDATE event occurs, i.e., when feature flags change in the background.
In v2.0.0, the default value of the updateOnSdkUpdate option changed from false to true. We have now explained this change in the MIGRATION-GUIDE.
So basically, you need to refactor the line:
const splitContext = useSplitClient({ splitKey: userId });
to
const splitContext = useSplitClient({ splitKey: userId, updateOnSdkUpdate: false });
to achieve the behavior you described (Context lastUpdate property will not change on an SDK_UPDATE event).
Additionally, consider upgrading the SDK to v2.1.1, which fixes an issue related to the behavior of the library hooks.
I hope this helps.
Hi @EmilianoSanchez,
Thanks for the quick response! Setting updateOnSdkUpdate on the client does change that to the previous behavior. However, I also have to set it on calls to useSplitTreatments. Is that the intention?
I may be misinterpreting the goal but it seems like I would set this on the client that I'm providing to the context and later consumers would use that client with this option already set.
Thanks again!
You are welcome @ewdicus
Thanks for the quick response! Setting updateOnSdkUpdate on the client does change that to the previous behavior. However, I also have to set it on calls to useSplitTreatments. Is that the intention?
Yes. Each hook updates (i.e., triggers a component re-render) independently. For example, if you are using both hooks in the same component, if one of them is called with updateOnSdkUpdate: false and the other with updateOnSdkUpdate: true (or no option, which is true by default), the component will re-render on SDK_UPDATE events on the userId client, due to the 2nd hook.
const MyComponent = () => {
const { client } = useSplitClient({ splitKey: userId, updateOnSdkUpdate: false })
const { treatments } = useSplitTreatments({ splitKey: userId, updateOnSdkUpdate: true })
...
Anyway, it is not recommended to use the client instance from the useSplitClient hook result directly, as better alternatives are available:
-
useSplitTreatmentshook to evaluate feature flags.
-
useTrackhook to track events.
useSplitClient is recommended for retrieving the underlying SDK client and factory instances to access methods that are not available in a "React-ish" way via hooks, like updating the logging configuration or user consent after the SDK factory was created.
I may be misinterpreting the goal but it seems like I would set this on the client that I'm providing to the context and later consumers would use that client with this option already set.
Yes, considering your CustomSplitClient example (very similar to the SplitClient component), if you call useSplitClient({ splitKey: userId, updateOnSdkUpdate: false }) (Note updateOnSdkUpdate: false), hooks used in children components of CustomSplitClient will consume the updated SplitContext and have updateOnSdkUpdate: false by default, but you need to update to the latest SDK version v2.2.0 to get that behavior.
Also, since v2.2.0 you can consider overwriting the default value of the updateOnSdkUpdate configuration option globally, passing the property to the SplitFactoryProvider, as follows:
const MY_SDK_FACTORY = SplitFactory(MY_SDK_CONFIG);
const MyApp = () => {
return (
<SplitFactoryProvider
factory={MY_SDK_FACTORY}
// all usages of SDK hooks in children components will have `updateOnSdkUpdate` false if not provided
updateOnSdkUpdate={false}
>
<MyComponent />
</SplitFactoryProvider>
)
}
@EmilianoSanchez I appreciate the extra information. For context we have exactly the situation indicated here (https://help.split.io/hc/en-us/articles/360046771911-React-SDK-Lazy-initialization-of-Split-client) where we don't know the user id when the factory is constructed. Is there a recommended way to handle this situation in v2.2.0 that keeps everything centralized?
Thank you again 🙌🏻
@ewdicus, thanks for pointing out that FAQ. It’s outdated. I’ll check to get it updated soon.
Here’s the equivalent FAQ's code snippet for v2.2.0:
import { useState, useEffect } from 'react';
import { SplitFactoryProvider, useSplitTreatments } from '@splitsoftware/splitio-react';
const sdkConfig = {
core: {
authorizationKey: 'YOUR-SDK-CLIENT-SIDE-KEY',
key: 'anonymous',
}
}
const featureName = 'test_split';
function MyComponent({ userId }) {
const { treatments, isReady } = useSplitTreatments({ names: [featureName], splitKey: userId, updateOnSdkUpdate: false });
return isReady ?
<p>Treatment for {userId} in {featureName} is: {treatments[featureName].treatment}</p> :
<p>loading...</p>; // Render a spinner if the SDK client for `userId` is not ready yet
}
function App() {
// using 'anonymous' as initial userId
const [userId, setUserId] = useState(sdkConfig.core.key);
// update userId to 'loggedinId' after 3 seconds
useEffect(() => {
setTimeout(() => { setUserId('loggedinId'); }, 3000);
}, [])
return (
<SplitFactoryProvider config={sdkConfig}>
<MyComponent userId={userId} />
</SplitFactoryProvider>
);
}
export default App;
Notice how I replaced the usage of deprecated SplitClient and SplitTreatments components with the useSplitTreatments hook, and how it is called with updateOnSdkUpdate: false to replicate the default behaviour of React SDK v1.
Remember, if you don't want to manually pass updateOnSdkUpdate: false to all usages of useSplitClient|Treatments, you can pass it as a prop in the SplitFactoryProvider component to overwrite the default.
Ok great! 🙌🏻 We have several places in the app where we use useSplitTreatments, and we'd like to prevent prop-drilling the userId to all those call sites. Is there a "splitio way" of handling that, or should we handle it ourselves (in a context, in state management, etc.)?
Is something like this an anti-pattern / should we avoid changing the config like this?
const sdkConfig = {
core: {
authorizationKey: 'YOUR-SDK-CLIENT-SIDE-KEY',
key: 'anonymous',
}
}
function App() {
// using 'anonymous' as initial userId
const [userId, setUserId] = useState(sdkConfig.core.key);
// update userId to 'loggedinId' after 3 seconds
useEffect(() => {
setTimeout(() => { setUserId('loggedinId'); }, 3000);
}, [])
const config = useMemo(()=>({
...sdkConfig,
key: userId
}), [userId])
return (
<SplitFactoryProvider config={sdkConfig}>
{/* Rest of app */}
</SplitFactoryProvider>
);
}
Ok great! 🙌🏻 We have several places in the app where we use useSplitTreatments, and we'd like to prevent prop-drilling the userId to all those call sites. Is there a "splitio way" of handling that, or should we handle it ourselves (in a context, in state management, etc.)?
I understand now, @ewdicus. If you want to prevent prop-drilling the userId, using the SplitClient component is a good choice. We deprecated it to reduce component‑tree depth and because React is better optimized for hooks, but perhaps we should revisit that decision.
Anyway, the implementation of the SplitClient is quite simple, as you can check here, in case you want to create your own "SplitClient" component and avoid the deprecation warning.
Also consider the following improved version of the code snippet here using the SplitClient:
import { useState, useEffect } from 'react';
import { SplitFactoryProvider, SplitClient, useSplitTreatments } from '@splitsoftware/splitio-react';
const sdkConfig = {
core: {
authorizationKey: 'YOUR-SDK-CLIENT-SIDE-KEY',
key: 'anonymous',
}
}
const featureName = 'test_split';
function MyComponent() {
const { treatments, isReady, client } = useSplitTreatments({ names: [featureName] });
return isReady ?
<p>Treatment for userId {client.key} in {featureName} is: {treatments[featureName].treatment}</p> :
<p>loading...</p>; // Render a spinner if the SDK client for `userId` is not ready yet
}
function App() {
// using 'anonymous' as initial userId
const [userId, setUserId] = useState(sdkConfig.core.key);
// update userId to 'loggedinId' after 3 seconds
useEffect(() => {
setTimeout(() => { setUserId('loggedinId'); }, 3000);
}, [])
return (
<SplitFactoryProvider config={sdkConfig} >
<SplitClient splitKey={userId} updateOnSdkUpdate={false} >
<MyComponent />
</SplitClient>
</SplitFactoryProvider>
);
}
export default App;
MyComponent will render 4 times:
- 1st: 'anonymous' userId and
isReadyfalse - 2nd (when the SDK main client is ready): 'anonymous' userId and
isReadytrue - 3rd (after 3 seconds): 'loggedinId' userId and
isReadyfalse - 4th (when the client for 'loggedinId' key is ready): 'loggedinId' userId and
isReadytrue
Is something like this an anti-pattern / should we avoid changing the config like this?
Yes. Avoid changing the config like that because passing a different config object causes the SplitFactoryProvider component to destroy its internal SDK factory instance and recreate it for the new config. The code will still work, but at the expense of extra HTTP(S) requests to synchronize a second factory.
Closing as resolved.
FAQ https://help.split.io/hc/en-us/articles/360046771911-React-SDK-Lazy-initialization-of-Split-client has been updated with the correct code snippets.