Skip to content

Commit 773863a

Browse files
App directory support and example (#34)
* update package dependencies * fix edge runtime variant helper * update to example to canary nextjs * add server component * add package exports for server-only * add nextjs app route * fix core ip dependency * update lib dependencies * update example dependencies * add app directory to readme * app directory example * add app router example to readme * fix typo * fix module resolution * ts update * fix eslint * update app directory compilation * Update example/src/app/app-page/page.tsx Co-authored-by: Thomas Heartman <thomasheartman+github@gmail.com> * Update README.md Co-authored-by: Thomas Heartman <thomasheartman+github@gmail.com> * Update README.md Co-authored-by: Thomas Heartman <thomasheartman+github@gmail.com> * improve readme wording * fix eslint ts5 issue * Update README.md * Update README.md * Update README.md --------- Co-authored-by: Thomas Heartman <thomasheartman+github@gmail.com>
1 parent 69fa718 commit 773863a

31 files changed

+811
-534
lines changed

README.md

+61-20
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,68 @@ You can use both to have different values on client-side and server-side.
5454

5555
# Usage
5656

57-
## A). Client-side only - simple use case and for development purposes (CSR)
57+
## A). 🌟 **App directory** (new)
58+
59+
This package is ready for server-side use with [App Router](https://nextjs.org/docs/app/building-your-application/routing).
60+
61+
Refer to [`./example/README.md#App-router`](https://github.com/Unleash/unleash-client-nextjs/tree/main/example#app-router) for an implementation example.
62+
63+
```tsx
64+
import { cookies } from "next/headers";
65+
import { evaluateFlags, flagsClient, getDefinitions } from "@unleash/nextjs";
66+
67+
const getFlag = async () => {
68+
const cookieStore = cookies();
69+
const sessionId =
70+
cookieStore.get("unleash-session-id")?.value ||
71+
`${Math.floor(Math.random() * 1_000_000_000)}`;
72+
73+
const definitions = await getDefinitions({
74+
fetchOptions: {
75+
next: { revalidate: 15 }, // Cache layer like Unleash Proxy!
76+
},
77+
});
78+
79+
const { toggles } = await evaluateFlags(definitions, {
80+
sessionId,
81+
});
82+
const flags = flagsClient(toggles);
83+
84+
return flags.isEnabled("nextjs-example");
85+
};
86+
87+
export default async function Page() {
88+
const isEnabled = await getFlag();
89+
90+
return (
91+
<p>
92+
Feature flag is{" "}
93+
<strong>
94+
<code>{isEnabled ? "ENABLED" : "DISABLED"}</code>
95+
</strong>
96+
.
97+
</p>
98+
);
99+
}
100+
```
101+
102+
## B). Middleware
103+
104+
It's possible to run this SDK in Next.js Edge Middleware. This is a great use case for A/B testing, where you can transparently redirect users to different pages based on a feature flag. Target pages can be statically generated, improving performance.
105+
106+
Refer to [`./example/README.md#Middleware`](https://github.com/Unleash/unleash-client-nextjs/tree/main/example#middleware) for an implementation example.
107+
108+
## C). Client-side only - simple use case and for development purposes (CSR)
58109

59110
Fastest way to get started is to connect frontend directly to Unleash.
60111
You can find out more about direct [Front-end API access](https://docs.getunleash.io/reference/front-end-api) in our documentation,
61112
including a guide on how to [setup a client-side SDK key](https://docs.getunleash.io/how-to/how-to-create-api-tokens).
62113

114+
Important: Hooks and provider are only available in `@unleash/nextjs/client`.
115+
63116
```tsx
64117
import type { AppProps } from "next/app";
65-
import { FlagProvider } from "@unleash/nextjs";
118+
import { FlagProvider } from "@unleash/nextjs/client";
66119

67120
export default function App({ Component, pageProps }: AppProps) {
68121
return (
@@ -76,7 +129,7 @@ export default function App({ Component, pageProps }: AppProps) {
76129
With `<FlagProvider />` in place you can now use hooks like: `useFlag`, `useVariant`, or `useFlagsStatus` to block rendering until flags are ready.
77130

78131
```jsx
79-
import { useFlag } from "@unleash/nextjs";
132+
import { useFlag } from "@unleash/nextjs/client";
80133

81134
const YourComponent = () => {
82135
const isEnabled = useFlag("nextjs-example");
@@ -102,7 +155,7 @@ Optionally, you can configure `FlagProvider` with the `config` prop. It will tak
102155

103156
If you only plan to use [Unleash client-side React SDK](https://github.com/Unleash/proxy-client-react) now also works with Next.js. Check documentation there for more examples.
104157

105-
## B). Static Site Generation, optimized performance (SSG)
158+
## D). Static Site Generation, optimized performance (SSG)
106159

107160
With same access as in the client-side example above you can resolve Unleash feature flags when building static pages.
108161

@@ -155,7 +208,7 @@ The same approach will work for [ISR (Incremental Static Regeneration)](https://
155208

156209
Both `getDefinitions()` and `getFrontendFlags()` can take arguments overriding URL, token and other request parameters.
157210

158-
## C). Server Side Rendering (SSR)
211+
## E). Server Side Rendering (SSR)
159212

160213
```tsx
161214
import {
@@ -198,7 +251,7 @@ export const getServerSideProps: GetServerSideProps<Data> = async (ctx) => {
198251
export default ExamplePage;
199252
```
200253

201-
## D). Bootstrapping / rehydration
254+
## F). Bootstrapping / rehydration
202255

203256
You can bootstrap Unleash React SDK to have values loaded from the start.
204257
Initial value can be customized server-side.
@@ -250,12 +303,6 @@ CustomApp.getInitialProps = async (ctx: AppContext) => {
250303
};
251304
```
252305

253-
## E). Middleware
254-
255-
It's possible to run this SDK in Next.js Edge Middleware. This is a great use case for A/B testing, where you can transparently redirect users to different pages based on a feature flag. Target pages can be statically generated, improving performance.
256-
257-
See [`./example/README.md#Middleware`](https://github.com/Unleash/unleash-client-nextjs/blob/main/example/README.md#middleware)
258-
259306
# ⚗️ CLI (experimental)
260307

261308
You can use `unleash [action] [options]` in your `package.json` `scripts` section, or with:
@@ -290,13 +337,7 @@ UNLEASH_SERVER_API_TOKEN=test-server:default.8a090f30679be7254af997864d66b86e44d
290337
npx @unleash/nextjs generate-types ./unleash.ts
291338
```
292339

293-
# What's next
294-
295-
## Experimental features support
296-
297-
Unleash Next.js SDK can run on [Edge Runtime](https://nextjs.org/docs/api-reference/edge-runtime) and in [Middleware](https://nextjs.org/docs/advanced-features/middleware). We are also interested in providing an example with [App Directory](https://beta.nextjs.org/docs/app-directory-roadmap).
298-
299-
## Known limitation
340+
# Known limitation
300341

301-
- In current interation server-side SDK does not support metrics.
342+
- In current interation **server-side SDK does not support metrics**.
302343
- When used server-side, this SDK does not support the "Hostname" and "IP" strategies. Use custom context fields and constraints instead.

common/eslint-config-custom/package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
"license": "MIT",
66
"private": true,
77
"dependencies": {
8-
"eslint": "^8.33.0",
9-
"eslint-config-next": "13.1.6",
8+
"eslint": "^8.40.0",
9+
"eslint-config-next": "13.4.1",
1010
"eslint-config-prettier": "^8.6.0",
1111
"eslint-plugin-react": "7.32.2",
1212
"eslint-config-turbo": "latest"
1313
},
1414
"devDependencies": {
15-
"typescript": "^4.9.5"
15+
"typescript": "^5.0.4"
1616
},
1717
"publishConfig": {
1818
"access": "public"

common/tsconfig/nextjs.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,16 @@
88
"allowJs": true,
99
"skipLibCheck": true,
1010
"strict": true,
11+
"moduleResolution": "bundler",
1112
"forceConsistentCasingInFileNames": true,
1213
"noEmit": true,
1314
"incremental": true,
1415
"esModuleInterop": true,
1516
"module": "esnext",
1617
"resolveJsonModule": true,
1718
"isolatedModules": true,
18-
"jsx": "preserve"
19+
"jsx": "preserve",
20+
"plugins": [{ "name": "next" }]
1921
},
2022
"include": ["src", "next-env.d.ts"],
2123
"exclude": ["node_modules"]

example/.vscode/settings.json

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"typescript.tsdk": "../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib",
3+
"typescript.enablePromptUseWorkspaceTsdk": true
4+
}

example/README.md

+13-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
## Getting Started
2-
3-
First, run the development server:
1+
## Next.js with Unleash
42

3+
To run this code locally:
54
```bash
6-
yarn dev
5+
git clone https://github.com/Unleash/unleash-client-nextjs.git
6+
cd unleash-client-nextjs/example
7+
pnpm install
8+
pnpm dev
79
```
810

911
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
@@ -12,6 +14,13 @@ Flag in use is `nextjs-example`. https://app.unleash-hosted.com/demo/projects/de
1214

1315
## Available examples
1416

17+
### App Router
18+
19+
- `./src/app/app-page/page.tsx` - Server-side component page, with loader
20+
- `./src/app/api-route/route.tsx` - JSON API response
21+
22+
### Pages Router
23+
1524
- `./src/pages/csr.tsx` - Client-side rendering - simple use case, with loader
1625
- `./src/pages/ssr.tsx` - Server-side rendering - when you need to keep some data private
1726
- `./src/pages/ssg.tsx` - Static site generation - performance optimization

example/next-env.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/// <reference types="next" />
22
/// <reference types="next/image-types/global" />
3+
/// <reference types="next/navigation-types/compat/navigation" />
34

45
// NOTE: This file should not be edited
56
// see https://nextjs.org/docs/basic-features/typescript for more information.

example/next.config.js

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
module.exports = {
1+
/** @type {import('next').NextConfig} */
2+
const nextConfig = {
23
reactStrictMode: true,
3-
transpilePackages: ["ui"],
4-
};
4+
experimental: {
5+
appDir: true,
6+
typedRoutes: true,
7+
}
8+
}
9+
10+
module.exports = nextConfig

example/package.json

+10-10
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,21 @@
1111
"dependencies": {
1212
"@unleash/nextjs": "workspace:*",
1313
"@vercel/examples-ui": "1.0.5",
14-
"next": "13.2.3",
14+
"next": "13.4.1",
1515
"react": "18.2.0",
1616
"react-dom": "18.2.0"
1717
},
1818
"devDependencies": {
19-
"@babel/core": "7.21.0",
20-
"@types/node": "18.14.5",
21-
"@types/react": "18.0.28",
22-
"@types/react-dom": "18.0.11",
23-
"autoprefixer": "^10.4.13",
24-
"eslint": "8.35.0",
19+
"@babel/core": "7.21.8",
20+
"@types/node": "18.16.5",
21+
"@types/react": "18.2.6",
22+
"@types/react-dom": "18.2.4",
23+
"autoprefixer": "^10.4.14",
24+
"eslint": "8.40.0",
2525
"eslint-config-custom": "workspace:*",
26-
"postcss": "8.4.21",
27-
"tailwindcss": "3.2.7",
26+
"postcss": "8.4.23",
27+
"tailwindcss": "3.3.2",
2828
"tsconfig": "workspace:*",
29-
"typescript": "4.9.5"
29+
"typescript": "5.0.4"
3030
}
3131
}

example/src/app/api-route/route.ts

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import {
2+
evaluateFlags,
3+
flagsClient,
4+
getDefinitions,
5+
} from "@unleash/nextjs";
6+
import type { NextRequest } from "next/server";
7+
export const runtime = "edge";
8+
export const preferredRegion = "fra1";
9+
10+
const COOKIE_NAME = "unleash-session-id";
11+
12+
export async function GET(request: NextRequest) {
13+
const sessionId =
14+
request.cookies.get(COOKIE_NAME)?.value ||
15+
`${Math.floor(Math.random() * 1_000_000_000)}`;
16+
17+
const headers = {
18+
"Content-Type": "application/json",
19+
"Access-Control-Allow-Origin": "*",
20+
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
21+
"Access-Control-Allow-Headers": "Content-Type, Authorization",
22+
"Set-Cookie": `${COOKIE_NAME}=${sessionId}`,
23+
};
24+
25+
try {
26+
const definitions = await getDefinitions();
27+
const { toggles } = await evaluateFlags(definitions, {
28+
sessionId,
29+
});
30+
const flags = flagsClient(toggles);
31+
return new Response(
32+
JSON.stringify({
33+
activeToggles: toggles.length,
34+
exampleToggle: {
35+
url: "https://app.unleash-hosted.com/demo/projects/default/features/nextjs-example",
36+
isEnabled: flags.isEnabled("nextjs-example"),
37+
variant: flags.getVariant("nextjs-example"),
38+
},
39+
}),
40+
{
41+
status: 200,
42+
statusText: "OK",
43+
headers,
44+
}
45+
);
46+
} catch (error) {
47+
return new Response(
48+
JSON.stringify({
49+
error: (error as Error)?.message || "Internal Server Error",
50+
}),
51+
{
52+
status: 500,
53+
statusText: "Internal Server Error",
54+
headers,
55+
}
56+
);
57+
}
58+
};

example/src/app/app-page/layout.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { ReactNode, FC } from "react";
2+
import { Page } from "@vercel/examples-ui";
3+
import "@vercel/examples-ui/globals.css";
4+
5+
export default function Layout({ children }: { children: ReactNode }) {
6+
return <Page>{children}</Page>;
7+
}

example/src/app/app-page/loading.tsx

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { LoadingDots } from "@vercel/examples-ui";
2+
3+
export default function Loading() {
4+
return (
5+
<>
6+
<p>Loading&hellip;</p>
7+
<LoadingDots />
8+
</>
9+
);
10+
}

0 commit comments

Comments
 (0)