Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/examples/basic-app/node_modules
/examples/basic-app/.env
/node_modules/
/dist
yarn-error.log
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ npm install @unleash/proxy-client-react unleash-proxy-client
yarn add @unleash/proxy-client-react unleash-proxy-client
```

## Example application

To see the SDK in action, explore the [`examples/basic-app`](examples/basic-app/README.md) directory.
It contains a minimal Vite + React setup that connects to Unleash, evaluates a feature toggle,
and updates the evaluation context dynamically at runtime. The README in that folder includes step-by-step instructions
for running the example.

# How to use

This library uses the core [unleash-js-sdk](https://github.com/Unleash/unleash-js-sdk) client as a base.
Expand Down
6 changes: 6 additions & 0 deletions examples/basic-app/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
VITE_UNLEASH_URL=http://localhost:4242/api/frontend
VITE_UNLEASH_CLIENT_KEY=YOUR_FRONTEND_TOKEN
VITE_UNLEASH_APP_NAME=react-example-app
VITE_UNLEASH_REFRESH_INTERVAL=15
VITE_UNLEASH_ENVIRONMENT=development
VITE_EXAMPLE_DEFAULT_USER=guest
59 changes: 59 additions & 0 deletions examples/basic-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Unleash React SDK example

This directory contains a small Vite + React application that consumes
`@unleash/proxy-client-react`. It connects to the Unleash Frontend API and
shows how to:

- bootstrap the SDK with `FlagProvider`
- evaluate a toggle with `useFlag`
- inspect variants with `useVariant`
- update the evaluation context with `useUnleashContext`
- react to the client status via `useFlagsStatus`

## Prerequisites

You need a running [Unleash Frontend API](https://docs.getunleash.io/reference/front-end-api)
or [Unleash Edge](https://docs.getunleash.io/reference/unleash-edge) along with an
appropriate frontend token.

## Local setup

From the repository root, install the dependencies used by the example:

```bash
cd examples/basic-app
yarn install
```

The Vite configuration aliases `@unleash/proxy-client-react` to the local `src`
folder, so you do not need to build the SDK before running the demo. Swap this
alias for the npm package if you copy the example into another project.

Create a `.env` file with your connection details:

```bash
cp .env.example .env
```

and edit the file to match your environment:

```env
VITE_UNLEASH_URL=https://<your-unleash-domain>/api/frontend
VITE_UNLEASH_CLIENT_KEY=<frontend-or-pretrusted-token>
VITE_UNLEASH_APP_NAME=react-example-app
VITE_UNLEASH_REFRESH_INTERVAL=15
VITE_UNLEASH_ENVIRONMENT=development
```

> The app evaluates the `demo-app.simple-toggle` flag by default. Make sure the
> toggle exists and is available to the token you supplied.

Start the Vite dev server:

```bash
yarn dev
```

Open the printed URL (defaults to http://localhost:5173) in your browser. The
UI renders the current status for the configured toggle and exposes a small
form that lets you swap the `userId` used for evaluations.
12 changes: 12 additions & 0 deletions examples/basic-app/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Unleash React SDK Example</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
24 changes: 24 additions & 0 deletions examples/basic-app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "unleash-react-sdk-example",
"private": true,
"type": "module",
"version": "0.0.0",
"scripts": {
"dev": "vite --open",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@unleash/proxy-client-react": "file:../..",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"unleash-proxy-client": "^3.7.3"
},
"devDependencies": {
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"typescript": "^5.5.4",
"vite": "^5.4.0"
}
}
110 changes: 110 additions & 0 deletions examples/basic-app/src/App.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
:root {
color-scheme: light dark;
font-synthesis: none;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
margin: 0;
padding: 0;
background: #f6f6f6;
}

body {
margin: 0;
min-height: 100vh;
background: radial-gradient(circle at top, #ffffff 0%, #f0f4ff 100%);
}

.app {
max-width: 720px;
margin: 0 auto;
padding: 2rem 1.5rem 4rem;
color: #1d2130;
}

.app--loading {
display: flex;
flex-direction: column;
gap: 1rem;
}

header {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 2rem;
}

.card {
background: rgba(255, 255, 255, 0.88);
border-radius: 16px;
padding: 1.5rem;
box-shadow: 0 24px 60px rgba(15, 23, 42, 0.08);
backdrop-filter: blur(12px);
margin-bottom: 1.5rem;
}

.card h2 {
margin-top: 0;
}

.toggle {
padding: 0.5rem 0.75rem;
border-radius: 12px;
display: inline-block;
font-weight: 600;
}

.toggle--on {
background: rgba(34, 197, 94, 0.16);
color: #0d6831;
}

.toggle--off {
background: rgba(248, 113, 113, 0.18);
color: #991b1b;
}

.context-form {
display: flex;
flex-direction: column;
gap: 0.75rem;
}

.context-form__row {
display: flex;
gap: 0.5rem;
}

.context-form input {
flex: 1;
border-radius: 10px;
border: 1px solid rgba(148, 163, 184, 0.6);
padding: 0.6rem 0.9rem;
font-size: 1rem;
}

.context-form button {
border: none;
border-radius: 10px;
padding: 0.6rem 1.2rem;
font-size: 1rem;
background: #2563eb;
color: white;
cursor: pointer;
}

.context-form button:disabled {
opacity: 0.6;
cursor: not-allowed;
}

.context-form__error {
color: #991b1b;
margin: 0;
}

pre {
background: rgba(15, 23, 42, 0.08);
border-radius: 12px;
padding: 1rem;
overflow: auto;
}
162 changes: 162 additions & 0 deletions examples/basic-app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { FormEvent, useState } from 'react';
import {
useFlag,
useFlagsStatus,
useUnleashContext,
useVariant
} from '@unleash/proxy-client-react';
import './App.css';

const TOGGLE_NAME = 'demo-app.simple-toggle';

const VariantDetails = ({ toggleName }: { toggleName: string }) => {
const variant = useVariant(toggleName);

if (!variant?.enabled) {
return (
<p>
Variant is not enabled for <code>{toggleName}</code>.
</p>
);
}

return (
<>
<p>
<strong>Variant name:</strong> {variant.name}
</p>
{variant.payload && (
<p>
<strong>Payload:</strong> {JSON.stringify(variant.payload)}
</p>
)}
</>
);
};

const ToggleStatus = ({ toggleName }: { toggleName: string }) => {
const enabled = useFlag(toggleName);

return (
<p className={`toggle toggle--${enabled ? 'on' : 'off'}`}>
Feature <code>{toggleName}</code> is{' '}
<strong>{enabled ? 'enabled' : 'disabled'}</strong> for the current user.
</p>
);
};

const ContextForm = ({
userId,
onSubmit
}: {
userId: string;
onSubmit: (nextUserId: string) => Promise<void>;
}) => {
const [value, setValue] = useState(userId);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);

const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!value) {
setError('Provide a user id before updating the context.');
return;
}
if (value === userId) {
setError('The provided user id matches the current context.');
return;
}

setError(null);
setIsSubmitting(true);
try {
await onSubmit(value);
} catch (err) {
setError(
err instanceof Error ? err.message : 'Failed to update the Unleash context.'
);
} finally {
setIsSubmitting(false);
}
};

return (
<form className="context-form" onSubmit={handleSubmit}>
<label htmlFor="userId">Simulate another user</label>
<div className="context-form__row">
<input
id="userId"
name="userId"
value={value}
onChange={event => {
setError(null);
setValue(event.target.value);
}}
placeholder="e.g. [email protected]"
/>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Updating…' : 'Update context'}
</button>
</div>
{error && <p className="context-form__error">{error}</p>}
</form>
);
};

const App = () => {
const { flagsReady, flagsError } = useFlagsStatus();
const [currentUserId, setCurrentUserId] = useState<string>(
import.meta.env.VITE_EXAMPLE_DEFAULT_USER ?? 'guest'
);
const updateContext = useUnleashContext();

const handleContextUpdate = async (nextUserId: string) => {
await updateContext({ userId: nextUserId });
setCurrentUserId(nextUserId);
};

if (!flagsReady) {
return (
<main className="app app--loading">
<h1>Unleash React SDK Example</h1>
<p>Fetching feature toggles…</p>
</main>
);
}

return (
<main className="app">
<header>
<h1>Unleash React SDK Example</h1>
<p>
Connected to the Unleash Frontend API configured in{' '}
<code>.env</code>. The example evaluates the{' '}
<code>{TOGGLE_NAME}</code> toggle.
</p>
</header>

{flagsError && (
<section className="card">
<h2>Latest client error</h2>
<pre>{JSON.stringify(flagsError, null, 2)}</pre>
</section>
)}

<section className="card">
<h2>Toggle status</h2>
<ToggleStatus toggleName={TOGGLE_NAME} />
<VariantDetails toggleName={TOGGLE_NAME} />
</section>

<section className="card">
<h2>Current context</h2>
<p>
Evaluating toggles for <code>{currentUserId}</code>.
</p>
<ContextForm userId={currentUserId} onSubmit={handleContextUpdate} />
</section>
</main>
);
};

export default App;
Loading