Skip to content
Closed
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
28 changes: 28 additions & 0 deletions packages/react-realtime/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# @constructive-io/react-realtime

React hooks for Constructive realtime subscriptions with React Query cache bridge.

## Usage

```tsx
import { RealtimeClient } from '@constructive-io/realtime';
import { RealtimeProvider, useConnectionState } from '@constructive-io/react-realtime';

const client = new RealtimeClient({
url: 'wss://api.example.com/graphql',
getToken: () => authStore.getToken(),
});

function App() {
return (
<RealtimeProvider client={client}>
<MyApp />
</RealtimeProvider>
);
}

function ConnectionStatus() {
const state = useConnectionState();
return <span>{state}</span>;
}
```
43 changes: 43 additions & 0 deletions packages/react-realtime/__tests__/hooks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { createSubscriptionHook } from '../src/use-subscription';
import { RealtimeContext } from '../src/context';
import type { SubscriptionFieldMeta } from '@constructive-io/realtime';

describe('createSubscriptionHook', () => {
const TEST_META: SubscriptionFieldMeta = {
fieldName: 'onContactChanged',
tableName: 'contact',
dataFieldName: 'contact',
};

it('returns a function (hook)', () => {
const hook = createSubscriptionHook(
TEST_META,
(filter) => ({
document: 'subscription { onContactChanged { event } }',
variables: filter ? { filter } : {},
})
);

expect(typeof hook).toBe('function');
});

it('accepts default cache bridge config', () => {
const hook = createSubscriptionHook(
TEST_META,
(filter) => ({
document: 'subscription { onContactChanged { event } }',
variables: filter ? { filter } : {},
}),
{ detailKey: ['contacts', 'detail'], listKey: ['contacts', 'list'] }
);

expect(typeof hook).toBe('function');
});
});

describe('RealtimeContext', () => {
it('exports a React context', () => {
expect(RealtimeContext).toBeDefined();
expect(RealtimeContext.Provider).toBeDefined();
});
});
18 changes: 18 additions & 0 deletions packages/react-realtime/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
babelConfig: false,
tsconfig: 'tsconfig.json'
}
]
},
transformIgnorePatterns: [`/node_modules/*`],
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
modulePathIgnorePatterns: ['dist/*']
};
53 changes: 53 additions & 0 deletions packages/react-realtime/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"name": "@constructive-io/react-realtime",
"version": "0.1.0",
"author": "Constructive <developers@constructive.io>",
"description": "React hooks for Constructive realtime subscriptions with React Query cache bridge",
"main": "index.js",
"module": "esm/index.js",
"types": "index.d.ts",
"homepage": "https://github.com/constructive-io/constructive",
"license": "MIT",
"publishConfig": {
"access": "public",
"directory": "dist"
},
"repository": {
"type": "git",
"url": "https://github.com/constructive-io/constructive"
},
"bugs": {
"url": "https://github.com/constructive-io/constructive/issues"
},
"scripts": {
"clean": "makage clean",
"prepack": "npm run build",
"build": "makage build",
"build:dev": "makage build --dev",
"lint": "eslint . --fix",
"test": "jest --passWithNoTests",
"test:watch": "jest --watch"
},
"dependencies": {
"@constructive-io/realtime": "workspace:^",
"@tanstack/react-query": "^5.90.21"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0"
},
"devDependencies": {
"@types/node": "^22.19.11",
"@types/react": "^19.2.14",
"makage": "^0.3.0",
"react": "^19.2.4",
"typescript": "^5.9.3"
},
"keywords": [
"graphql",
"realtime",
"subscriptions",
"react",
"react-query",
"constructive"
]
}
26 changes: 26 additions & 0 deletions packages/react-realtime/src/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* React context for the RealtimeClient.
*
* Provides a RealtimeClient instance to the component tree via
* RealtimeProvider. Hooks use useRealtimeClient() to access it.
*/
import { createContext, useContext } from 'react';
import type { RealtimeClient } from '@constructive-io/realtime';

export const RealtimeContext = createContext<RealtimeClient | null>(null);

/**
* Access the RealtimeClient from the nearest RealtimeProvider.
*
* @throws if no RealtimeProvider is found in the tree.
*/
export function useRealtimeClient(): RealtimeClient {
const client = useContext(RealtimeContext);
if (!client) {
throw new Error(
'useRealtimeClient: no RealtimeClient found. ' +
'Wrap your app with <RealtimeProvider client={...}>.'
);
}
return client;
}
9 changes: 9 additions & 0 deletions packages/react-realtime/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export { RealtimeProvider } from './provider';
export type { RealtimeProviderProps } from './provider';
export { RealtimeContext, useRealtimeClient } from './context';
export { useSubscription, createSubscriptionHook } from './use-subscription';
export type {
UseSubscriptionOptions,
CacheBridgeConfig,
} from './use-subscription';
export { useConnectionState } from './use-connection-state';
40 changes: 40 additions & 0 deletions packages/react-realtime/src/provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* RealtimeProvider — wraps your React app to provide a RealtimeClient.
*
* @example
* ```tsx
* import { RealtimeClient } from '@constructive-io/realtime';
* import { RealtimeProvider } from '@constructive-io/react-realtime';
*
* const realtime = new RealtimeClient({
* url: 'wss://api.example.com/graphql',
* getToken: () => authStore.getToken(),
* });
*
* function App() {
* return (
* <RealtimeProvider client={realtime}>
* <MyApp />
* </RealtimeProvider>
* );
* }
* ```
*/
import type { ReactNode } from 'react';
import React from 'react';
import type { RealtimeClient } from '@constructive-io/realtime';

import { RealtimeContext } from './context';

export interface RealtimeProviderProps {
client: RealtimeClient;
children: ReactNode;
}

export function RealtimeProvider({ client, children }: RealtimeProviderProps) {
return (
<RealtimeContext.Provider value={client}>
{children}
</RealtimeContext.Provider>
);
}
29 changes: 29 additions & 0 deletions packages/react-realtime/src/use-connection-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* useConnectionState — React hook for monitoring the RealtimeClient
* WebSocket connection state.
*
* @example
* ```tsx
* function ConnectionIndicator() {
* const state = useConnectionState();
* return <span>{state}</span>; // 'connected' | 'connecting' | ...
* }
* ```
*/
import { useEffect, useState } from 'react';
import type { ConnectionState } from '@constructive-io/realtime';

import { useRealtimeClient } from './context';

export function useConnectionState(): ConnectionState {
const client = useRealtimeClient();
const [state, setState] = useState<ConnectionState>(client.getConnectionState());

useEffect(() => {
setState(client.getConnectionState());
const unsubscribe = client.onConnectionStateChange(setState);
return unsubscribe;
}, [client]);

return state;
}
Loading
Loading