Динамические модели

дисклеймер

В данный момент динамические модели все еще находятся на стадии разработки, их API может меняться со временем. Данная статья носит исключительно ознакомительный характер мы настоятельно не рекомендуем использовать в продакшене этот функционал.

НЕ РЕКОМЕНДУЕТСЯ ИСПОЛЬЗОВАТЬ В ПРОДАКШЕНЕ.

На данный момент effector не имеет возможности динамически создавать юниты, юниты должны быть инициализированы статически на уровне модуля. Если же создавать юниты во время рантайма, то произойдет утечка памяти, потому что юниты навсегда останутся в графе. Хотя и можно попробовать использовать withRegion, createNode и clearNode, но это требует определенного навыка разработчика, поскольку это низкоуровневый API и более того придется самому отслеживать жизненный цикл юнитов, что может быть головной болью.

Поэтому для случаев когда нужно было иметь динамику использовались key-value сторы, которые хранили в себе объекты, где ключами были идентификаторы, а значениями состояния, например:

model.ts
import { createStore, createEvent } from "effector";
import { useStoreMap } from "effector-react";
type Item = { id: string; count: number };
const $items = createStore<Record<string, Item>>({});
const addItem = createEvent<Item>();
const removeItem = createEvent<string>();
$items.on(addItem, (state, item) => ({
...state,
[item.id]: item,
}));
$items.on(removeItem, (state, id) => {
const copy = { ...state };
delete copy[id];
return copy;
});

При этом в UI подписывались с помощью useStoreMap только на ту часть, что соответствует id для получения данных:

counter.tsx
import { $items, addItem, removeItem } from "./model";
import { useStoreMap, useUnit } from "effector-react";
function Counter({ id }: { id: string }) {
const item = useStoreMap({
store: $items,
keys: [id],
fn: (state, [key]) => state[key],
});
const [onAddItem, onRemoveItem] = useUnit([addItem, removeItem]);
if (!item) return null;
return (
<div>
<span>{item.count}</span>
<button onClick={() => onAddItem({ id, count: item.count + 1 })}>+</button>
<button onClick={() => onRemoveItem(id)}>Удалить</button>
</div>
);
}

Хотя такой подход работает, это не очень удобно, особенно если структура куда сложнее, чем в этом примере.

Модели приносят новый способ работы с динамическими состояниями, позволяя создавать экземпляры моделей на лету, которые имеют свои собственные состояния и логику.

Установка и работа с моделями

В данный момент модели реализованы в отдельном репозитории и доступны отдельным пакетом:

Terminal window
npm install @effector/model

а также пакет для интеграции с React:

Terminal window
npm install @effector/model-react

В корне репозитория вы сможете найти директорию apps , где есть примеры использования моделей в приложениях с около-реальным функционалом. В данной статье мы просто ознакомимся с API и что из себя будут представлять динамические модели.

API моделей

Динамические модели приносят ряд новых API для работы:

  • keyval – оператор который создаёт коллекцию инстансов модели, где каждый элемент идентифицируется ключом. Именно через keyval происходит динамическое создание и удаление экземпляров модели. keyval также может использоваться внутри keyval для вложенных структур. В аргументе ожидает колбэк который вернет объект со свойствами:

    • state – состояние модели, является объектом со сторами или keyval моделью . Одно из свойств также должно быть ключом модели
    • key – ключ модели, иначе говоря ее уникальный идентификатор
    • api – опциональный объект с событиями или эффектами для работы с моделью
    • onMount – опциональное событие или эффект, которое вызывается при создании инстанса модели
    • optional – опциональный массив строк с необязательными полями модели при создании

Например:

export const restaurantsList = keyval(() => {
const $name = createStore("");
const $description = createStore("");
const $category = createStore<string[]>([]);
const dishesList = keyval(() => {
const $name = createStore("");
const $description = createStore("");
const $price = createStore(0);
const $additives = createStore<Additive[]>([]);
return {
key: "name",
state: {
name: $name,
description: $description,
price: $price,
additives: $additives,
},
optional: ["additives"],
};
});
return {
key: "name",
state: {
name: $name,
description: $description,
category: $category,
dishes: dishesList,
},
api: {
addDish: dishesList.edit.add,
removeDish: dishesList.edit.remove,
},
optional: ["category", "dishes"],
};
});

Теперь используя restaurantsList мы можем в рантайме добавлять, изменять или удалять экземпляры модели. Все, что описано внутри keyval будет динамически создано для каждого инстанса.

const addRestaurant = createEvent();
sample({
clock: addRestaurant,
fn: () => ({
name: "Starbucks",
description: "Американская корпорация и крупнейшая в мире сеть кофеен",
}),
target: restaurantsList.edit.add,
});
  • lens – линза необходима нам чтобы погрузиться внутрь keyval для работы с данными, например имея вложенные keyval мы можем достучаться с самого верха до самого низа и получить данные или апи для работы с ним:
const menuItemIdLens = lens(orderKeyval).item(orderId).menuItemId;
const foodDescLens = lens(restaurantKeyval).item(restId).menu.item(menuItemIdLens).description;
lens api

В данный момент API линзы дорабатывается и может отличаться от того, что есть в примерах репозитория.

Помимо основного пакета effector также имеется API для effector-react, чтобы удобно работать с моделями в React:

  • useEntityList(keyval, View) – хук, который принимает keyval первым аругментом и компонент вторым. Итерирует по всем ключам коллекции и для каждого создаёт EntityProvider, передавая в него View. Проще говоря, это способ отрисовать список и в дальнейшем удобнее работать с остальными хуками без передачи id.
  • useEntityItem(keyval, key?) – возвращает сущность по id в коллекции keyval. Если key передан явно, ищет элемент по этому ключу, если ключ не передан, пытается получить его из ближайшего EntityProvider.
  • useItemApi(keyval, key?) – возвращает объект API сущности для работы с ней.
  • useEditItemField(keyval, key?)– возвращает объект с функциями для обновления каждого поля модели. Если key передан явно, ищет элемент по этому ключу, если ключ не передан, пытается получить его из ближайшего EntityProvider.
  • useEditKeyval(keyval) – возвращает объект методов для модификации модели, например добавить, удалить или обновить.
const { add, map, remove, replaceAll, set, update } = useEditKeyval(ordersList);
Перевод поддерживается сообществом

Документация на английском языке - самая актуальная, поскольку её пишет и обновляет команда effector. Перевод документации на другие языки осуществляется сообществом по мере наличия сил и желания.

Помните, что переведенные статьи могут быть неактуальными, поэтому для получения наиболее точной и актуальной информации рекомендуем использовать оригинальную англоязычную версию документации.

Соавторы