From 46bfceb17288ad235e3fcd7bfce59809028753a6 Mon Sep 17 00:00:00 2001 From: Adam Wootton Date: Wed, 20 Dec 2023 17:45:03 -0500 Subject: [PATCH] feat: bring back streaming mode with docs updates (#656) --- e2e/nextjs/app-router/app/yarn.lock | 22 +++--- e2e/nextjs/app-router/playwright.config.ts | 2 - e2e/nextjs/pages-router/app/yarn.lock | 22 +++--- examples/nextjs/app-router/app/layout.tsx | 10 +-- package.json | 2 +- sdk/nextjs/README.md | 17 ++--- .../src/client/DevCycleClientsideProvider.tsx | 76 +++++++++---------- sdk/nextjs/src/client/useVariableValue.ts | 9 +-- .../src/server/DevCycleServersideProvider.tsx | 6 +- sdk/nextjs/src/server/initialize.ts | 3 +- yarn.lock | 4 +- 11 files changed, 79 insertions(+), 94 deletions(-) diff --git a/e2e/nextjs/app-router/app/yarn.lock b/e2e/nextjs/app-router/app/yarn.lock index cc9d7470f..851b76d20 100644 --- a/e2e/nextjs/app-router/app/yarn.lock +++ b/e2e/nextjs/app-router/app/yarn.lock @@ -42,7 +42,7 @@ __metadata: "@devcycle/nextjs-sdk@file:../../../../dist/sdk/nextjs::locator=app%40workspace%3A.": version: 0.0.4 - resolution: "@devcycle/nextjs-sdk@file:../../../../dist/sdk/nextjs#../../../../dist/sdk/nextjs::hash=919019&locator=app%40workspace%3A." + resolution: "@devcycle/nextjs-sdk@file:../../../../dist/sdk/nextjs#../../../../dist/sdk/nextjs::hash=11f915&locator=app%40workspace%3A." dependencies: "@devcycle/bucketing": "npm:^1.7.4" "@devcycle/js-client-sdk": "npm:^1.16.3" @@ -53,7 +53,7 @@ __metadata: peerDependencies: next: ^14.0.0 react: ^18.2.0 - checksum: 07fc055921546c529ade5f2717b660a60eb975d90553108ad5d42efd464e4f10a4e8acb12f268b17b613496b5d74b592b6f78e9a0eb3f1b95afda8dc841986f8 + checksum: 3b1947b28c1cbf2ac2eded4d30d48312a4b33e8d606b0cc3c13431ddf0f5bdbc608248ce9ed4ee0cb9a188e61563f325d9c003903d2ea4038bb194a18c5e2dca languageName: node linkType: hard @@ -173,11 +173,11 @@ __metadata: linkType: hard "@types/node@npm:^20": - version: 20.10.4 - resolution: "@types/node@npm:20.10.4" + version: 20.10.5 + resolution: "@types/node@npm:20.10.5" dependencies: undici-types: "npm:~5.26.4" - checksum: c10c1dd13f5c2341ad866777dc32946538a99e1ebd203ae127730814b8e5fa4aedfbcb01cb3e24a5466f1af64bcdfa16e7de6e745ff098fff0942aa779b7fe03 + checksum: 4a378428d2c9f692b19801a5a3d20dc4c0ad5d4a3d103350f8b401af439941a9aa5efeadc8eb9db13c66c620318bc7f336abfc8934f82fd32c4a689d85068c6f languageName: node linkType: hard @@ -377,9 +377,9 @@ __metadata: linkType: hard "libphonenumber-js@npm:^1.9.43": - version: 1.10.51 - resolution: "libphonenumber-js@npm:1.10.51" - checksum: 925fda2ecba5d1e655f9d6e43cd15b2d41f7fe9db38828f4159442bc263ae959a26f212223336831c6f64e58b82e91477f7ac814d66b48c80e5852529545032b + version: 1.10.52 + resolution: "libphonenumber-js@npm:1.10.52" + checksum: d8a41623e1144b07482c01156316e35708fd9b371a2a72086d25213f2dee1755ff2abf660dfe9df3c38500689868dc2fb7e462ca23207b0b8ca95daad9c5bd74 languageName: node linkType: hard @@ -550,9 +550,9 @@ __metadata: linkType: hard "regenerator-runtime@npm:^0.14.0": - version: 0.14.0 - resolution: "regenerator-runtime@npm:0.14.0" - checksum: 6c19495baefcf5fbb18a281b56a97f0197b5f219f42e571e80877f095320afac0bdb31dab8f8186858e6126950068c3f17a1226437881e3e70446ea66751897c + version: 0.14.1 + resolution: "regenerator-runtime@npm:0.14.1" + checksum: 5db3161abb311eef8c45bcf6565f4f378f785900ed3945acf740a9888c792f75b98ecb77f0775f3bf95502ff423529d23e94f41d80c8256e8fa05ed4b07cf471 languageName: node linkType: hard diff --git a/e2e/nextjs/app-router/playwright.config.ts b/e2e/nextjs/app-router/playwright.config.ts index 84aafd33f..690ec1e45 100644 --- a/e2e/nextjs/app-router/playwright.config.ts +++ b/e2e/nextjs/app-router/playwright.config.ts @@ -28,9 +28,7 @@ export default defineConfig({ baseURL, /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', - video: 'retain-on-failure', }, - /* Run your local dev server before starting the tests */ webServer: { command: 'yarn e2e:nextjs-app-router:start', diff --git a/e2e/nextjs/pages-router/app/yarn.lock b/e2e/nextjs/pages-router/app/yarn.lock index 612154241..851b76d20 100644 --- a/e2e/nextjs/pages-router/app/yarn.lock +++ b/e2e/nextjs/pages-router/app/yarn.lock @@ -42,7 +42,7 @@ __metadata: "@devcycle/nextjs-sdk@file:../../../../dist/sdk/nextjs::locator=app%40workspace%3A.": version: 0.0.4 - resolution: "@devcycle/nextjs-sdk@file:../../../../dist/sdk/nextjs#../../../../dist/sdk/nextjs::hash=7c9d07&locator=app%40workspace%3A." + resolution: "@devcycle/nextjs-sdk@file:../../../../dist/sdk/nextjs#../../../../dist/sdk/nextjs::hash=11f915&locator=app%40workspace%3A." dependencies: "@devcycle/bucketing": "npm:^1.7.4" "@devcycle/js-client-sdk": "npm:^1.16.3" @@ -53,7 +53,7 @@ __metadata: peerDependencies: next: ^14.0.0 react: ^18.2.0 - checksum: 0913c9cbf1cfe9263b7d9367f001ac3526378340a1943d917177af122abb9b1d796b1c9947811c5e7aae11556e9ba06e6eb239d8fd84318718584b6fa69af253 + checksum: 3b1947b28c1cbf2ac2eded4d30d48312a4b33e8d606b0cc3c13431ddf0f5bdbc608248ce9ed4ee0cb9a188e61563f325d9c003903d2ea4038bb194a18c5e2dca languageName: node linkType: hard @@ -173,11 +173,11 @@ __metadata: linkType: hard "@types/node@npm:^20": - version: 20.10.4 - resolution: "@types/node@npm:20.10.4" + version: 20.10.5 + resolution: "@types/node@npm:20.10.5" dependencies: undici-types: "npm:~5.26.4" - checksum: c10c1dd13f5c2341ad866777dc32946538a99e1ebd203ae127730814b8e5fa4aedfbcb01cb3e24a5466f1af64bcdfa16e7de6e745ff098fff0942aa779b7fe03 + checksum: 4a378428d2c9f692b19801a5a3d20dc4c0ad5d4a3d103350f8b401af439941a9aa5efeadc8eb9db13c66c620318bc7f336abfc8934f82fd32c4a689d85068c6f languageName: node linkType: hard @@ -377,9 +377,9 @@ __metadata: linkType: hard "libphonenumber-js@npm:^1.9.43": - version: 1.10.51 - resolution: "libphonenumber-js@npm:1.10.51" - checksum: 925fda2ecba5d1e655f9d6e43cd15b2d41f7fe9db38828f4159442bc263ae959a26f212223336831c6f64e58b82e91477f7ac814d66b48c80e5852529545032b + version: 1.10.52 + resolution: "libphonenumber-js@npm:1.10.52" + checksum: d8a41623e1144b07482c01156316e35708fd9b371a2a72086d25213f2dee1755ff2abf660dfe9df3c38500689868dc2fb7e462ca23207b0b8ca95daad9c5bd74 languageName: node linkType: hard @@ -550,9 +550,9 @@ __metadata: linkType: hard "regenerator-runtime@npm:^0.14.0": - version: 0.14.0 - resolution: "regenerator-runtime@npm:0.14.0" - checksum: 6c19495baefcf5fbb18a281b56a97f0197b5f219f42e571e80877f095320afac0bdb31dab8f8186858e6126950068c3f17a1226437881e3e70446ea66751897c + version: 0.14.1 + resolution: "regenerator-runtime@npm:0.14.1" + checksum: 5db3161abb311eef8c45bcf6565f4f378f785900ed3945acf740a9888c792f75b98ecb77f0775f3bf95502ff423529d23e94f41d80c8256e8fa05ed4b07cf471 languageName: node linkType: hard diff --git a/examples/nextjs/app-router/app/layout.tsx b/examples/nextjs/app-router/app/layout.tsx index 2109c250d..105dfd847 100644 --- a/examples/nextjs/app-router/app/layout.tsx +++ b/examples/nextjs/app-router/app/layout.tsx @@ -18,12 +18,10 @@ export default async function RootLayout({ user_id: process.env.NEXT_PUBLIC_USER_ID || 'server-user', }} - options={ - { - // enableStreaming: - // process.env.NEXT_PUBLIC_ENABLE_STREAMING === '1', - } - } + options={{ + enableStreaming: + process.env.NEXT_PUBLIC_ENABLE_STREAMING === '1', + }} > {children} diff --git a/package.json b/package.json index 1a0220670..c7324580f 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "murmurhash": "^2.0.0", "next": "14.0.0", "protobufjs": "^7.2.5", - "react": "^18.2.0", + "react": "18.2.0", "react-bootstrap": "^2.2.1", "react-dom": "18.2.0", "react-is": "18.2.0", diff --git a/sdk/nextjs/README.md b/sdk/nextjs/README.md index fd7f42578..3a3a5f5f0 100644 --- a/sdk/nextjs/README.md +++ b/sdk/nextjs/README.md @@ -7,13 +7,13 @@ Official SDK for integrating DevCycle feature flags with your Next.js applicatio - keep server and client rendered content in sync with the same flag values - keep user data for targeting rule evaluation private on the server - realtime updates to flag values for both server and client components -- support for non-blocking flag state retrieval and streaming (not implemented, see below) - -## Requirements -- Minimum Next.js version: 14.0.0 -- Minimum React version: 18.2 +- support for non-blocking flag state retrieval and streaming ## Limitations +- Minimum Next.js version: 14.0.0 +- Minimum React version: 18.3 (currently only available in Canary and Experimental releases). This version is required +because the SDK relies on the new [React Cache API](https://react.dev/reference/react/cache) +in order to share context across Server Components during rendering. - variable evaluations are only tracked in client components in App Router. ## Installation @@ -115,12 +115,7 @@ Currently, tracking events in server components is not supported. Please trigger from client components. ## Advanced -### Non-Blocking Initialization (Not Implemented) -**Note**: This feature is not yet implemented because it relies on the new React `use` function, which is only -available in canary builds currently. When `use` is released in a stable React version, the SDK will be updated to -include this feature. The documentation for the feature as it will exist continues below: - - +### Non-Blocking Initialization If you wish to render your page without waiting for the DevCycle configuration to be retrieved, you can use the `enableStreaming` option. Doing so enables the following behaviour: - the DevCycleServersideProvider will not block rendering of the rest of the server component tree diff --git a/sdk/nextjs/src/client/DevCycleClientsideProvider.tsx b/sdk/nextjs/src/client/DevCycleClientsideProvider.tsx index 363d63c3b..ac942e0e3 100644 --- a/sdk/nextjs/src/client/DevCycleClientsideProvider.tsx +++ b/sdk/nextjs/src/client/DevCycleClientsideProvider.tsx @@ -1,5 +1,5 @@ 'use client' -import React, { useRef } from 'react' +import React, { Suspense, use, useContext, useRef, useState } from 'react' import { DevCycleClient, DevCycleUser, @@ -29,36 +29,35 @@ export const DevCycleClientContext = React.createContext( {} as ClientProviderContext, ) -// /** -// * Component which renders nothing, but runs code to keep client state in sync with server -// * Also waits for the server's data promise with the `use` hook. This triggers the nearest suspense boundary, -// * so this component is being rendered inside of a Suspense by the DevCycleClientsideProvider. -// * @param serverDataPromise -// * @constructor -// */ -// TODO - re-add when React 18.3 is released with a stable "use" function -// export const SuspendedProviderInitialization = ({ -// serverDataPromise, -// }: Pick< -// DevCycleClientsideProviderProps, -// 'serverDataPromise' -// >): React.ReactElement => { -// const serverData = use(serverDataPromise) -// const [previousContext, setPreviousContext] = useState< -// DevCycleServerDataForClient | undefined -// >() -// const context = useContext(DevCycleClientContext) -// if (previousContext !== serverData) { -// // change user and config data to match latest server data -// // if the data has changed since the last invocation -// context.client.synchronizeBootstrapData( -// serverData.config, -// serverData.user, -// ) -// setPreviousContext(serverData) -// } -// return <> -// } +/** + * Component which renders nothing, but runs code to keep client state in sync with server + * Also waits for the server's data promise with the `use` hook. This triggers the nearest suspense boundary, + * so this component is being rendered inside of a Suspense by the DevCycleClientsideProvider. + * @param serverDataPromise + * @constructor + */ +export const SuspendedProviderInitialization = ({ + serverDataPromise, +}: Pick< + DevCycleClientsideProviderProps, + 'serverDataPromise' +>): React.ReactElement => { + const serverData = use(serverDataPromise) + const [previousContext, setPreviousContext] = useState< + DevCycleServerDataForClient | undefined + >() + const context = useContext(DevCycleClientContext) + if (previousContext !== serverData) { + // change user and config data to match latest server data + // if the data has changed since the last invocation + context.client.synchronizeBootstrapData( + serverData.config, + serverData.user, + ) + setPreviousContext(serverData) + } + return <> +} export const DevCycleClientsideProvider = ({ serverDataPromise, @@ -97,14 +96,13 @@ export const DevCycleClientsideProvider = ({ serverDataPromise, }} > - {/* TODO - re-add when React 18.3 is released with a stable "use" function */} - {/*{enableStreaming && (*/} - {/* */} - {/* */} - {/* */} - {/*)}*/} + {enableStreaming && ( + + + + )} {children} ) diff --git a/sdk/nextjs/src/client/useVariableValue.ts b/sdk/nextjs/src/client/useVariableValue.ts index b85d1ca04..f0e69c477 100644 --- a/sdk/nextjs/src/client/useVariableValue.ts +++ b/sdk/nextjs/src/client/useVariableValue.ts @@ -1,6 +1,6 @@ 'use client' import type { DVCVariableValue } from '@devcycle/js-client-sdk' -import { useCallback, useContext, useEffect, useState } from 'react' +import { useCallback, useContext, useEffect, useState, use } from 'react' import { VariableTypeAlias } from '@devcycle/types' import { DVCVariable } from '@devcycle/js-client-sdk' import { DevCycleClientContext } from './DevCycleClientsideProvider' @@ -13,11 +13,10 @@ export const useVariable = ( const forceRerenderCallback = useCallback(() => forceRerender({}), []) const context = useContext(DevCycleClientContext) - // TODO - re-add when React 18.3 is released with a stable "use" function // Fall back to nearest suspense boundary if client is not initialized yet. - // if (context.enableStreaming) { - // use(context.serverDataPromise) - // } + if (context.enableStreaming) { + use(context.serverDataPromise) + } useEffect(() => { context.client.subscribe( diff --git a/sdk/nextjs/src/server/DevCycleServersideProvider.tsx b/sdk/nextjs/src/server/DevCycleServersideProvider.tsx index 3848342aa..47aa24e4b 100644 --- a/sdk/nextjs/src/server/DevCycleServersideProvider.tsx +++ b/sdk/nextjs/src/server/DevCycleServersideProvider.tsx @@ -41,13 +41,11 @@ export const DevCycleServersideProvider = async ({ {children} diff --git a/sdk/nextjs/src/server/initialize.ts b/sdk/nextjs/src/server/initialize.ts index 4e226ed76..3e7794ca8 100644 --- a/sdk/nextjs/src/server/initialize.ts +++ b/sdk/nextjs/src/server/initialize.ts @@ -17,8 +17,7 @@ export type DevCycleNextOptions = DevCycleOptions & { * When this is enabled, client components will initially render using default variable values, * and will re-render when the configuration is ready. */ - // TODO - re-add when React 18.3 is released with a stable "use" function - // enableStreaming?: boolean + enableStreaming?: boolean /** * Used to disable any SDK features that require dynamic request context. This allows the SDK to be used in pages diff --git a/yarn.lock b/yarn.lock index a5e040f19..dcb157fa8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15964,7 +15964,7 @@ __metadata: prettier-eslint-cli: ~5.0.1 protobufjs: ^7.2.5 protobufjs-cli: ^1.1.2 - react: ^18.2.0 + react: 18.2.0 react-bootstrap: ^2.2.1 react-dom: 18.2.0 react-is: 18.2.0 @@ -28860,7 +28860,7 @@ __metadata: languageName: node linkType: hard -"react@npm:*, react@npm:^18.2.0": +"react@npm:*, react@npm:18.2.0": version: 18.2.0 resolution: "react@npm:18.2.0" dependencies: