Skip to content
Open
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
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,59 @@ const { liquidity, refreshLiquidity } = useBridgeLiquidity({

`BridgeCompare` uses this data to prioritize high-liquidity routes and warn on low-liquidity paths.

## Next.js SSR Compatibility

BridgeWise UI components now support server-side rendering (SSR) with Next.js App Router and Pages Router.

### Basic Usage

```tsx
import { BridgeStatus, ClientOnly } from '@bridgewise/ui-components';

// Safe for SSR - renders skeleton during server-side render
export default function BridgePage() {
return (
<ClientOnly fallback={<div>Loading bridge...</div>}>
<BridgeStatus chainId={1} />
</ClientOnly>
);
}
```

### Next.js Dynamic Import

For maximum compatibility, use the Next.js adapter:

```tsx
import { BridgeStatusDynamic, BridgeCompareDynamic } from '@bridgewise/next-adapter';

export default function BridgePage() {
return (
<div>
<BridgeStatusDynamic chainId={1} />
<BridgeCompareDynamic />
</div>
);
}
```

### SSR Utilities

Use built-in utilities for browser-only code:

```tsx
import { useIsClient, safeStorage, createBrowserGuard } from '@bridgewise/ui-components';

function MyComponent() {
const isClient = useIsClient();

if (!isClient) return <div>Server rendering...</div>;

const stored = safeStorage.get('user-prefs', '{}');
return <div>Client ready: {stored}</div>;
}
```

## Project setup

```bash
Expand Down
1 change: 1 addition & 0 deletions libs/ui-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
".": "./src/index.ts",
"./theme": "./src/theme/index.ts",
"./headless": "./src/components/headless/index.ts",
"./ssr": "./src/ssr-utils/index.ts",
"./styles/globals.css": "./src/styles/globals.css"
},
"peerDependencies": {
Expand Down
20 changes: 20 additions & 0 deletions libs/ui-components/src/components/SSR/ClientOnly.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use client';

import { type ReactNode } from 'react';
import { useIsClient } from '../../ssr-hooks/useIsClient';

export interface ClientOnlyProps {
/** Content rendered only after client-side hydration. */
children: ReactNode;
/** Optional fallback rendered during SSR / before hydration. */
fallback?: ReactNode;
}

/**
* Renders its children only on the client.
* During SSR (and the first client render) the `fallback` is shown instead.
*/
export function ClientOnly({ children, fallback = null }: ClientOnlyProps) {
const isClient = useIsClient();
return <>{isClient ? children : fallback}</>;
}
13 changes: 13 additions & 0 deletions libs/ui-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,16 @@ export type {
WalletProviderProps,
WalletContextValue,
} from './wallet';

// SSR Compatibility Utils
export { isServer, isClient } from './ssr-utils/env';
export { safeStorage } from './ssr-utils/safe-storage';
export { createBrowserGuard, ServerAccessError } from './ssr-utils/browser-guard';

// SSR Hooks
export { useIsClient } from './ssr-hooks/useIsClient';
export { useIsomorphicLayoutEffect } from './ssr-hooks/useIsomorphicLayoutEffect';

// SSR Components
export { ClientOnly } from './components/SSR/ClientOnly';
export type { ClientOnlyProps } from './components/SSR/ClientOnly';
2 changes: 2 additions & 0 deletions libs/ui-components/src/ssr-hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './useIsClient';
export * from './useIsomorphicLayoutEffect';
17 changes: 17 additions & 0 deletions libs/ui-components/src/ssr-hooks/useIsClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use client';

import { useState, useEffect } from 'react';

/**
* Returns `true` once the component has mounted on the client.
* Useful for guarding browser-only code inside components.
*/
export function useIsClient(): boolean {
const [isClient, setIsClient] = useState(false);

useEffect(() => {
setIsClient(true);
}, []);

return isClient;
}
12 changes: 12 additions & 0 deletions libs/ui-components/src/ssr-hooks/useIsomorphicLayoutEffect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use client';

import { useEffect, useLayoutEffect } from 'react';
import { isServer } from '../utils/env';

/**
* `useLayoutEffect` on the client, `useEffect` on the server.
* Avoids the React SSR warning about useLayoutEffect.
*/
export const useIsomorphicLayoutEffect = isServer
? useEffect
: useLayoutEffect;
41 changes: 41 additions & 0 deletions libs/ui-components/src/ssr-utils/browser-guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { isServer } from './env';

/**
* Error thrown when a browser-only API is accessed on the server.
*/
export class ServerAccessError extends Error {
constructor(api: string) {
super(
`[BridgeWise] "${api}" is not available during server-side rendering. ` +
`Wrap the call in a useEffect or use the <ClientOnly> component.`,
);
this.name = 'ServerAccessError';
}
}

/**
* Creates a proxy that throws `ServerAccessError` when any property is
* accessed on the server, and delegates to `factory()` on the client.
*
* @example
* ```ts
* const ethereum = createBrowserGuard('window.ethereum', () => window.ethereum);
* ```
*/
export function createBrowserGuard<T extends object>(
name: string,
factory: () => T,
): T {
if (!isServer) {
return factory();
}

return new Proxy({} as T, {
get(_target, prop) {
// Allow Symbol.toPrimitive / toJSON etc. to avoid noisy errors in SSR
// serialisation paths.
if (typeof prop === 'symbol') return undefined;
throw new ServerAccessError(`${name}.${String(prop)}`);
},
});
}
9 changes: 9 additions & 0 deletions libs/ui-components/src/ssr-utils/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Environment detection utilities for SSR compatibility.
*/

/** Returns `true` when running on the server (no `window` global). */
export const isServer = typeof window === 'undefined';

/** Returns `true` when running in a browser environment. */
export const isClient = !isServer;
3 changes: 3 additions & 0 deletions libs/ui-components/src/ssr-utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './env';
export * from './safe-storage';
export * from './browser-guard';
34 changes: 34 additions & 0 deletions libs/ui-components/src/ssr-utils/safe-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { isServer } from './env';

/**
* A storage wrapper that is safe to use in SSR environments.
* All operations silently no-op on the server.
*/
export const safeStorage = {
get(key: string): string | null {
if (isServer) return null;
try {
return window.localStorage.getItem(key);
} catch {
return null;
}
},

set(key: string, value: string): void {
if (isServer) return;
try {
window.localStorage.setItem(key, value);
} catch {
// Storage may be full or blocked (e.g. private browsing).
}
},

remove(key: string): void {
if (isServer) return;
try {
window.localStorage.removeItem(key);
} catch {
// Silently ignore.
}
},
};
27 changes: 27 additions & 0 deletions packages/next-adapter/dynamic.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use client';

import dynamic from 'next/dynamic';
import type { ClientOnlyProps } from '@bridgewise/ui-components';
import type { BridgeStatusProps as BridgeWidgetProps } from '@bridgewise/ui-components';

/**
* Dynamically imported `BridgeWidget` with SSR disabled.
* Drop-in replacement for App Router and Pages Router.
*/
export const BridgeStatusDynamic = dynamic<BridgeWidgetProps>(
() => import('@bridgewise/ui-components').then((m) => m.BridgeStatus),
{ ssr: false },
);

export const BridgeCompareDynamic = dynamic(
() => import('@bridgewise/ui-components').then((m) => m.BridgeCompare),
{ ssr: false },
);

/**
* Dynamically imported `ClientOnly` with SSR disabled.
*/
export const ClientOnlyDynamic = dynamic<ClientOnlyProps>(
() => import('@bridgewise/ui-components').then((m) => m.ClientOnly),
{ ssr: false },
);
5 changes: 5 additions & 0 deletions packages/next-adapter/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Re-export everything from the core UI components for convenience.
export * from '@bridgewise/ui-components';

// Next.js-specific dynamic imports (SSR disabled).
export { BridgeStatusDynamic, BridgeCompareDynamic, ClientOnlyDynamic } from './dynamic';
29 changes: 29 additions & 0 deletions packages/next-adapter/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "@bridgewise/next-adapter",
"version": "0.1.0",
"description": "Next.js adapter for BridgeWise SDK with SSR support",
"main": "src/index.ts",
"exports": {
".": "./src/index.ts",
"./dynamic": "./src/dynamic.tsx"
},
"peerDependencies": {
"@bridgewise/ui-components": "^0.1.0",
"next": ">=13.4.0",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"devDependencies": {
"typescript": "^5.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0"
},
"keywords": [
"bridgewise",
"nextjs",
"ssr",
"react",
"defi",
"bridge"
]
}