Skip to content

Commit 20aa283

Browse files
committed
feat: add react sdk
1 parent d9fa794 commit 20aa283

38 files changed

Lines changed: 8557 additions & 1599 deletions

eslint.config.mjs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import eslint from "@eslint/js";
22
import tseslint from "typescript-eslint";
3+
import reactPlugin from "eslint-plugin-react";
4+
import reactHooksPlugin from "eslint-plugin-react-hooks";
35

46
export default tseslint.config(
57
eslint.configs.recommended,
@@ -17,7 +19,33 @@ export default tseslint.config(
1719
"no-console": "off",
1820
},
1921
},
22+
// React-specific config for packages/react and packages/next
2023
{
21-
ignores: ["dist/**", "node_modules/**", "*.config.*"],
24+
files: ["packages/react/**/*.{ts,tsx}", "packages/next/**/*.{ts,tsx}"],
25+
plugins: {
26+
react: reactPlugin,
27+
"react-hooks": reactHooksPlugin,
28+
},
29+
languageOptions: {
30+
parserOptions: {
31+
ecmaFeatures: {
32+
jsx: true,
33+
},
34+
},
35+
},
36+
settings: {
37+
react: {
38+
version: "detect",
39+
},
40+
},
41+
rules: {
42+
...reactPlugin.configs.recommended.rules,
43+
...reactPlugin.configs["jsx-runtime"].rules,
44+
...reactHooksPlugin.configs.recommended.rules,
45+
"react/prop-types": "off",
46+
},
47+
},
48+
{
49+
ignores: ["**/dist/**", "**/node_modules/**", "**/*.config.*"],
2250
}
2351
);

package.json

Lines changed: 21 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,29 @@
11
{
2-
"name": "@replanejs/sdk",
3-
"version": "0.6.1",
4-
"description": "Dynamic configuration SDK for browser and server environments (Node.js, Deno, Bun). Powered by Replane.",
5-
"type": "module",
6-
"license": "MIT",
7-
"homepage": "https://github.com/replane-dev/replane-javascript#readme",
8-
"bugs": {
9-
"url": "https://github.com/replane-dev/replane-javascript/issues"
10-
},
11-
"repository": {
12-
"type": "git",
13-
"url": "git+https://github.com/replane-dev/replane-javascript.git"
14-
},
15-
"author": "Dmitry Tilyupo <tilyupo@gmail.com>",
16-
"files": [
17-
"dist"
18-
],
19-
"main": "./dist/index.cjs",
20-
"module": "./dist/index.js",
21-
"types": "./dist/index.d.ts",
22-
"exports": {
23-
".": {
24-
"types": "./dist/index.d.ts",
25-
"import": "./dist/index.js",
26-
"require": "./dist/index.cjs"
27-
},
28-
"./package.json": "./package.json"
29-
},
30-
"publishConfig": {
31-
"access": "public"
32-
},
33-
"keywords": [
34-
"replane",
35-
"config",
36-
"configuration",
37-
"dynamic configuration",
38-
"remote config",
39-
"feature-flags"
40-
],
41-
"sideEffects": false,
2+
"name": "replanejs",
3+
"private": true,
4+
"description": "Replane JavaScript SDKs monorepo",
425
"scripts": {
43-
"build": "pnpm run build:esm && pnpm run build:cjs",
44-
"build:esm": "tsdown",
45-
"build:cjs": "esbuild src/index.ts --platform=node --format=cjs --outfile=dist/index.cjs",
46-
"dev": "tsdown --watch",
47-
"test": "vitest run",
48-
"typecheck": "tsc --noEmit",
49-
"lint": "eslint src tests --max-warnings 0",
50-
"lint:fix": "eslint src tests --fix",
51-
"format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\" \"README.md\"",
52-
"format:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\" \"README.md\"",
53-
"release": "pnpm run build && bumpp && npm publish"
6+
"build": "pnpm -r build",
7+
"test": "pnpm -r test",
8+
"typecheck": "pnpm -r typecheck",
9+
"lint": "eslint packages --max-warnings 0",
10+
"lint:fix": "eslint packages --fix",
11+
"format": "prettier --write \"packages/**/*.{ts,tsx}\"",
12+
"format:check": "prettier --check \"packages/**/*.{ts,tsx}\"",
13+
"clean": "pnpm -r exec rm -rf dist node_modules"
5414
},
5515
"devDependencies": {
5616
"@eslint/js": "^9.18.0",
57-
"@types/node": "^22.15.17",
58-
"@typescript-eslint/eslint-plugin": "^8.20.0",
59-
"@typescript-eslint/parser": "^8.20.0",
60-
"async-channel": "^0.2.0",
61-
"bumpp": "^10.1.0",
62-
"esbuild": "^0.23.1",
17+
"@types/node": "^25.0.3",
6318
"eslint": "^9.18.0",
64-
"prettier": "^3.4.2",
65-
"tsdown": "^0.11.9",
66-
"typescript": "^5.8.3",
67-
"typescript-eslint": "^8.20.0",
68-
"vitest": "^3.1.3"
19+
"eslint-plugin-react": "^7.37.0",
20+
"eslint-plugin-react-hooks": "^5.0.0",
21+
"prettier": "^3.4.0",
22+
"typescript": "^5.4.0",
23+
"typescript-eslint": "^8.20.0"
6924
},
70-
"packageManager": "pnpm@10.7.0+sha1.66453f13fbf9078d3db193718206a8d738afdbdb"
25+
"packageManager": "pnpm@10.25.0",
26+
"engines": {
27+
"node": ">=18.0.0"
28+
}
7129
}

packages/react/package.json

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
{
2+
"name": "@replanejs/react",
3+
"version": "0.1.0",
4+
"description": "React SDK for Replane - feature flags and remote configuration",
5+
"type": "module",
6+
"main": "./dist/index.cjs",
7+
"module": "./dist/index.js",
8+
"types": "./dist/index.d.ts",
9+
"exports": {
10+
".": {
11+
"types": "./dist/index.d.ts",
12+
"import": "./dist/index.js",
13+
"require": "./dist/index.cjs"
14+
}
15+
},
16+
"files": [
17+
"dist"
18+
],
19+
"sideEffects": false,
20+
"scripts": {
21+
"build": "tsdown",
22+
"dev": "tsdown --watch",
23+
"typecheck": "tsc --noEmit",
24+
"test": "vitest run",
25+
"test:watch": "vitest",
26+
"prepublishOnly": "pnpm run build"
27+
},
28+
"keywords": [
29+
"replane",
30+
"react",
31+
"feature-flags",
32+
"remote-config",
33+
"configuration",
34+
"sdk"
35+
],
36+
"author": "",
37+
"license": "MIT",
38+
"repository": {
39+
"type": "git",
40+
"url": "https://github.com/replane-dev/replane-javascript",
41+
"directory": "packages/react"
42+
},
43+
"bugs": {
44+
"url": "https://github.com/replane-dev/replane-javascript/issues"
45+
},
46+
"homepage": "https://github.com/replane-dev/replane-javascript#readme",
47+
"peerDependencies": {
48+
"react": ">=18.0.0"
49+
},
50+
"dependencies": {
51+
"@replanejs/sdk": "workspace:*"
52+
},
53+
"devDependencies": {
54+
"@testing-library/jest-dom": "^6.9.1",
55+
"@testing-library/react": "^16.3.1",
56+
"@types/node": "^22.19.3",
57+
"@types/react": "^18.2.0",
58+
"@types/react-dom": "^18.3.7",
59+
"jsdom": "^27.3.0",
60+
"react": "^18.2.0",
61+
"react-dom": "^18.3.1",
62+
"tsdown": "^0.11.9",
63+
"typescript": "^5.4.0",
64+
"vitest": "^3.2.4"
65+
},
66+
"engines": {
67+
"node": ">=18.0.0"
68+
},
69+
"publishConfig": {
70+
"access": "public"
71+
}
72+
}

packages/react/src/context.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { createContext } from "react";
2+
import type { ReplaneContextValue } from "./types";
3+
4+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
5+
export const ReplaneContext = createContext<ReplaneContextValue<any> | null>(null);

packages/react/src/hooks.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { useContext, useSyncExternalStore } from "react";
2+
import { ReplaneContext } from "./context";
3+
import type { ReplaneContextValue } from "./types";
4+
5+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
6+
export function useReplane<T extends object = any>(): ReplaneContextValue<T> {
7+
const context = useContext(ReplaneContext);
8+
if (!context) {
9+
throw new Error("useReplane must be used within a ReplaneProvider");
10+
}
11+
return context as ReplaneContextValue<T>;
12+
}
13+
14+
export function useConfig<T>(
15+
name: string,
16+
options?: { context?: Record<string, string | number | boolean | null> }
17+
): T {
18+
const { client } = useReplane();
19+
20+
const value = useSyncExternalStore(
21+
(onStoreChange) => {
22+
return client.subscribe(name, onStoreChange);
23+
},
24+
() => client.get(name, options) as T,
25+
() => client.get(name, options) as T
26+
);
27+
28+
return value;
29+
}

packages/react/src/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export { ReplaneProvider } from "./provider";
2+
export { useReplane, useConfig } from "./hooks";
3+
export { clearSuspenseCache } from "./useReplaneClient";
4+
export type {
5+
ReplaneProviderProps,
6+
ReplaneProviderWithClientProps,
7+
ReplaneProviderWithOptionsProps,
8+
ReplaneContextValue,
9+
} from "./types";

packages/react/src/provider.tsx

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { useMemo } from "react";
2+
import { ReplaneContext } from "./context";
3+
import { useReplaneClient, useReplaneClientSuspense } from "./useReplaneClient";
4+
import type {
5+
ReplaneProviderProps,
6+
ReplaneProviderWithClientProps,
7+
ReplaneProviderWithOptionsProps,
8+
ReplaneContextValue,
9+
} from "./types";
10+
import { hasClient } from "./types";
11+
12+
/**
13+
* Internal provider component for pre-created client.
14+
*/
15+
function ReplaneProviderWithClient<T extends object>({
16+
client,
17+
children,
18+
}: ReplaneProviderWithClientProps<T>) {
19+
const value = useMemo<ReplaneContextValue<T>>(() => ({ client }), [client]);
20+
return <ReplaneContext.Provider value={value}>{children}</ReplaneContext.Provider>;
21+
}
22+
23+
/**
24+
* Internal provider component for options-based client creation (non-suspense).
25+
*/
26+
function ReplaneProviderWithOptions<T extends object>({
27+
options,
28+
children,
29+
loader,
30+
onError,
31+
}: ReplaneProviderWithOptionsProps<T>) {
32+
const state = useReplaneClient<T>(options, onError);
33+
34+
if (state.status === "loading") {
35+
return <>{loader ?? null}</>;
36+
}
37+
38+
if (state.status === "error") {
39+
// Error was already reported via onError callback
40+
// Return loader or null to prevent rendering children without a client
41+
return <>{loader ?? null}</>;
42+
}
43+
44+
const value: ReplaneContextValue<T> = { client: state.client };
45+
return <ReplaneContext.Provider value={value}>{children}</ReplaneContext.Provider>;
46+
}
47+
48+
/**
49+
* Internal provider component for options-based client creation with Suspense.
50+
*/
51+
function ReplaneProviderWithSuspense<T extends object>({
52+
options,
53+
children,
54+
}: ReplaneProviderWithOptionsProps<T>) {
55+
const client = useReplaneClientSuspense<T>(options);
56+
const value = useMemo<ReplaneContextValue<T>>(() => ({ client }), [client]);
57+
return <ReplaneContext.Provider value={value}>{children}</ReplaneContext.Provider>;
58+
}
59+
60+
/**
61+
* Provider component that makes a ReplaneClient available to the component tree.
62+
*
63+
* Can be used in two ways:
64+
*
65+
* 1. With a pre-created client:
66+
* ```tsx
67+
* const client = await createReplaneClient({ ... });
68+
* <ReplaneProvider client={client}>
69+
* <App />
70+
* </ReplaneProvider>
71+
* ```
72+
*
73+
* 2. With options (client managed internally):
74+
* ```tsx
75+
* <ReplaneProvider
76+
* options={{ baseUrl: '...', sdkKey: '...' }}
77+
* loader={<LoadingSpinner />}
78+
* >
79+
* <App />
80+
* </ReplaneProvider>
81+
* ```
82+
*
83+
* 3. With Suspense:
84+
* ```tsx
85+
* <Suspense fallback={<LoadingSpinner />}>
86+
* <ReplaneProvider
87+
* options={{ baseUrl: '...', sdkKey: '...' }}
88+
* suspense
89+
* >
90+
* <App />
91+
* </ReplaneProvider>
92+
* </Suspense>
93+
* ```
94+
*/
95+
export function ReplaneProvider<T extends object>(props: ReplaneProviderProps<T>) {
96+
if (hasClient(props)) {
97+
return <ReplaneProviderWithClient {...props} />;
98+
}
99+
100+
if (props.suspense) {
101+
return <ReplaneProviderWithSuspense {...props} />;
102+
}
103+
104+
return <ReplaneProviderWithOptions {...props} />;
105+
}

0 commit comments

Comments
 (0)