React-lens — эффективное управление состоянием в приложениях в ReactJs

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Во многих разрабатываемых программах мы сталкиваемся с необходимостью организации работы с данными. Такие задачи могут быть самыми разными: хранение, актуализация, масштабирование и т. п. А ещё приходится реализовывать взаимодействие различных библиотек. Рассмотрим один из способов решения этих проблем при помощи React Lens.

Как бы мы не хотели, всегда будут ограничения в вычислительных мощностях или человеческих ресурсах. Большую роль оказывает необходимость развивать и расширять свой продукт. Потому баланс задач по организации состояния может быть также разным: где-то нужно сделать упор на производительность, где-то на масштабируемость и т. д.

Введение

Сейчас сообщество разработчиков предлагает множество решений: MST, Zustand, hookstate, jotai, react-lens и другие... Однако часто можно столкнуться с тем, что на многих проектах эти решения игнорируются в силу того, что о них мало что известно. Если в предыдущем проекте некая технология показала себя не так хорошо (сильная зацеплённость, боллерплейт), то и в следующем, возможно, будет также. Простым «магическим мышлением» эту ситуацию не решить.

В итоге может возникнуть забавная ситуация, как в выражении - «Мыши плакали, кололись, но продолжали есть кактус». Потому важно иметь представление больше, чем об одной технологии, и в данной статье мы рассмотрим ещё одну такую — ReactLens.

React Lens

ReactLens — это модульный менеджер состояния, направленных на расширяемость и прозрачность кода.

NPM репозиторий

Первая задача решается предоставлением сразу нескольких подходов расширения: классический, как в ООП и функциональный, которые можно применять одновременно.

Задача прозрачности решается отступлением от декларативного подхода в пользу императивного. Это работает потому, что декларативный подход, хоть и прост на начальных этапах разработки, но он неминуемо создаёт новый (декларативный) язык программирования, который нужно поддерживать и передавать другим разработчикам. И к средине жизненного цикла проекта этот язык может разрастаться и представлять большую проблему для разработки. Императивный подход сложнее изначально, но его сложность не растёт так быстро, как у его оппонента, и по прошествии времени оказывается проще, т. к. зачастую основывается на семантике основного языка.

Ещё одна особенность ReactLens — модульность. Все сущности состояния разбиты на отдельные модели, которые объединяются в один или несколько графов. При этом, каждый узел графа ведёт себя, как корневой. Это означает, что тип узла может быть передан в компонент, как тип принимаемого аргумента без повышения зацеплённости. Другими словами компонент остается независимым от глобального состояния, как и его примитив, что позволяет создавать общие компонентные модели и распространять их между проектами.

Ещё стоит отметить, что ReactLens не хранит данные внутри себя, а реализует интерфейс для работы с ними, т. е. можно создавать модели для работы с локальным хранилищем, кукками или удалёнными данными.

Практика

И так, давайте переходить к практике… Для начала рассмотрим пример классического счётчика

const Counter: React.FC = () => {
  const [value, setValue] = useState(0);
  return <button onClick={() => setValue(value + 1)}>{value}</button>;
}

Как вы считаете, сколько нужно ещё дописать кода, чтобы подключить его к глобальному состоянию? Давайте посмотрим!

const store = createStore(0);  // ← одну строчку

const Counter: React.FC = () => {
  const [value, setValue] = useLens(store);
  return <button onClick={() => setValue(value + 1)}>{value}</button>;
}

Вот так просто... Мы добавили одну строку, инициализирующую состояние и заменили классический хук useState() на useLens(). Больше ничего делать не нужно. Всё будет работать, как по волшебству.

Впечатляюще? Если так, то у меня плохие новости. Простые компоненты, как этот счётчик, не должны вызывать стихийного роста сложности кода — «Это норма!». Этот пример просто должен быть таким априори. Если кода больше - возможно что-то не так...

Давайте разберёмся как это работает...

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

Создание состояния

Работа с ReactLens начитается с создания корневой модели и выбором её области использования. Глобальные модели создаются методом createStore().

export const store = createStore({ message: 'Hello World!' });

Разработчики рекомендуют создавать несколько маленьких моделей, вместо одной большой.

Локальные модели можно создавать внутри компонентов с помощью хукa useLocalStore().

const initStore = { message: 'Hello World!' };

const Component: React.FC = () => {
  const store = useLocalStore(initStore);

  return <></>;
}

Есть ещё несколько способов организации локального состояния: useDerivedStore() и useLocalStaticStore(). См. wiki

Расширение состояния

Объектный подход

ReactLens создаст базовую модель, которая уже будет содержать несколько методов манипуляции с данными. Но мы можем её расширить двумя способами: классическим и функциональным.

Классический способ заключается в создании наследника от базового класса Store и передаче его в конструктор узла.

class Car extends Store {
  move() {
    console.log('Move!')
  }
}

const car = createStore({}, Car);
car.move();  // Move!

Такой же способ работает и для локального состояния:

const store = useLocalStore(initStore, Car);

На данном этапе можно уже определить тип аргумента, который будет передан в компонент:

interface CarProps {
  value: Car;
}

const Component: React.FC<CarProps> = ({ value }) => {
  value.move();
  ...
}

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

interface MyType {}

interface CarProps {
  value: Store<MyType>;
}

Функциональный подход

Функциональный способ расширяет сам узел цепочкой методов extends(). Каждый метод принимает функцию, возвращающую прототип или объект.

const car = createStore({})
  .extends(current => {
    move: () => console.log('move')
  }).
  extends(current => { color: 'red' });

store.move();  // move
console.log(store.color )  // red

Такой способ позволяет легко создавать вложенные состояния.

const nested = createStore('Hello!');
const base = createStore({}).extends({ nested });

console.log(base.nested);  // Hello!

Также, есть универсальный способ получить вложенное состояние от любого узла, используя метод go().

const base = createStore({ deep: { nested: 'Hello!' } });

const nested = base.go('deep').go('nested');
console.log(nested);  // Hello!

Метод go() позволяет получить состояние с определённым прототипом, если передать его во втором аргументе. Это похоже на создание корневого узла.

const nested = base.go('nested', Car);
nested.move();

Ещё go() кеширует модель и реализует шаблон «одиночка».

Манипуляция с данными

Каждый узел ReactLens определяет только интерфейс работы с данными. Это означает, что просто приравнять одно из вложенных состояний к определённому значению не получится. Чтобы получить доступ к данным нужно использовать методы get() и set() в каждом из узлов.

const store = createStore('Foo');

console.log(store.get());  // Foo

store.set('Moo');
console.log(store.get());  // Moo

В некоторых случаях, когда данные узла являются примитивом, можно get() упустить.

const store = createStore('String');
console.log(store + ' is simple');  // String is simple

Стоить отметить, что работать можно с объектами любой вложенности и массивами.

Метод set() может принимать функцию, которая передаёт предыдущее состояние и должна вернуть новое.

const store = createStore(0);

const add = prev = > prev + 1;

store.set(add);  // 1
store.set(add);  // 2

На самом деле, ReactLens уже реализует «под капотом» работу с данными. Вам нужно только пользоваться методами, предоставленными хуком useLens().

Связывание состояния

С созданием состояния разобрались, теперь нужно связать его с компонентом таким образом, чтобы он смог обновиться, если что-то в данных измениться. ReactLens реализует событийную модель по шаблону «подписчик-издатель».

Связать компонент можно при помощи хука useLens(), который работает как привычный useState().

const store = createStore(0);

const Counter: React.FC = () => {
  const [value, setValue] = useLens(store);
  return <button onClick={() => setValue(value + 1)}>{value}</button>;
}

Связывание можно осуществить и на уровне локального состояния, передавая узел в компонент, как параметр, как мы уже делали раньше.

interface Props {
  value: Store<number>;
}

 const Counter: React.FC<Props> = ({ value }) => {
  const [value, setValue] = useLens(value);
  return <button onClick={() => setValue(value + 1)}>{value}</button>;
}

ReactLens не запускает каскадного обновления всех компонентов, а изменяет только связанный компонент, что хорошо влияет на производительность. В следующем примере, обновляться будет только тот счётчик, который используется.

const store = createStore({ one: 0, two: 0 });

const Form: React.FC = () => (
  <>
    <Counter value={store.go('one')} />
    <Counter value={store.go('two')} />
  </>
);

Триггеры изменений

Хук useLens() позволяет фильтровать изменения по большому числу параметров. О них подробно написано в документации. Например, нам нужно, чтобы счётчик обновлялся только на чётные значения.

/* Триггер */
const isEven = (e, node) =>  node.get() % 2 === 0;

/* Хук будет выглядеть так */
const [value, setValue] = useLens(value, isEven);

На самом деле, если триггер во втором аргументе не указан, useLens() использует встроенный триггер Trigger.object, который фильтрует изменения, связанные только с текущим узлом. Потому, если нужно сохранить его функциональность, то следует его тоже добавить.

const [value, setValue] = useLens(value, Trigger.object, isEven);

Реакция на изменения

Иногда нам не обязательно обновлять компонент, а нужно только отловить сам факт изменений. В зависимости от ситуации, ReactLens позволяет сделать это при помощи: расширения модели, метода subscribe() или хука useSubscribe().

Если нужно определить реакцию глобально, то при создании модели можно использовать метод расширения on() и передать туда нужную реакцию. Также, можно использовать триггер, по принципу useLens().

const store = createStore(0)
  .on((e, node) => console.log(node));  // 1 2 3 4
  .on(isEven, (e, node) => console.log(node));  // 2 4 6 8

Для отслеживания изменений внутри компонента, можно использовать хук useSubscribe(). Он не приведёт к обновлении компонента.

const callback = (e, node) => console.log(node);

const Component: React.FC = () => {
  useSubscribe(store, callback);
  ...
}

В остальных случаях можно использовать стандартный метод subscribe() позволяющий организовать подписку на изменения на уровне модели. Его можно использовать для разработки собственных хуков на базе ReactLens.

interface Props<T = unknown> {
  value: Store<T>;
}

const Component: React.FC<Props> = ({ value }) => {
  useEffect(() => {
    const unsubscriber = store.subscribe(value, (e, node) => console.log(node));
    return unsubscriber;
  }, [value])
  ...
}

Ещё немножко функций

ReactLens имеет широкий спектр поставляемых утилит из коробки. Вот некоторые из них:

Работа с контекстом

Иногда нужно определить глобальные зоны видимости для состояния. Для этого можно использовать хук useLensContext(), который позволяет использовать узел из передаваемого контекста. Это полезно для тестирования или организации мультиролевой модели. Например, если нужно чтобы форма открывалась от разных пользователей из одного сеанса. (Аналог функции «открыть от имени...»)

Асинхронные события

ReactLens работает асинхронно. Нет необходимости в использовании специального промежуточного ПО для организации асинхронного взаимодействия. Также есть утилиты Callback.async() и Callback.pipe() для организации синхронизированной обработки данных.

Хук useLensDebounce() позволит быстро организовать работу ленивых полей ввода. Например создадим поле с отложенным вызовом событий на его изменение.

interface Props {
  value:  Strote<string | number>;
}

const DebounceInput: React.FC<Props> = ({ value }) => {
  const [value, setValue] = useLensDebounce(value);
  return <input, value={value}, onChange={e => setValue(e.target.value)}>;
}

Трансформация данных «на лету»

Мы можем назначать каждому узлу адаптер, который будет работать, как двусторонний преобразователь данных. Сделать это можно несколькими способами: функцией transform() или более низкоуровневым методом — chain(). Например, мы хотим, чтобы дата выводилась в формате ISO, но в состоянии хранилась, как число.

const date = createStore(Date.now());

/* Создаём реплику узла, который будет адаптером для date */
const iso =  date.transform(
  value => new Date(value).toISO(),
  iso => new Date(iso).getTime()
);

iso.set(/* ISO string */);
ico.get()  //  ISO string

date.set(/* number */);
date.get()  // number;

Так можно разносить одни и те же данные по полям разного типа или проводить синхронную валидацию.

Выводы

Мы рассмотрели базовые возможности ReactLens и показали, что при своей функциональности он не требует большой описательной работы. ReactLens старается быть простым и походим на ReactJs, чтобы не вызывать дискомфорта от перехода на него.

И так, ReactLens может:

  • Работать с глобальным и локальным состояниями

  • Работать с контекстом или без него

  • Не требует оборачивания компонентов во врапперы

  • Позволяет разрабатывать универсальные примитивы

  • Может расширятся и реализовывать бизнес-логику в модели

  • Из коробки умеет работать с асинхронным кодом

  • Умеет обрабатывать события и управлять реакцией на них

  • Умеет трансформировать данные на лету и создавать адаптеры

  • Содержит набор утилит, позволяющих расширять большинство базовых функций

Есть и минусы, его популярность не так высока, в сравнении с другими менеджерами. Но так ли важна популярность перед постановкой задачи — решать вам?

Послесловие

Давайте теперь немного улучшим наш счётчик, пример которого мы рассмотрели в начале статьи. Давайте сделаем наш счётчик...

  1. Универсальным компонентом

  2. Универсальным компонентом

Что не сделаешь ради науки...

Остановимся на классическом ООП подходе и реализуем метод increment() в классе-наследнике от Store.

class CounterModel extends Store<number> {
  increment() {
    this.set(this.get() + 1);
  }
}

Теперь можем определить интерфейс для параметров компонента.

interface CounterProps {
  value:  CounterModel;
}

Реализуем сам компонент. В самом компоненте можно использовать функции из нашей модели.

const Counter: React.FC<CounterProps> = ({ value }) => {
  const [count] = useLens(value);
  return <button onClick={() => value.increment()}>{count}</button>;
}

А само состояние может быть создано с указанием нашей модели CounterModel, как прототипа.

const counter = createStore(0, CounterModel);

<Counter value={counter}>

Или же её можно взять от любого узла в любом графе состояния...

const store = createStore({ conter: { value: 0 } });
const counter =  store.go('counter').go('value', CounterModel);

<Counter value={counter}>

Можно ещё использовать и вложенную модель.

const counter = createStore(0, CounterModel);
const store = createStore({}).extends({ counter });

<Counter value={store.counter}>

Что же, счётчик не такой уж и сложный компонент, чтобы раскрыть все возможности ReactLens, но уже можно показать некоторые плюсы этой библиотеки.

Исходные тексты взяты тут.

Приятного исследования!

Ссылки

  • NPM репозиторий

  • Исходные тексты

  • Домашняя страничка

  • Справка

В следующих статьях я хочу познакомить вас с другими менеджерами состояния, которые могут предложить интересные решения.

Источник: https://habr.com/ru/articles/789246/


Интересные статьи

Интересные статьи

При большом количестве микросервисов в проекте приходится сталкиваться с тем, что в некоторых из них нужно дублировать один и тот же код, а обнаружив баг в одном месте, искать и исправлять его везде. ...
Эта статья по своей сути что-то вроде конспекта по вебинарам Ани Обуховой об эффективном использовании ресурсов нашего мозга, дополненного моими умозаключениями и рекомендациями.Если хотите посмотреть...
В предыдущей статье из цикла мы рассказывали, что такое группа процессов и области знаний. Чтобы понимать, как управлять проектом, важно осознавать разницу между понятиями. Мы также уточняли...
Доброго времени суток, %username%. Пора расчехлить блог после 6 лет простоя и попробовать опять что-то полезное принести сообществу. Я крайне удивлен, что на хабре до сих пор нет ни одной ...
(достаточно вольный перевод огромной эмоциональной статьи, которая на практике наводит мосты между возможностями Си и Rust в плане решения бизнес-задач и разрешение багов, связанных с ручным упра...