@oimdb/react
Version:
React integration for OIMDB - Hooks for selection and subscription with external storage
791 lines (597 loc) • 22.1 kB
Markdown
# @oimdb/react
React integration for OIMDB - Hooks for selection and subscription with reactive collections and indexes.
## Overview
`@oimdb/react` provides React hooks that work with OIMDB reactive objects (`OIMReactiveCollection` and `OIMReactiveIndex`). The library includes both direct hooks for component-level usage and React Context utilities for application-wide data management.
## Features
- **Reactive Integration**: Hooks work with `OIMReactiveCollection` and reactive indexes from `@oimdb/core`
- **Index Type Support**: Separate hooks for SetBased indexes (return `Set<TPk>`) and ArrayBased indexes (return `TPk[]`)
- **Automatic Subscription**: Uses `useSyncExternalStore` for optimal React 18+ performance
- **Event Coalescing**: Leverages OIMDB's built-in event coalescing for efficient updates
- **Type Safety**: Full TypeScript support with advanced generic type inference
- **Context Support**: Optional React Context for centralized collection management
- **Flexible Usage**: Use hooks directly or through context provider pattern
## Installation
```bash
npm install @oimdb/react @oimdb/core
```
## Usage
### Basic Setup
```typescript
import {
OIMEventQueue,
OIMRICollection,
OIMReactiveIndexManualSetBased,
OIMReactiveIndexManualArrayBased
} from '@oimdb/core';
import {
useSelectEntitiesByPks,
useSelectEntitiesByIndexKeySetBased,
useSelectEntitiesByIndexKeyArrayBased,
useSelectEntityByPk
} from '@oimdb/react';
// Create event queue and reactive collections
const queue = new OIMEventQueue();
// Choose index type based on your needs:
// - SetBased: for frequent add/remove operations, order doesn't matter
// - ArrayBased: for full replacements or when order/sorting matters
const userTeamIndex = new OIMReactiveIndexManualSetBased<string, string>(queue);
const deckCardsIndex = new OIMReactiveIndexManualArrayBased<string, string>(queue);
const usersCollection = new OIMRICollection(queue, {
collectionOpts: { selectPk: (user: User) => user.id },
indexes: { byTeam: userTeamIndex },
});
```
### Single Entity Selection
```typescript
function UserProfile({ userId }: { userId: string }) {
const user = useSelectEntityByPk(usersCollection, userId);
if (!user) return <div>Loading...</div>;
return (
<div>
<h2>{user.name}</h2>
<p>Email: {user.email}</p>
</div>
);
}
```
### Multiple Entities Selection
```typescript
function UserList({ userIds }: { userIds: string[] }) {
const users = useSelectEntitiesByPks(usersCollection, userIds);
return (
<ul>
{users.map((user, index) => (
<li key={user?.id || index}>
{user ? user.name : 'Loading...'}
</li>
))}
</ul>
);
}
```
### Index-based Selection
OIMDB provides separate hooks for SetBased and ArrayBased indexes:
#### SetBased Indexes (returns Set)
```typescript
import { useSelectEntitiesByIndexKeySetBased, useSelectPksByIndexKeySetBased } from '@oimdb/react';
function TeamMembers({ teamId }: { teamId: string }) {
// For SetBased indexes, use SetBased hooks
const teamUsers = useSelectEntitiesByIndexKeySetBased(
usersCollection,
usersCollection.indexes.byTeam, // OIMReactiveIndexManualSetBased
teamId
);
// Or get just the PKs as Set
const teamUserIds = useSelectPksByIndexKeySetBased(
usersCollection.indexes.byTeam,
teamId
); // Returns Set<string>
return (
<div>
{teamUsers.map((user, index) => (
<div key={user?.id || index}>
{user ? `${user.name} (${user.role})` : 'Loading...'}
</div>
))}
</div>
);
}
```
#### ArrayBased Indexes (returns Array)
```typescript
import { useSelectEntitiesByIndexKeyArrayBased, useSelectPksByIndexKeyArrayBased } from '@oimdb/react';
function DeckCards({ deckId }: { deckId: string }) {
// For ArrayBased indexes, use ArrayBased hooks
const cards = useSelectEntitiesByIndexKeyArrayBased(
cardsCollection,
cardsCollection.indexes.byDeck, // OIMReactiveIndexManualArrayBased
deckId
);
// Or get just the PKs as Array (preserves order)
const cardIds = useSelectPksByIndexKeyArrayBased(
cardsCollection.indexes.byDeck,
deckId
); // Returns string[] (preserves order/sorting)
return (
<div>
{cards.map((card, index) => (
<div key={card?.id || index}>
{card ? `${index + 1}. ${card.name}` : 'Loading...'}
</div>
))}
</div>
);
}
```
## React Context Integration
For applications with multiple collections, use the React Context pattern for centralized management:
### Context Setup
```typescript
import {
OIMRICollectionsProvider,
useOIMCollectionsContext
} from '@oimdb/react';
interface User {
id: string;
name: string;
teamId: string;
}
interface Team {
id: string;
name: string;
}
function createCollections() {
const queue = new OIMEventQueue();
// Use SetBased for frequent add/remove operations
const userTeamIndex = new OIMReactiveIndexManualSetBased<string, string>(queue);
const usersCollection = new OIMRICollection(queue, {
collectionOpts: { selectPk: (user: User) => user.id },
indexes: { byTeam: userTeamIndex },
});
const teamsCollection = new OIMRICollection(queue, {
collectionOpts: { selectPk: (team: Team) => team.id },
indexes: {},
});
return { users: usersCollection, teams: teamsCollection } as const;
}
type AppCollections = ReturnType<typeof createCollections>;
```
## TypeScript Typing Strategies
For maximum type safety, you should properly type your collections dictionary. There are two main approaches, similar to how Redux handles state typing:
### Approach 1: Using `typeof` (Recommended for Simple Cases)
The simplest approach is to use TypeScript's `typeof` operator to infer types from your collection instances:
```typescript
import { OIMEventQueue, OIMRICollection, OIMReactiveIndexManual } from '@oimdb/core';
interface User {
id: string;
name: string;
teamId: string;
}
interface Team {
id: string;
name: string;
}
// Create collections
const queue = new OIMEventQueue();
const userTeamIndex = new OIMReactiveIndexManualSetBased<string, string>(queue);
const usersCollection = new OIMRICollection(queue, {
collectionOpts: { selectPk: (user: User) => user.id },
indexes: { byTeam: userTeamIndex },
});
const teamsCollection = new OIMRICollection(queue, {
collectionOpts: { selectPk: (team: Team) => team.id },
indexes: {},
});
// Infer types using typeof
const collections = {
users: usersCollection,
teams: teamsCollection,
} as const;
// Extract the type
type AppCollections = typeof collections;
```
**Usage:**
```typescript
function MyComponent() {
const { users, teams } = useOIMCollectionsContext<AppCollections>();
// users and teams are fully typed with all their generics preserved
}
```
### Approach 2: Creating Explicit Types (Recommended for Complex Projects)
For larger applications or when you need more control, create explicit type definitions similar to Redux's approach:
```typescript
import {
OIMEventQueue,
OIMRICollection,
OIMReactiveIndexManualSetBased
} from '@oimdb/core';
import type {
TOIMPk,
OIMIndexSetBased,
OIMReactiveIndexSetBased
} from '@oimdb/core';
interface User {
id: string;
name: string;
teamId: string;
}
interface Team {
id: string;
name: string;
}
// Define your collection types explicitly
type UserCollection = OIMRICollection<
User,
string,
'byTeam',
string,
OIMIndexSetBased<string, string>,
OIMReactiveIndexSetBased<string, string, OIMIndexSetBased<string, string>>
>;
type TeamCollection = OIMRICollection<
Team,
string,
never,
never,
OIMIndexSetBased<never, never>,
OIMReactiveIndexSetBased<never, never, OIMIndexSetBased<never, never>>
>;
// Define your collections dictionary type
interface AppCollections {
users: UserCollection;
teams: TeamCollection;
}
// Factory function that returns properly typed collections
function createCollections(): AppCollections {
const queue = new OIMEventQueue();
const userTeamIndex = new OIMReactiveIndexManualSetBased<string, string>(queue);
return {
users: new OIMRICollection(queue, {
collectionOpts: { selectPk: (user: User) => user.id },
indexes: { byTeam: userTeamIndex },
}) as UserCollection,
teams: new OIMRICollection(queue, {
collectionOpts: { selectPk: (team: Team) => team.id },
indexes: {},
}) as TeamCollection,
};
}
```
**Usage:**
```typescript
function MyComponent() {
const { users, teams } = useOIMCollectionsContext<AppCollections>();
// Full type safety with explicit types
}
```
### When to Use Each Approach
- **Use `typeof`** when:
- You have simple collection setups
- You want TypeScript to infer everything automatically
- You prefer less boilerplate
- Your collections are created in one place
- **Use explicit types** when:
- You need to share types across multiple files
- You want to document your data structure explicitly
- You're building a library or shared module
- You need to ensure type consistency across your application
- You prefer Redux-style explicit typing patterns
### Provider Setup
```typescript
function App() {
const collections = React.useMemo(() => createCollections(), []);
return (
<OIMRICollectionsProvider collections={collections}>
<UserDashboard />
</OIMRICollectionsProvider>
);
}
```
### Using Context in Components
```typescript
function UserDashboard() {
const { users, teams } = useOIMCollectionsContext<AppCollections>();
// Use collections with hooks
const allUsers = useSelectEntitiesByPks(users, []);
// Use appropriate hook based on index type
const teamMembers = useSelectEntitiesByIndexKeySetBased(
users,
users.indexes.byTeam, // SetBased index
'team1'
);
return (
<div>
<h2>All Users: {allUsers.length}</h2>
<h3>Team 1 Members: {teamMembers.length}</h3>
</div>
);
}
```
### Custom Context
For multiple independent contexts:
```typescript
const UserContext = createOIMCollectionsContext<{ users: typeof usersCollection }>();
function UserProvider({ children }: { children: React.ReactNode }) {
const collections = React.useMemo(() => ({ users: usersCollection }), []);
return (
<OIMRICollectionsProvider collections={collections} context={UserContext}>
{children}
</OIMRICollectionsProvider>
);
}
function UserComponent() {
const { users } = useOIMCollectionsContext(UserContext);
// Use users collection...
}
```
## API Reference
### `useSelectEntityByPk(reactiveCollection, pk)`
Subscribes to a single entity from a reactive collection.
**Parameters:**
- `reactiveCollection: OIMReactiveCollection<TEntity, TPk>` - Reactive collection instance
- `pk: TPk` - Primary key of the entity
**Returns:**
- `TEntity | undefined` - Entity data or undefined if not found
### `useSelectEntitiesByPks(reactiveCollection, pks)`
Subscribes to multiple entities from a reactive collection.
**Parameters:**
- `reactiveCollection: OIMReactiveCollection<TEntity, TPk>` - Reactive collection instance
- `pks: readonly TPk[]` - Array of primary keys
**Returns:**
- `(TEntity | undefined)[]` - Array of entities (undefined for missing entities)
### Index-based Selection Hooks
OIMDB provides separate hooks for SetBased and ArrayBased indexes to ensure type safety and correct return types.
#### SetBased Index Hooks
##### `useSelectEntitiesByIndexKeySetBased(reactiveCollection, reactiveIndex, key)`
Subscribes to entities indexed by a specific key from a SetBased index.
**Parameters:**
- `reactiveCollection: OIMReactiveCollection<TEntity, TPk>` - Reactive collection instance
- `reactiveIndex: OIMReactiveIndexSetBased<TKey, TPk, TIndex>` - SetBased reactive index instance
- `key: TKey` - Index key to query
**Returns:**
- `(TEntity | undefined)[]` - Array of entities for the given index key
##### `useSelectEntitiesByIndexKeysSetBased(reactiveCollection, reactiveIndex, keys)`
Subscribes to entities indexed by multiple keys from a SetBased index.
**Parameters:**
- `reactiveCollection: OIMReactiveCollection<TEntity, TPk>` - Reactive collection instance
- `reactiveIndex: OIMReactiveIndexSetBased<TKey, TPk, TIndex>` - SetBased reactive index instance
- `keys: readonly TKey[]` - Array of index keys to query
**Returns:**
- `(TEntity | undefined)[]` - Array of entities for the given index keys
##### `useSelectPksByIndexKeySetBased(reactiveIndex, key)`
Subscribes to primary keys indexed by a specific key from a SetBased index.
**Parameters:**
- `reactiveIndex: OIMReactiveIndexSetBased<TKey, TPk, TIndex>` - SetBased reactive index instance
- `key: TKey` - Index key to query
**Returns:**
- `Set<TPk>` - Set of primary keys for the given index key
##### `useSelectPksByIndexKeysSetBased(reactiveIndex, keys)`
Subscribes to primary keys indexed by multiple keys from a SetBased index.
**Parameters:**
- `reactiveIndex: OIMReactiveIndexSetBased<TKey, TPk, TIndex>` - SetBased reactive index instance
- `keys: readonly TKey[]` - Array of index keys to query
**Returns:**
- `Map<TKey, Set<TPk>>` - Map of index keys to their corresponding primary key Sets
#### ArrayBased Index Hooks
##### `useSelectEntitiesByIndexKeyArrayBased(reactiveCollection, reactiveIndex, key)`
Subscribes to entities indexed by a specific key from an ArrayBased index.
**Parameters:**
- `reactiveCollection: OIMReactiveCollection<TEntity, TPk>` - Reactive collection instance
- `reactiveIndex: OIMReactiveIndexArrayBased<TKey, TPk, TIndex>` - ArrayBased reactive index instance
- `key: TKey` - Index key to query
**Returns:**
- `(TEntity | undefined)[]` - Array of entities for the given index key (preserves order)
##### `useSelectEntitiesByIndexKeysArrayBased(reactiveCollection, reactiveIndex, keys)`
Subscribes to entities indexed by multiple keys from an ArrayBased index.
**Parameters:**
- `reactiveCollection: OIMReactiveCollection<TEntity, TPk>` - Reactive collection instance
- `reactiveIndex: OIMReactiveIndexArrayBased<TKey, TPk, TIndex>` - ArrayBased reactive index instance
- `keys: readonly TKey[]` - Array of index keys to query
**Returns:**
- `(TEntity | undefined)[]` - Array of entities for the given index keys (preserves order)
##### `useSelectPksByIndexKeyArrayBased(reactiveIndex, key)`
Subscribes to primary keys indexed by a specific key from an ArrayBased index.
**Parameters:**
- `reactiveIndex: OIMReactiveIndexArrayBased<TKey, TPk, TIndex>` - ArrayBased reactive index instance
- `key: TKey` - Index key to query
**Returns:**
- `TPk[]` - Array of primary keys for the given index key (preserves order/sorting)
##### `useSelectPksByIndexKeysArrayBased(reactiveIndex, keys)`
Subscribes to primary keys indexed by multiple keys from an ArrayBased index.
**Parameters:**
- `reactiveIndex: OIMReactiveIndexArrayBased<TKey, TPk, TIndex>` - ArrayBased reactive index instance
- `keys: readonly TKey[]` - Array of index keys to query
**Returns:**
- `Map<TKey, TPk[]>` - Map of index keys to their corresponding primary key arrays (preserves order)
## Context API Reference
### `OIMRICollectionsProvider<T>`
Provider component for collections context.
**Props:**
- `collections: T` - Dictionary of reactive collections
- `children: ReactNode` - React children
- `context?: React.Context<OIMContextValue<T>>` - Optional custom context
### `useOIMCollectionsContext<T>(context?)`
Hook to access collections from context.
**Parameters:**
- `context?: React.Context<OIMContextValue<T>>` - Optional custom context
**Returns:**
- `T` - Collections dictionary with full type safety
**Throws:**
- Error if used outside of provider
### `createOIMCollectionsContext<T>()`
Creates a custom collections context with specific typing.
**Returns:**
- `React.Context<OIMContextValue<T>>` - Typed React context
### Type Utilities
#### `CollectionsDictionary`
Base type for any collections dictionary. Use `typeof` to extract types from your collection instances, or define explicit types using `OIMRICollection` generics.
## Architecture
### Reactive Collections Integration
The hooks work directly with OIMDB reactive objects:
```typescript
// Use reactive collections and indexes directly
const user = useSelectEntityByPk(reactiveCollection, 'user123');
// Use appropriate hook based on index type
const posts = useSelectEntitiesByIndexKeySetBased(
reactiveCollection,
reactiveIndexSetBased, // SetBased index
'tech'
);
const orderedCards = useSelectEntitiesByIndexKeyArrayBased(
reactiveCollection,
reactiveIndexArrayBased, // ArrayBased index
'deck1'
);
```
### Event Subscription
Hooks automatically subscribe to OIMDB reactive events using `useSyncExternalStore`:
- **Collection updates**: Subscribe to `reactiveCollection.updateEventEmitter`
- **Index updates**: Subscribe to `reactiveIndex.updateEventEmitter`
- **Optimized subscriptions**: Subscribe only to specific keys for efficient updates
- **Automatic cleanup**: Unsubscribe when component unmounts
### Index Type Selection
When working with indexes, choose the appropriate hook based on your index type:
- **SetBased indexes** (`OIMReactiveIndexManualSetBased`): Use `*SetBased` hooks (e.g., `useSelectPksByIndexKeySetBased`) - returns `Set<TPk>`
- **ArrayBased indexes** (`OIMReactiveIndexManualArrayBased`): Use `*ArrayBased` hooks (e.g., `useSelectPksByIndexKeyArrayBased`) - returns `TPk[]` (preserves order)
This ensures type safety and correct return types. TypeScript will enforce the correct hook usage based on your index type.
### Performance
- **React 18+ Integration**: Uses `useSyncExternalStore` for optimal performance
- **Event Coalescing**: OIMDB's built-in event coalescing reduces unnecessary re-renders
- **Key-specific subscriptions**: Only listen to changes for relevant data
- **Memory Management**: Automatic cleanup prevents memory leaks
- **Efficient batching**: Updates are batched through React's concurrent features
## Examples
### Complete Example
```typescript
import React from 'react';
import {
OIMEventQueue,
OIMRICollection,
OIMReactiveIndexManualSetBased,
OIMReactiveIndexManualArrayBased
} from '@oimdb/core';
import {
useSelectEntityByPk,
useSelectEntitiesByPks,
useSelectEntitiesByIndexKeySetBased,
useSelectEntitiesByIndexKeyArrayBased
} from '@oimdb/react';
interface User {
id: string;
name: string;
email: string;
teamId: string;
}
// Setup
function createUserCollection() {
const queue = new OIMEventQueue();
// Use SetBased for frequent add/remove operations
const teamIndex = new OIMReactiveIndexManualSetBased<string, string>(queue);
return new OIMRICollection(queue, {
collectionOpts: { selectPk: (user: User) => user.id },
indexes: { byTeam: teamIndex },
});
}
const usersCollection = createUserCollection();
// Component
function UserProfile({ userId }: { userId: string }) {
const user = useSelectEntityByPk(usersCollection, userId);
if (!user) return <div>Loading...</div>;
return <h2>{user.name}</h2>;
}
function TeamDashboard({ teamId }: { teamId: string }) {
// Use SetBased hook for SetBased index
const teamMembers = useSelectEntitiesByIndexKeySetBased(
usersCollection,
usersCollection.indexes.byTeam, // OIMReactiveIndexManualSetBased
teamId
);
return (
<div>
<h3>Team Members ({teamMembers.length})</h3>
{teamMembers.map(user => (
<div key={user?.id}>{user?.name}</div>
))}
</div>
);
}
```
### With Context Provider
```typescript
import {
OIMRICollectionsProvider,
useOIMCollectionsContext
} from '@oimdb/react';
function App() {
const collections = React.useMemo(() => ({
users: createUserCollection(),
// ... other collections
}), []);
return (
<OIMRICollectionsProvider collections={collections}>
<Dashboard />
</OIMRICollectionsProvider>
);
}
function Dashboard() {
const { users } = useOIMCollectionsContext();
const allUsers = useSelectEntitiesByPks(users, []);
return <div>Total Users: {allUsers.length}</div>;
}
```
## Migration from v0.x
The v1.x API has changed significantly to work with reactive collections:
### Hook Name Changes
```typescript
// v0.x - Abstract storage interfaces
const user = useEntity(userStorage, 'user123');
const users = useEntities(userStorage, userIds);
const posts = useIndex(postStorage, categoryIndex, 'tech');
// v1.x - Reactive collections with typed indexes
const user = useSelectEntityByPk(reactiveCollection, 'user123');
const users = useSelectEntitiesByPks(reactiveCollection, userIds);
// Use appropriate hook based on index type
const posts = useSelectEntitiesByIndexKeySetBased(
reactiveCollection,
reactiveIndexSetBased,
'tech'
);
const orderedItems = useSelectEntitiesByIndexKeyArrayBased(
reactiveCollection,
reactiveIndexArrayBased,
'category1'
);
```
### Collection Creation
```typescript
// v0.x - With DX layer
const db = createDb({ scheduler: 'microtask' });
const users = db.createCollection<User>();
const user = useEntity(users.advanced.collection, userId);
// v1.x - Direct reactive collections
const queue = new OIMEventQueue();
const usersCollection = new OIMRICollection(queue, {
collectionOpts: { selectPk: (user: User) => user.id },
indexes: {},
});
const user = useSelectEntityByPk(usersCollection, userId);
```
### Context API
```typescript
// v0.x - No context support
// v1.x - Full context support
const collections = { users: usersCollection };
<OIMRICollectionsProvider collections={collections}>
<App />
</OIMRICollectionsProvider>
```
### Key Changes
- **Hook naming**: More explicit names like `useSelectEntityByPk` vs `useEntity`
- **Parameters**: Direct reactive collection objects instead of storage abstractions
- **Context**: New context API for centralized collection management
- **Type safety**: Enhanced TypeScript support with better inference
## Dependencies
- `@oimdb/core` - Core OIMDB functionality
- `react` - React hooks and components
## License
MIT