Skip to content

@virentia/react API

Use @virentia/react at the rendering boundary. Keep domain logic in @virentia/core models.

ScopeProvider

Provides a core scope to hooks.

Use it once per React tree that should share state. Nested trees can provide another scope when they need isolation.

tsx
import { scope } from "@virentia/core";
import { ScopeProvider } from "@virentia/react";

const appScope = scope();

export function App() {
  return (
    <ScopeProvider scope={appScope}>
      <Routes />
    </ScopeProvider>
  );
}

useProvidedScope

Reads the provided scope.

Use it when a component needs to pass the scope into boundary helpers such as allSettled, caches, or external adapters.

tsx
function SaveButton({ saved }: { saved: EventCallable<void> }) {
  const scope = useProvidedScope();
  const onClick = () => allSettled(saved, { scope });

  return <button onClick={onClick}>Save</button>;
}

It throws when there is no ScopeProvider.

useUnit

Reads stores and binds callable units to the provided scope.

Use it for simple components that only need a few stores, events, or effects.

tsx
const countValue = useUnit(count);
const increment = useUnit(incremented);

Object shape:

tsx
const model = useUnit({
  count,
  incremented,
  pending: saveFx.pending,
  save: saveFx,
});

Array shape:

tsx
const [countValue, increment] = useUnit([count, incremented]);

useModel

Unwraps a model object.

Use it when a component works with a whole model and wants stores as values and events/effects as callbacks.

tsx
const model = useModel({
  count,
  incremented,
});

Creates a model from props:

tsx
function createCounterModel({ props }: ModelContext<{ step: number }>) {
  const clicked = event<void>();
  const count = store(0);

  reaction({
    on: clicked,
    run() {
      count.value += props.step;
    },
  });

  return { clicked, count };
}

function Counter(props: { step: number }) {
  const model = useModel(createCounterModel, props);
  const increment = () => model.clicked();

  return <button onClick={increment}>{model.count}</button>;
}

Use a cache when the model should survive unmount:

tsx
const model = useModel(createChatModel, props, {
  cache: chatCache,
  key: props.chatId,
});

component

Pairs a model factory and a view.

Use it when the model belongs to the component lifecycle. It keeps creation, props, mount events, unmount events, and rendering in one pattern.

tsx
export const Counter = component({
  model({ props }: ModelContext<{ step: number }>) {
    const clicked = event<void>();
    const count = store(0);

    reaction({
      on: clicked,
      run() {
        count.value += props.step;
      },
    });

    return { clicked, count };
  },
  view({ model }) {
    const increment = () => model.clicked();

    return <button onClick={increment}>{model.count}</button>;
  },
});

Cached component:

tsx
export const ChatPanel = component({
  cache: chatCache,
  key: (props: { chatId: string }) => props.chatId,
  model: createChatModel,
  view({ model }) {
    return <div>{model.messages.items.length}</div>;
  },
});

Controlled component:

tsx
export const Parent = component({
  model() {
    const counter = Counter.create({ step: 2 });

    return { counter };
  },

  view({ model }) {
    return <Counter step={2} model={model.counter} />;
  },
});

.create(props) lets a parent component model create and own a child component model. Passing model to the child keeps that child model controlled by the parent. The child component updates context.props and emits lifecycle units while mounted, but it does not dispose a controlled model on unmount.

createModelCache

Creates a scope-aware cache keyed by your ID.

Use it when a model should survive unmount and be reused later by key: chats, tabs, detail screens, media players, previews.

ts
const chatCache = createModelCache<string, ChatProps, ChatModel>();

Read cached models:

ts
chatCache.has("support", appScope);
chatCache.get("support", appScope);
chatCache.getInstance("support", appScope);

Dispose cached models:

ts
chatCache.delete("support", appScope);
chatCache.clear(appScope);

ModelContext

Model factories receive:

Use this context when model logic depends on props, lifecycle, the current scope, or a cache key.

ts
interface ModelContext<Props, Key = undefined> {
  readonly scope: Scope;
  readonly owner: Owner;
  readonly props: StoreWritable<Props>;
  readonly mounted: EventCallable<void>;
  readonly unmounted: EventCallable<void>;
  readonly mounts: StoreWritable<number>;
  readonly key: Key;
}

Use mounted, unmounted, and mounts for lifecycle logic inside the model.