ReactLens и менеджмент состояния без сложностей на ReactJS

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

В качестве предисловия

А сколько всего менеджеров состояния существует?

Если коротко, то около десятка. Вот некоторые: recoil, Jotai, hookstate, rematch, mobx-state-tree, zustand, react-lens, redux и т.д.

У каждого свои "плюшки". Какие-то хороши для определённого спектра задач. В этой статье мы рассмотрим один из таких менеджеров — react-lens.

Немного о react-lens

react-lens - это новый инструмент, который облегчает работу с состоянием в приложении React. Он предоставляет набор компонентов и утилит для управления состоянием, делая его простым и эффективным.

react-lens нацелен на понятность и переносимость кода, масштабируемость и расширяемость. Основная аудитория — это стартапы и проекты-питомцы. Главная фишка — быстро и без боллерплейта развернуть управление состоянием проекта.

Вот ещё несколько особенностей:

  • Инкапсуляция — можно создавать локальные части состояния, закреплённые за отдельным компонентом и переносить его вместе с ним, например на другой проект с другим менеджером состояния.
  • Моделирование — react-lens состоит из моделей, которые можно расширять разными способами.
  • Асинхронность — менеджер можно использовать в асинхронном коде или внедрять такой код в модели, и не боятся, что что-то пойдёт не так.
  • Атомы — состояние может быть разделено на отдельные независимые маленькие кусочки. Это полезно для производительности и масштабирования.

Начнём с примера

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

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

Теперь подключим его к react-lens

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

Это всё! Наш компонент теперь подключён к глобальному состоянию. Если store изменится, то компонент обновится.

Заманчиво? Теперь давайте разберёмся…

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

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

В нашем случае, это глобальное состояние.

// Создаёт глобальное состояние. export const store = createStore(0);

Помимо примитивных значений можно хранить и сложные объекты и обращаться к их полям, как к отдельным моделям со своей реализацией.

Есть несколько способов обратиться ко вложенным моделям и данным:

  • Использовать стандартный метод go()
  • Расширить текущую модель вложенной моделью или наследованием.
  • Получить объект целиком, используя метод get()

Какой способ выбрать - зависит от задачи и необходимости. Первые два способа эквивалентны и используются для получения модели, связанной с конкретным полем объекта. Последний способ - это простое чтение данных.

// Получение состояния методом go() const store = createStore({ foo: { moo: 'Hello!' } }); // Это модель, связанная с полем foo.moo const moo = store.go('foo').go('moo'); // Получение данных из модели moo moo.get(); // Hello! // Получение данных из всего состояния store.get(); // { foo: { moo: 'Hello!' } } // А вот так можно записать данные в поле foo.moo moo.set('World!'); // World!

Кстати, не нужно никаких редьюсеров. react-lens запишет данные в нужное поле или создаст его, если оно отсутствовало.

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

  • Наследованием, указав прототип при создании модели
  • Функциональным способом, используя метод extends()

Первый способ выглядит так:

// Создадим свою модель от Store class MyModel<T> extends Store<T> { hello() {} } // Укажем её при создании состояния const store = createStore({}, MyModel); store.hello();

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

const store = createStore({ foo: { moo: {} } }); // Создадим модель из нашего прототипа MyModel, // связанную с полем foo.moo const mooModel = store.go('foo').go('moo', MyModel); mooModel.hello();

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

Кстати, Методы в фабрике могут быть асинхронными.

const store = createStore({}). extends(current => { hello: () => {}, fetch: async () => {} // Пример асинхронного метода }); store.hello();

Если создать некую модель заранее и передать её в метод extends(), как поле объекта, то к этому полю можно будет обратиться через точку.

const nested = createStore('Hello!'); const store = createStore({}).extends({ nested }); store.nested.get(); // Hello!

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

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

Подключение состояния к компоненту

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

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

import { store } from './stores'; const Counter: React.FC = () => { const [count, setCount] = useLens(store.counter); ... }

Теперь, если store.counter изменится, то и Counter обновится.

Но можно сделать наш компонент чуть более универсальным. Например, мы хотим, чтобы он мог подключаться к любой модели, имеющей тип — число. Давайте просто пробросим модель, как параметр компонента Counter.

export Props { value: Store<number>; } const Counter: React.FC<Props> = ({ value }) => { const [count, setCount] = useLens(value); ... }

Вот собственно и всё. Теперь компонент может быть использован на любой форме.

Моделирование

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

Можно воспользоваться функциональным способом и определить метод increment в модели.

// Создадим расширенную модель для счётчика const counter = createStore(0) .extends(current => { increment: () => current.set(current.get() + 1) }); export const store = createStore({}).extends({ counter });

Теперь Counter будет выглядеть так:

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

А использование его на форме, примерно вот так:

import { store } from './store'; const Form: React.FC = () => ( <Counter value={store.counter} /> )

Заключение

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

Но не забывайте, что react-lens имеет ещё множество интересных способов проработать ваш проект. Это и трансформирование данных на лету и создание «ленивых» полей ввода, оптимизация и многое другое.

А главное, не придётся тратить много времени на проектирование состояния, что очень важно, особенно, если удастся сберечь ваш солнечный выходной =)

Начать дискуссию