Keyval это хранилище инстансов и данных для них, прямого доступа к инстансам он не даёт, можно лишь добавить либо удалить инстанс (есть отдельный апи для этого), поэтому возникает вопрос: как достать из кейвала какой-то элемент? Могут требоваться как данные из этого инстанса, так и апи для управления ним

Ответ на него — линзы

Допустим, есть иерархия — массив ресторанов, в каждом ресторане массив блюд, в каждом блюде массив добавок. Как достать конкретную добавку, имея лишь статически вызываемую функцию create и три стора $restaurant, $dish и $additive, которые хранят id элементов?

Линзы это апи чтобы погрузиться внутрь иерархии. Имея глубоко вложенный кейвал с кейвалами внутри, линзы дают возможность взять самый верхний, сказать: верни мне ресторан с ID из стора $restaurant, в нем прочитай поле dishes — это тоже кейвал, в dishes возьми инстанс с ID из стора $dish и так до тех пор пока не дойдёшь до какого-то окончательного юнита в модели. Если тебе нужен стор — ты получаешь на выходе стор, который будет трекать что находится по описанному тобой пути и соответственно обновляться. Если тебе нужен эвент — ты получишь эвент, и сможешь его вызывать как обычный эвент, а он сам погрузится в глубину иерахии по нужному пути и вызовет именно её эвент

Таким образом происходит работа с глубокими вложенностями не имея инстансов этой самой иерархии а лишь декларативно описывая адрес с которым ты хочешь работать

Untitled

Эта линза написана для модели отражающей такую структуру данных:

type Restaurants = Array<{
  name: string
  dishes: Array<{
    name: string
    price: number
    additives: Array<{
      name: string
      price: number
    }>
  }>
}>

Раскрытый вид модели, с описанием юнитов будет иметь такой вид

type Restaurants = Keyval<{
  name: Store<string>
  dishes: Keyval<{
    name: Store<string>
    price: Store<number>
    additives: Keyval<{
      name: Store<string>
      price: Store<number>
    }>
  }>
}>

Функции dishes и additives создаются автоматически исходя из структуры кейвала, ведь она известна с момента создания кейвала. Эти функции-геттеры возвращают вложенные линзы до тех пор пока не будет прочитаны поля, возвращающие итоговые юниты, как .store, который в данном случае возвращает объект добавки целиком, насколько глубокая модель бы за ней не стояла

В случае с $additiveEntity мы получили объект добавки целиком, потому что обратились к полю .store сразу после чтения элемента кейвала additives, но можно читать и конкретные поля:

const $restaurantName = lens(restaurantsList, $restaurant)
	.name
	.store

const $dishPrice = lens(restaurantsList, $restaurant)
	.dishes($dish)
	.price
	.store

const $additivePrice = lens(restaurantsList, $restaurant)
  .dishes($dish)
  .additives($additive)
  .price
  .store;

Линзы для триггера эвентов

Эвенты читаются через линзы точно таким же образом, на выходе после чтения поля .event будет эвент, который при триггере пройдёт по всей иерархии и вызовет событие в конкретном инстансе

Допустим, мы расширяем модель списка ресторанов для управления его содержимым, редактированием цен в меню:

type Restaurants = Keyval<{
  name: Store<string>
  dishes: Keyval<{
    name: Store<string>
    price: Store<number>
    changePrice: EventCallable<number>
    additives: Keyval<{
      name: Store<string>
      price: Store<number>
      changePrice: EventCallable<number>
    }>
  }>
}>

Тогда для изменения цены блюда или добавки, имея id ресторана и id блюда, через линзу можно получить эвент, который при вызове будет менять соответствующее поле в структуре данных:

const changeDishPrice = lens(restaurantsList, $restaurant)
  .dishes($dish)
  .changePrice
  .event

const changeAdditivePrice = lens(restaurantsList, $restaurant)
  .dishes($dish)
  .additives($additive)
  .changePrice
  .event

Константные значения в линзах