Skip to content

Commit d27edb1

Browse files
authored
fix: realtime updates not working on clientside (#997)
1 parent 87ffd93 commit d27edb1

5 files changed

+71
-52
lines changed

sdk/nextjs/src/client/DevCycleClientsideProvider.tsx

+1-25
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,6 @@ type DevCycleClientsideProviderProps = {
1414
children: React.ReactNode
1515
}
1616

17-
/**
18-
* Function which synchronously checks if the promise is resolved
19-
* If it is resolved, then a true is returned which is passed to the InternalDevCycleClientsideProvider,
20-
* telling it to "use" the promise to obtain the resolved value in the first render pass.
21-
* This is to work around cases where the initialize promise has resolved by now due to layouts rendering after pages
22-
* so the server is already rendering with variable values, but the client provider won't otherwise
23-
* use those values on the first pass. We can't always "use" the promise inside the provider
24-
* because in streaming mode that would sometimes block rendering unless the provider was inside a suspense
25-
* @param promise
26-
* @constructor
27-
*/
28-
const checkIfPromiseResolved = (promise: Promise<unknown>) => {
29-
let promiseResolved = false
30-
promise.then(() => {
31-
promiseResolved = true
32-
})
33-
return promiseResolved
34-
}
35-
3617
export const DevCycleClientsideProvider = async ({
3718
context,
3819
children,
@@ -44,13 +25,8 @@ export const DevCycleClientsideProvider = async ({
4425
: await context.serverDataPromise,
4526
}
4627

47-
const promiseResolved = checkIfPromiseResolved(context.serverDataPromise)
48-
4928
return (
50-
<InternalDevCycleClientsideProvider
51-
context={clientsideContext}
52-
promiseResolved={promiseResolved}
53-
>
29+
<InternalDevCycleClientsideProvider context={clientsideContext}>
5430
{children}
5531
</InternalDevCycleClientsideProvider>
5632
)

sdk/nextjs/src/client/internal/InternalDevCycleClientsideProvider.tsx

+50-23
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
'use client'
2-
import React, { use, useRef } from 'react'
2+
import React, { Suspense, use, useEffect, useRef } from 'react'
33
import { DevCycleClient, initializeDevCycle } from '@devcycle/js-client-sdk'
44
import { invalidateConfig } from '../../common/invalidateConfig'
55
import { DevCycleNextOptions, DevCycleServerData } from '../../common/types'
@@ -17,16 +17,54 @@ export type DevCycleClientContext = {
1717

1818
type DevCycleClientsideProviderProps = {
1919
context: DevCycleClientContext
20-
promiseResolved: boolean
2120
children: React.ReactNode
2221
}
2322

2423
const isServer = typeof window === 'undefined'
2524

25+
/**
26+
* keep the clientside instance of the SDK up-to-date with new data coming from the server during realtime updates
27+
* @param serverDataPromise
28+
* @param client
29+
* @param enableStreaming
30+
* @constructor
31+
*/
32+
const SynchronizeClientData = ({
33+
serverDataPromise,
34+
client,
35+
enableStreaming,
36+
}: {
37+
serverDataPromise: Promise<DevCycleServerData>
38+
client: DevCycleClient
39+
enableStreaming: boolean
40+
}) => {
41+
const serverData = use(serverDataPromise)
42+
const dataRef = useRef<DevCycleServerData | null>(
43+
// when streaming is disabled, we run synchronization on the initial server data in the
44+
// InternalDevCycleClientsideProvider component so we don't need to do it again immediately.
45+
// In streaming mode we want to synchronize on that initial server data since we aren't doing it above
46+
// Therefore set this ref to the initial server data so the below check won't run when not in streaming mode
47+
!enableStreaming ? serverData : null,
48+
)
49+
const clientRef = useRef<DevCycleClient>(client)
50+
51+
if (dataRef.current !== serverData || clientRef.current !== client) {
52+
dataRef.current = serverData
53+
clientRef.current = client
54+
// do this in a timeout to avoid setting React state in components that are subscribed to variables as a
55+
// side effect of the current render, since this causes errors. Instead schedule the update to occur after
56+
// render completes
57+
setTimeout(() =>
58+
client.synchronizeBootstrapData(serverData.config, serverData.user),
59+
)
60+
}
61+
62+
return null
63+
}
64+
2665
export const InternalDevCycleClientsideProvider = ({
2766
context,
2867
children,
29-
promiseResolved,
3068
}: DevCycleClientsideProviderProps): React.ReactElement => {
3169
const clientRef = useRef<DevCycleClient>()
3270
const router = useRouter()
@@ -56,14 +94,6 @@ export const InternalDevCycleClientsideProvider = ({
5694
}
5795
}
5896

59-
let resolvedServerData = serverData
60-
61-
if (!serverData && promiseResolved) {
62-
// here the provider is being told from above that this promise has already resolved, so we can safely "use"
63-
// it without blocking rendering, even in streaming mode
64-
resolvedServerData = use(serverDataPromise)
65-
}
66-
6797
if (!clientRef.current) {
6898
clientRef.current = initializeDevCycle(clientSDKKey, {
6999
...context.options,
@@ -81,7 +111,8 @@ export const InternalDevCycleClientsideProvider = ({
81111
},
82112
})
83113

84-
if (resolvedServerData || !enableStreaming) {
114+
if (!enableStreaming) {
115+
const resolvedServerData = use(serverDataPromise)
85116
// we expect that either the promise has resolved and we got the server data that way, or we weren't in
86117
// streaming mode and so the promise was awaited at a higher level and passed in here as serverData
87118
if (!resolvedServerData) {
@@ -94,17 +125,6 @@ export const InternalDevCycleClientsideProvider = ({
94125
resolvedServerData.user,
95126
resolvedServerData.userAgent,
96127
)
97-
} else {
98-
// if the promise isnt resolved yet, schedule it to synchronize when it is
99-
// we check the above condition first to make sure we can use the resolved data on the first render pass
100-
// since the `.then` here is only evaluated after this render pass is finished
101-
serverDataPromise.then((serverData) => {
102-
clientRef.current!.synchronizeBootstrapData(
103-
serverData.config,
104-
serverData.user,
105-
serverData.userAgent,
106-
)
107-
})
108128
}
109129
}
110130

@@ -117,6 +137,13 @@ export const InternalDevCycleClientsideProvider = ({
117137
serverDataPromise,
118138
}}
119139
>
140+
<Suspense fallback={null}>
141+
<SynchronizeClientData
142+
serverDataPromise={serverDataPromise}
143+
client={clientRef.current}
144+
enableStreaming={enableStreaming}
145+
/>
146+
</Suspense>
120147
{children}
121148
</DevCycleProviderContext.Provider>
122149
)
+9-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
'use client'
22
import { useDevCycleClient } from './internal/useDevCycleClient'
33
import { useRerenderOnVariableChange } from './internal/useRerenderOnVariableChange'
4-
import { DVCFeatureSet } from '@devcycle/js-client-sdk'
4+
import { DevCycleClient, DVCFeatureSet } from '@devcycle/js-client-sdk'
5+
import { use, useContext } from 'react'
6+
import { DevCycleProviderContext } from './internal/context'
57

68
export const useAllFeatures = (): DVCFeatureSet => {
79
const client = useDevCycleClient()
10+
const context = useContext(DevCycleProviderContext)
11+
812
useRerenderOnVariableChange()
13+
// Fall back to nearest suspense boundary if client is not initialized yet.
14+
if (context.enableStreaming) {
15+
use((client as DevCycleClient).onClientInitialized())
16+
}
917
return client.allFeatures()
1018
}
+9-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
'use client'
22
import { useDevCycleClient } from './internal/useDevCycleClient'
33
import { useRerenderOnVariableChange } from './internal/useRerenderOnVariableChange'
4-
import { DVCVariableSet } from '@devcycle/js-client-sdk'
4+
import { DevCycleClient, DVCVariableSet } from '@devcycle/js-client-sdk'
5+
import { use, useContext } from 'react'
6+
import { DevCycleProviderContext } from './internal/context'
57

68
export const useAllVariables = (): DVCVariableSet => {
79
const client = useDevCycleClient()
10+
const context = useContext(DevCycleProviderContext)
11+
812
useRerenderOnVariableChange()
13+
// Fall back to nearest suspense boundary if client is not initialized yet.
14+
if (context.enableStreaming) {
15+
use((client as DevCycleClient).onClientInitialized())
16+
}
917
return client.allVariables()
1018
}

sdk/nextjs/src/client/useVariableValue.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
'use client'
2-
import type { DVCVariableValue } from '@devcycle/js-client-sdk'
2+
import { DevCycleClient, DVCVariableValue } from '@devcycle/js-client-sdk'
33
import { useContext, use } from 'react'
44
import { VariableTypeAlias } from '@devcycle/types'
55
import { DVCVariable } from '@devcycle/js-client-sdk'
@@ -15,7 +15,7 @@ export const useVariable = <T extends DVCVariableValue>(
1515

1616
// Fall back to nearest suspense boundary if client is not initialized yet.
1717
if (context.enableStreaming) {
18-
use(context.serverDataPromise)
18+
use((context.client as DevCycleClient).onClientInitialized())
1919
}
2020

2121
return context.client.variable(key, defaultValue)

0 commit comments

Comments
 (0)