name: react-river-api description: 'Generate @zerologix/react-river provider and component wiring code. Use when: building river providers, wiring API calls into react-river, creating asyncNotifierProvider, promiseProvider, stateProvider, notifierProvider, adding river state management, connecting services to river providers, creating river data layer for a feature.' argument-hint: 'Describe the API/feature to integrate (e.g. "fetch user profile with reload action")'
React-River API Integration
When to Use
- Building a new data layer for a feature using
@zerologix/react-river - Connecting an existing service method to a river provider
- Adding river-based state (local UI state, async fetch, complex mutations)
- Wiring a provider into a React component
Step 1 — Choose the Provider Type
Answer these questions to pick the right primitive:
| Situation | Provider |
|---|---|
| Inject a service instance (no async) | stateProvider |
| One-time fetch, read-only | promiseProvider |
| One-time fetch with a parameter | promiseProviderFamily |
| Fetch + mutations / reload / optimistic update | asyncNotifierProvider |
| Fetch + mutations + parameter | asyncNotifierProviderFamily |
| Derived / computed from other providers | provider |
| Complex sync state with methods | notifierProvider |
Default heuristic for fetch-based features:
- Read-only fetch →
promiseProvider(addFamilyif parameterised) - Fetch + mutations / reload →
asyncNotifierProvider(addFamilyif parameterised)
For the remaining cases use the table directly:
- Service injection (no async) →
stateProvider - Derived / computed value →
provider - Complex sync state with methods (no async fetch) →
notifierProvider
If the feature matches multiple rows (e.g. needs a parameter and derived computation), ask the user a clarifying question before generating code rather than guessing.
Step 2 — Provider File Structure
Determine the target directory by inspection:
- Check whether
src/river/already exists in the package — if yes, use it. - Otherwise check for
src/providers/— if yes, use it. - If neither exists, default to
src/river/and confirm the path with the user before creating the file.
stateProvider (service injection)
import { stateProvider } from '@zerologix/react-river';
import type { MyService } from '@/services/MyService';
export const myServiceProvider = stateProvider<MyService | null>(() => null, {
name: 'MyServiceProvider',
});
promiseProvider (simple fetch)
import { promiseProvider } from '@zerologix/react-river';
import { myServiceProvider } from './myService';
export const myDataProvider = promiseProvider(
async (ref) => {
const service = ref.watch(myServiceProvider);
if (!service) return null;
return await service.fetchData();
},
{ name: 'MyDataProvider' },
);
promiseProviderFamily (parameterised fetch)
import { promiseProviderFamily } from '@zerologix/react-river';
import { myServiceProvider } from './myService';
export const myDataByIdProvider = promiseProviderFamily(
async (ref, id: string) => {
const service = ref.watch(myServiceProvider);
if (!service) return null;
return await service.fetchDataById(id);
},
{ name: 'MyDataByIdProvider' },
);
asyncNotifierProvider (fetch + mutations)
import {
AsyncNotifier,
asyncNotifierProvider,
asyncData,
asyncLoading,
} from '@zerologix/react-river';
import type { MyItem } from '@/services/MyService';
import { myServiceProvider } from './myService';
class MyNotifier extends AsyncNotifier<MyItem[] | null> {
async build() {
const service = this.ref.watch(myServiceProvider);
if (!service) return null;
return await service.fetchItems();
}
async reload() {
this.state = asyncLoading(this.state.data);
const service = this.ref.read(myServiceProvider);
if (!service) return;
const items = await service.fetchItems();
this.state = asyncData(items);
}
async addItem(item: MyItem) {
const current = this.state.data ?? [];
// Optimistic update: apply locally first, then persist
this.state = asyncData([...current, item]);
try {
await this.ref.read(myServiceProvider)?.addItem(item);
} catch (e) {
// Roll back to previous state on failure
this.state = asyncData(current);
throw e;
}
}
}
export const myItemsProvider = asyncNotifierProvider(() => new MyNotifier(), {
name: 'MyItemsProvider',
});
provider (derived / computed)
import { provider } from '@zerologix/react-river';
import { myServiceProvider } from './myService';
import { myDataProvider } from './myData';
export const myDerivedProvider = provider(
(ref) => {
const raw = ref.watch(myDataProvider);
// transform / derive — re-runs whenever myDataProvider changes
return raw?.items.filter((item) => item.active) ?? [];
},
{ name: 'MyDerivedProvider' },
);
notifierProvider (complex sync state)
import { Notifier, notifierProvider } from '@zerologix/react-river';
class MyStateNotifier extends Notifier<MyState> {
build(): MyState {
return { count: 0, filter: 'all' };
}
increment() {
this.state = { ...this.state, count: this.state.count + 1 };
}
setFilter(filter: string) {
this.state = { ...this.state, filter };
}
}
export const myStateProvider = notifierProvider(() => new MyStateNotifier(), {
name: 'MyStateProvider',
});
asyncNotifierProviderFamily (fetch + mutations + parameter)
import {
AsyncNotifier,
asyncNotifierProviderFamily,
asyncData,
asyncLoading,
} from '@zerologix/react-river';
import { myServiceProvider } from './myService';
class MyParamNotifier extends AsyncNotifier<MyItem[] | null> {
constructor(private variables: MyQueryParams) {
super();
}
async build() {
const service = this.ref.watch(myServiceProvider);
if (!service) return null;
return await service.fetchItems(this.variables);
}
async keepDataAndRefetch() {
this.state = asyncLoading(this.state.data);
const service = this.ref.read(myServiceProvider);
if (!service) return;
const items = await service.fetchItems(this.variables);
this.state = asyncData(items);
}
}
export const myItemsByParamProvider = asyncNotifierProviderFamily(
(_ref, variables: MyQueryParams) => new MyParamNotifier(variables),
{ name: 'MyItemsByParamProvider' },
);
Step 3 — Export from index
Add the new provider to the package's src/river/index.ts (or the nearest barrel):
export * from './myData';
Step 4 — Wire Into a Component
Read-only display (promiseProvider / asyncNotifierProvider)
import { useRiverWatch } from '@zerologix/react-river';
import { myDataProvider } from '@/river/myData';
function MyComponent() {
const data = useRiverWatch(myDataProvider);
if (data.isLoading) return <div>Loading...</div>;
if (data.isError) return <div>Error</div>;
return <div>{data.data?.name}</div>;
}
With mutation actions
import { useRiverRef, useRiverWatch } from '@zerologix/react-river';
import { myItemsProvider } from '@/river/myItems';
function MyList() {
const items = useRiverWatch(myItemsProvider);
const riverRef = useRiverRef();
return (
<div>
{items.data?.map((item) => (
<div key={item.id}>{item.name}</div>
))}
<button onClick={() => riverRef.read(myItemsProvider.notifier).reload()}>
Reload
</button>
</div>
);
}
With selector (avoid unnecessary re-renders)
const symbolIds = useRiverWatch(
myItemsProvider,
({ data }) => data?.map((item) => item.id) ?? [],
);
Parameterised family
const data = useRiverWatch(myDataByIdProvider('abc123'));
// or with selector:
const name = useRiverWatch(myDataByIdProvider('abc123'), (v) => v.data?.name);
Step 5 — Naming Conventions
| Entity | Convention | Example |
|---|---|---|
| Provider variable | camelCase + Provider suffix |
tradingAccountsProvider |
| Notifier class | PascalCase + Notifier |
AccountAdditionalInfoNotifier |
name option |
PascalCase + Provider |
'TradingAccountsProvider' |
| Provider file | camelCase, in src/river/ |
tradingAccount.ts |
Quick Decision Checklist
Before generating code, confirm:
- [ ] What data does the provider expose? (type / shape)
- [ ] Which service / API call does it wrap?
- [ ] Does it depend on other providers? (use
ref.watch/ref.read) - [ ] Are there mutations or a reload action? → use
asyncNotifierProvider - [ ] Does it need a parameter? → use
*Familyvariant - [ ] Which component(s) will consume it?
Reference: AsyncValue Shape
useRiverWatch on an async provider returns:
{
status: 'loading' | 'data' | 'error';
data?: T;
error?: unknown;
isLoading: boolean;
isError: boolean;
}
Always guard isLoading / isError before accessing data.
Error Handling in Notifiers
When a service call inside build(), reload(), or a mutation can throw, wrap it with try/catch and set asyncError so the AsyncValue reflects the failure:
import { asyncData, asyncError, asyncLoading } from '@zerologix/react-river';
async reload() {
this.state = asyncLoading(this.state.data);
try {
const service = this.ref.read(myServiceProvider);
if (!service) return;
const items = await service.fetchItems();
this.state = asyncData(items);
} catch (error) {
this.state = asyncError(error, this.state.data); // preserve stale data
}
}
The asyncError(error, staleData) call lets the UI show an error state while optionally retaining the previous data for display.