npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@effector-kit/react

v0.4.0

Published

Effector React kit

Downloads

623

Readme

@effector-kit/react

React bindings for @effector-kit/models.

The package currently exports:

  • useModel
  • component

Installation

pnpm add @effector-kit/react @effector-kit/models effector-react effector react

useModel

useModel has two main modes and one created-model mode.

1. useModel(model)

Creates a managed instance on mount and deletes it on unmount.

import { useModel } from '@effector-kit/react';

function CounterScreen() {
  const counter = useModel(counterModel);

  return (
    <button onClick={() => counter.setCount(counter.count + 1)} type="button">
      {counter.count}
    </button>
  );
}

You can also pass initial contract data:

const counter = useModel(counterModel, {
  data: {
    count: 10,
  },
});

If you want one stable instance per explicit id, pass id. If that instance does not exist yet, it is created. If it already exists, the existing instance is reused.

function ChatScreen({ chatId }: { chatId: string }) {
  const chat = useModel(chatModel, {
    id: chatId,
    data: {
      title: 'New chat',
    },
    retain: true,
  });

  return <div>{chat.title}</div>;
}

With retain: true, the hook does not delete the instance on unmount. This is useful when model lifetime should follow app state, not React mount cycles.

id may be either the original instance id or any alias registered on the model. If an alias resolves to an existing instance, useModel returns that instance while preserving the requested id in the resolved entity.

chatModel.addAlias({
  aliasId: 'route-chat-id',
  instanceId: 'server-chat-id',
});

function ChatScreen() {
  const chat = useModel(chatModel, {
    id: 'route-chat-id',
    retain: true,
  });

  return <div>{chat.title}</div>;
}

2. useModel(model, lens)

Returns already existing instances selected by the lens.

function PositiveCounters() {
  const counters = useModel(
    counterModel,
    counterModel.lens.where(entity => entity.count > 0),
  );

  return (
    <ul>
      {counters.map(counter => (
        <li key={counter.id}>{counter.count}</li>
      ))}
    </ul>
  );
}

If the lens is narrowed to one instance with first(), last(), or single(), useModel(...) returns one resolved entity instead of an array.

function CurrentChat({ chatId }: { chatId: string }) {
  const chat = useModel(chatModel, chatModel.lens.ids(chatId).single());

  if (!chat) return null;

  return <div>{chat.title}</div>;
}

Lens ids can also be aliases. This is useful when external sources, such as routes or websocket payloads, may use a temporary id while the model stores the canonical id.

Advanced: useModel(createdModel)

This mode works with the value returned by Component.create(...).

The usual app-level paths are still:

  • useModel(model)
  • useModel(model, lens)
  • Component.create(...) + model prop when one parent owns one child instance
  • child(Component.model) when a parent owns multiple child instances
const createdModel = Counter.create({ count: 5 });

function Page() {
  const counter = useModel(createdModel);

  return <div>{counter.count}</div>;
}

What useModel returns

The hook resolves model API into React-friendly values:

  • stores become plain values
  • events and effects become callable functions
  • ref(...) becomes an array of resolved entities
  • child(...) becomes an array of resolved child entities
  • nested plain objects with units are resolved recursively

That means this works directly:

function Dashboard() {
  const dashboard = useModel(dashboardModel, {
    data: { title: 'Main' },
  });

  return (
    <div>
      <h1>{dashboard.title}</h1>
      <div>{dashboard.selected.map(item => item.count).join(',')}</div>
      <button onClick={() => dashboard.track('counter-1')} type="button">
        Track
      </button>
    </div>
  );
}

This also works for namespaced plain objects returned from model(...):

function SettingsScreen() {
  const settings = useModel(settingsModel);

  return (
    <button onClick={() => settings.panel.toggle()} type="button">
      {String(settings.panel.opened)}
    </button>
  );
}

Scope support

useModel reads the current scope from effector-react Provider.

import { Provider } from 'effector-react';
import { fork } from 'effector';

const scope = fork();

root.render(
  <Provider value={scope}>
    <App />
  </Provider>,
);

component(...)

component(...) creates a React component on top of an @effector-kit/models model.

import { createEvent, sample } from 'effector';
import { component } from '@effector-kit/react';
import { contract, define, type TNumber } from '@effector-kit/models';

const Counter = component({
  contract: contract({
    count: define.store(define.schema<TNumber>(), 0),
  })(),
  model: ({ count }, mounted, unmounted) => {
    const setCount = createEvent<number>();

    sample({
      clock: setCount,
      target: count,
    });

    mounted.watch(() => {
      console.log('mounted');
    });

    unmounted.watch(() => {
      console.log('unmounted');
    });

    return {
      count,
      setCount,
    };
  },
  view: ({ id, count, onSetCount }) => (
    <button onClick={() => onSetCount(count + 1)} type="button">
      {id}:{count}
    </button>
  ),
});

How props are mapped

Inside view(...):

  • store fields become plain values
  • event/effect fields become onXxx handlers
  • nested models and refs are resolved recursively
  • nested plain objects with units are resolved recursively too

For example, if model returns:

return {
  count,
  setCount,
};

then view receives:

{
  id: string;
  count: number;
  onSetCount: (payload: number) => void;
}

If model returns a nested plain object:

return {
  title,
  panel: {
    opened,
    toggle,
  },
};

then view receives:

{
  id: string;
  title: string;
  panel: {
    opened: boolean;
    onToggle: () => void;
  };
}

How mounted payload is built

The mounted event receives all props that are not part of the component contract.

  • contract fields are used as initial model data
  • model prop is ignored
  • every other prop is forwarded to mounted
import { type Event } from 'effector';
import { component } from '@effector-kit/react';
import {
  contract,
  define,
  type TBoolean,
  type TString,
} from '@effector-kit/models';

const Todo = component({
  contract: contract({
    title: define.store(define.schema<TString>(), ''),
    done: define.store(define.schema<TBoolean>(), false),
  })(),
  model: (
    { title, done },
    mounted: Event<{ userId: string; roomId: string }>,
  ) => {
    mounted.watch(payload => {
      console.log(payload.userId, payload.roomId);
    });

    return {
      title,
      done,
    };
  },
  view: ({ title, done }) => (
    <div>
      {title}:{String(done)}
    </div>
  ),
});

<Todo title="Ship fix" done userId="u1" roomId="room-1" />;

In this example, mounted receives:

{
  userId: "u1",
  roomId: "room-1",
}

Component.create(...)

Every created component has:

  • Component.model
  • Component.create(data?, options?)

Component.create(...) returns a created model descriptor.

That descriptor can be used in two places:

  • outside, through the component model prop
  • inside another component(...).model as one owned submodel

Outside component model

Use this when some outer layer wants to own one specific component instance and pass it down explicitly.

const controlled = Counter.create({ count: 5 });

function Page() {
  return <Counter model={controlled} />;
}

If model prop is passed, the component uses that existing created model. If model prop is omitted, the component creates and manages its own instance.

Inside component model

Use this when a parent has exactly one owned child model and you do not want to model it as a collection.

Typical example: one page and one dialog.

import { createEvent, sample } from 'effector';
import { component } from '@effector-kit/react';
import { contract, define, type TString } from '@effector-kit/models';

const Page = component({
  contract: contract({
    title: define.store(define.schema<TString>(), ''),
  })(),
  model: ({ title }) => {
    const dialog = Dialog.create({ opened: false });
    const openDialog = createEvent<void>();

    sample({
      clock: openDialog,
      target: dialog.open,
    });

    return {
      title,
      dialog,
      openDialog,
    };
  },
  view: ({ title, dialog, onOpenDialog }) => (
    <div>
      <h1>{title}</h1>
      <div>{String(dialog.opened)}</div>
      <button onClick={() => onOpenDialog()} type="button">
        open dialog
      </button>
      <Dialog model={dialog} />
    </div>
  ),
});

Inside component(...).model, the created model behaves like one owned submodel:

  • in model, you work with model-shaped units like dialog.open
  • in view, you get resolved values and handlers like dialog.opened and dialog.onOpen

One child vs many children

There are two different composition scenarios.

One owned child model

Use Component.create(...).

This is the right fit for things like:

  • page + dialog
  • page + toolbar model
  • widget + one settings panel

Multiple child instances

Use child(Component.model).

This is the right fit for things like:

  • cards
  • tabs
  • rows
  • list items

Reusing component models

Component.model is a normal model, so it is the main composition surface for parent-child modeling.

import { createEvent, sample } from 'effector';
import { component } from '@effector-kit/react';
import { child, contract, define, type TString } from '@effector-kit/models';

const Dashboard = component({
  contract: contract({
    title: define.store(define.schema<TString>(), ''),
  })(),
  model: ({ title }) => {
    const cards = child(Counter.model);
    const createCard = createEvent<{ id: string; data: { count: number } }>();

    sample({
      clock: createCard,
      target: cards.create,
    });

    return {
      title,
      cards,
      createCard,
    };
  },
  view: ({ title, cards, onCreateCard }) => (
    <div>
      <h1>{title}</h1>
      <div>{cards.map(card => card.count).join(',')}</div>
      <button
        onClick={() => onCreateCard({ id: 'a', data: { count: 1 } })}
        type="button"
      >
        Add card
      </button>
    </div>
  ),
});

Generics

The current API supports generic contracts and components through a factory function.

import { sample } from 'effector';
import { component } from '@effector-kit/react';
import { contract, define, type TRef } from '@effector-kit/models';

const makeValueContract = contract({
  value: define.store(define.schema<TRef<'Value'>>(), '' as never),
  change: define.event(define.schema<TRef<'Value'>>()),
});

function createValueComponent<Value extends string>() {
  return component({
    contract: makeValueContract<{ Value: Value }>(),
    model: ({ value, change }) => {
      sample({
        clock: change,
        target: value,
      });

      return {
        value,
        change,
      };
    },
    view: ({ value, onChange }) => (
      <button onClick={() => onChange('updated' as Value)} type="button">
        {value}
      </button>
    ),
  });
}

Direct JSX generics like <SomeSelect<string> /> are not part of the current public API.