diff --git a/apps/public/content/docs/sdks/react.mdx b/apps/public/content/docs/sdks/react.mdx index 81dbe49e7..a3f442f58 100644 --- a/apps/public/content/docs/sdks/react.mdx +++ b/apps/public/content/docs/sdks/react.mdx @@ -1,5 +1,325 @@ ---- -title: React ---- -Use [script tag](/docs/sdks/script) or [Web SDK](/docs/sdks/web) for now. We'll add a dedicated react sdk soon. +## About + +The React SDK provides a clean, idiomatic way to use OpenPanel in React applications. Built on `@openpanel/sdk`, it follows the same architecture as the React Native SDK and includes: + +- ✅ React Context API with `useOpenPanel()` hook +- ✅ Full TypeScript support +- ✅ SSR-safe (Next.js App Router & Pages Router) +- ✅ Zero configuration with environment variables +- ✅ Re-exports all SDK types and utilities + +## Installation + + +### Install dependencies + + + + ```bash + npm install @openpanel/react + ``` + + + ```bash + pnpm add @openpanel/react + ``` + + + ```bash + yarn add @openpanel/react + ``` + + + +This package requires React 18+ or React 19+. + +### Initialize + +Wrap your app with the `OpenPanelProvider` in your root component. + + + + ```tsx title="app/layout.tsx" + import { OpenPanelProvider } from '@openpanel/react'; + + export default function RootLayout({ + children + }: { + children: React.ReactNode + }) { + return ( + + + + {children} + + + + ); + } + ``` + + + ```tsx title="pages/_app.tsx" + import { OpenPanelProvider } from '@openpanel/react'; + import type { AppProps } from 'next/app'; + + export default function App({ Component, pageProps }: AppProps) { + return ( + + + + ); + } + ``` + + + ```tsx title="main.tsx" + import { OpenPanelProvider } from '@openpanel/react'; + import { App } from './App'; + + export function Root() { + return ( + + + + ); + } + ``` + + + +### Using Environment Variables + +The provider automatically reads from environment variables if no `clientId` prop is provided: + +```tsx + + {children} + +``` + +Create a `.env.local` file (Next.js) or `.env` file: + +```bash title=".env.local" +NEXT_PUBLIC_OPENPANEL_CLIENT_ID=your_client_id_here +``` + +The SDK checks for (in order): +1. `NEXT_PUBLIC_OPENPANEL_CLIENT_ID` (Next.js client-side) +2. `OPENPANEL_CLIENT_ID` (server-side or other environments) + +#### Options + + + + + + +## Usage + +### Client Components + +Use the `useOpenPanel()` hook in any component wrapped by the provider: + +```tsx title="components/subscribe-button.tsx" +'use client'; + +import { useOpenPanel } from '@openpanel/react'; + +export function SubscribeButton() { + const op = useOpenPanel(); + + const handleClick = () => { + op?.track('button_clicked', { + button_name: 'subscribe', + page: 'homepage' + }); + }; + + return ; +} +``` + +### Tracking Events + +Track custom events with properties: + +```tsx +const op = useOpenPanel(); + +op?.track('purchase', { + product_id: 'prod-123', + amount: 99.99, + currency: 'USD' +}); +``` + +### Identifying Users + +Identify users to associate events with specific profiles: + +```tsx +const op = useOpenPanel(); + +op?.identify({ + profileId: 'user-123', + email: 'user@example.com', + name: 'John Doe', + properties: { + plan: 'premium', + company: 'Acme Inc' + } +}); +``` + +### Setting Global Properties + +Set properties that will be sent with every event: + +```tsx +const op = useOpenPanel(); + +op?.setGlobalProperties({ + app_version: '1.0.2', + environment: 'production' +}); +``` + +### Incrementing Properties + +Increment a numeric property on a user profile: + +```tsx +const op = useOpenPanel(); + +op?.increment({ + profileId: 'user-123', + property: 'page_views', + value: 1 +}); +``` + +### Decrementing Properties + +Decrement a numeric property on a user profile: + +```tsx +const op = useOpenPanel(); + +op?.decrement({ + profileId: 'user-123', + property: 'credits', + value: 5 +}); +``` + +### Clearing User Data + +Clear the current user's data (useful for logout): + +```tsx +const op = useOpenPanel(); + +op?.clear(); +``` + +## Server-Side Rendering (SSR) + +The provider is SSR-safe and works seamlessly with Next.js: + +- ✅ OpenPanel client only initializes in the browser +- ✅ No hydration errors +- ✅ Safe to use in Server Components (wrap client components that use the hook) + +### Example with Server Component + +```tsx title="app/page.tsx" +import { AnalyticsButton } from './analytics-button'; + +export default function Page() { + return ( +
+

My Page

+ +
+ ); +} +``` + +```tsx title="app/analytics-button.tsx" +'use client'; + +import { useOpenPanel } from '@openpanel/react'; + +export function AnalyticsButton() { + const op = useOpenPanel(); + + return ( + + ); +} +``` + +## TypeScript Support + +All types from `@openpanel/sdk` are re-exported for your convenience: + +```tsx +import { + useOpenPanel, + type OpenPanelOptions, + type TrackProperties +} from '@openpanel/react'; + +const properties: TrackProperties = { + product_id: 'prod-123', + amount: 99.99 +}; + +const op = useOpenPanel(); +op?.track('purchase', properties); +``` + +## Advanced Usage + +### Server-Side Tracking (Next.js) + +For server-side event tracking in Next.js, use the JavaScript SDK directly: + +```tsx title="utils/op.ts" +import { OpenPanel } from '@openpanel/sdk'; + +export const opServer = new OpenPanel({ + clientId: 'your-client-id', + clientSecret: 'your-client-secret', +}); +``` + +Then use it in server components or API routes: + +```tsx title="app/api/subscribe/route.ts" +import { opServer } from '@/utils/op'; + +export async function POST() { + await opServer.track('user_subscribed', { + plan: 'premium' + }); + + return Response.json({ success: true }); +} +``` + +For more information on server-side tracking, refer to the [Next.js SDK](/docs/sdks/nextjs#server-side) documentation. \ No newline at end of file diff --git a/packages/sdks/react/index.tsx b/packages/sdks/react/index.tsx new file mode 100644 index 000000000..6d73dc4bc --- /dev/null +++ b/packages/sdks/react/index.tsx @@ -0,0 +1,110 @@ +/** + * OpenPanel React Provider + * + * A React context provider for OpenPanel analytics that provides a clean, + * idiomatic way to use OpenPanel in React/Next.js applications. + * + * This is temporary until OpenPanel publishes an official React/Next.js package. + * + * @see https://openpanel.dev + */ + +'use client'; + +import type { OpenPanelOptions } from '@openpanel/sdk'; +import { OpenPanel as OpenPanelBase } from '@openpanel/sdk'; +import { type ReactNode, createContext, useContext, useRef } from 'react'; + +export * from '@openpanel/sdk'; + +/** + * React-specific OpenPanel client + */ +export class OpenPanel extends OpenPanelBase { + constructor(options: OpenPanelOptions) { + super({ + ...options, + sdk: 'react', + sdkVersion: '19.0.0', + }); + } +} + +/** + * Configuration options for the OpenPanel provider + */ +interface OpenPanelProviderProps extends Omit { + /** React children to render */ + children: ReactNode; + /** OpenPanel client ID. Falls back to environment variables if not provided */ + clientId?: string; +} + +const OpenPanelContext = createContext(null); + +/** + * Hook to access the OpenPanel client instance + * + * @throws {Error} If used outside of OpenPanelProvider + * @returns OpenPanel client instance or null if not initialized + * + * @example + * ```tsx + * const openpanel = useOpenPanel(); + * openpanel?.track('button_clicked', { label: 'Subscribe' }); + * ``` + */ +export function useOpenPanel() { + const context = useContext(OpenPanelContext); + if (context === undefined) { + throw new Error('useOpenPanel must be used within OpenPanelProvider'); + } + return context; +} + +/** + * Provider component that initializes and provides OpenPanel to child components + * + * Automatically reads from environment variables: + * - NEXT_PUBLIC_OPENPANEL_CLIENT_ID + * - OPENPANEL_CLIENT_ID + * + * @param children - React children to render + * @param clientId - OpenPanel client ID. Falls back to environment variables if not provided + * @param options - Additional OpenPanel configuration options + * + * @example + * ```tsx + * + * + * + * ``` + */ +export function OpenPanelProvider({ + children, + clientId, + ...options +}: OpenPanelProviderProps) { + const openpanelRef = useRef(null); + + if (!openpanelRef.current && typeof window !== 'undefined') { + const id = + clientId || + process.env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID || + process.env.OPENPANEL_CLIENT_ID || + ''; + + if (id) { + openpanelRef.current = new OpenPanel({ + clientId: id, + ...options, + }); + } + } + + return ( + + {children} + + ); +} diff --git a/packages/sdks/react/package.json b/packages/sdks/react/package.json new file mode 100644 index 000000000..c158e847a --- /dev/null +++ b/packages/sdks/react/package.json @@ -0,0 +1,23 @@ +{ + "name": "@openpanel/react", + "version": "1.0.1-local", + "module": "index.ts", + "scripts": { + "build": "rm -rf dist && tsup", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@openpanel/sdk": "workspace:1.0.0-local" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "devDependencies": { + "@openpanel/tsconfig": "workspace:*", + "@types/node": "catalog:", + "@types/react": "catalog:", + "tsup": "^7.2.0", + "typescript": "catalog:" + } +} diff --git a/packages/sdks/react/tsconfig.json b/packages/sdks/react/tsconfig.json new file mode 100644 index 000000000..fa4341f12 --- /dev/null +++ b/packages/sdks/react/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@openpanel/tsconfig/base.json", + "compilerOptions": { + "incremental": false, + "outDir": "dist" + }, + "exclude": ["dist"] +} diff --git a/packages/sdks/react/tsup.config.ts b/packages/sdks/react/tsup.config.ts new file mode 100644 index 000000000..ce043ebef --- /dev/null +++ b/packages/sdks/react/tsup.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + format: ['cjs', 'esm'], + entry: ['index.tsx'], + external: ['react', 'react-dom'], + dts: true, + splitting: false, + sourcemap: false, + clean: true, + minify: true, +});