@livon/react
Purpose
@livon/react is the React adapter for @livon/sync units.
Public hooks are intentionally minimal:
useLivonState(unit)-> full snapshotuseLivonValue(unit)->snapshot.valueuseLivonStatus(unit)->snapshot.statususeLivonMeta(unit)->snapshot.meta
Keep framework-agnostic behavior in @livon/sync; React-specific subscription/render integration stays in @livon/react.
Install
pnpm add @livon/react @livon/sync
Hooks
import {
useLivonMeta,
useLivonState,
useLivonStatus,
useLivonValue,
} from '@livon/react';
useLivonState(unit)returns the complete unit snapshot, including unit-specific methods (load,submit,start,refresh,apply,set,clear, ...).useLivonValue(unit)subscribes only to value changes.useLivonStatus(unit)subscribes only to status changes.useLivonMeta(unit)subscribes only to meta changes.
Status values depend on unit type:
source/action/stream/view/transform use idle | rehydrated | loading | refreshing | success | error,
while draft uses 'dirty' | 'clear'.
Type Helpers
import type {
LivonMetaOf,
LivonSnapshotOf,
LivonStateOf,
LivonStatusOf,
LivonValueOf,
} from '@livon/react';
Available helpers:
LivonSnapshotOf<TUnit>LivonValueOf<TUnit>LivonStatusOf<TUnit>LivonMetaOf<TUnit>LivonState<TValue, TStatus, TMeta>LivonStateOf<TUnit>
Shared Todo Setup
import { action, entity, source, transform, view } from '@livon/sync';
interface Todo {
id: string;
title: string;
completed: boolean;
listId: string;
}
interface TodoIdentity {
listId: string;
}
interface ReadTodosPayload {
query: string;
}
interface UpdateTodoPayload {
id: string;
title: string;
}
const todoEntity = entity<Todo>({
idOf: ({ id }) => id,
});
const readTodos = source({
entity: todoEntity,
mode: 'many',
})<TodoIdentity, ReadTodosPayload>({
defaultValue: [],
run: async ({ identity: { listId }, payload: { query }, upsertMany }) => {
const todos = await api.readTodos({
listId,
query,
});
upsertMany(todos, { merge: true });
},
});
const updateTodo = action({
entity: todoEntity,
mode: 'one',
})<TodoIdentity, UpdateTodoPayload>({
defaultValue: null,
run: async ({ identity: { listId }, payload: { id, title }, upsertOne }) => {
const updated = await api.updateTodo({ listId, id, title });
upsertOne(updated, { merge: true });
},
});
const todoStatsView = view<TodoIdentity, { total: number; open: number }>({
defaultValue: { total: 0, open: 0 },
out: async ({ identity, get }) => {
const { value: todos } = await get(readTodos(identity));
return {
total: todos.length,
open: todos.filter(({ completed }) => !completed).length,
};
},
});
const renameFirstTodo = transform<TodoIdentity, { title: string }, string>({
defaultValue: '',
out: async ({ identity, get }) => {
const { value: todos } = await get(readTodos(identity));
return todos[0]?.title ?? '';
},
in: async ({ identity, payload: { title }, get, set }) => {
const { value: todos } = await get(readTodos(identity));
const first = todos[0];
if (!first) {
return;
}
await set(updateTodo(identity), {
id: first.id,
title,
});
},
});
Examples
useLivonState
const todoListUnit = readTodos({ listId: 'list-1' });
const { load, refetch, value } = useLivonState(todoListUnit);
void load({ query: 'open' });
void refetch();
useLivonState with view
const todoStatsUnit = todoStatsView({ listId: 'list-1' });
const {
value: stats,
status: statsStatus,
refresh: refreshStats,
} = useLivonState(todoStatsUnit);
void refreshStats();
useLivonState with transform
const renameUnit = renameFirstTodo({ listId: 'list-1' });
const {
value: firstTodoTitle,
apply: applyRename,
} = useLivonState(renameUnit);
void applyRename({ title: `${firstTodoTitle} (renamed)` });
useLivonValue
const todos = useLivonValue(readTodos({ listId: 'list-1' }));
useLivonStatus
const status = useLivonStatus(readTodos({ listId: 'list-1' }));
useLivonMeta
const meta = useLivonMeta(readTodos({ listId: 'list-1' }));
Listener Lifecycle
useLivonValue, useLivonStatus, and useLivonMeta are selective subscriptions.
A subscribe trigger does not force re-render when the selected slice did not change.
Initial render state always comes from unit.getSnapshot().