Контроль верхнего уровня с управлением состоянием Redux: учебник по ClojureScript

Опубликовано: 2022-03-11

Добро пожаловать во вторую захватывающую часть Unearthing ClojureScript! В этом посте я расскажу о следующем важном шаге для серьезного отношения к ClojureScript: управлении состоянием — в данном случае с использованием React.

В интерфейсном программном обеспечении управление состоянием имеет большое значение. Из коробки есть несколько способов обработки состояния в React:

  • Сохранение состояния на верхнем уровне и передача его (или обработчиков для определенной части состояния) дочерним компонентам.
  • Выбросить чистоту из окна и иметь глобальные переменные или какую-то лавкрафтовскую форму внедрения зависимостей.

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

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

Итак, как с этим можно справиться? Те из вас, кто знаком с React, возможно, уже пробовали Redux, контейнер состояний для приложений JavaScript. Возможно, вы обнаружили это по собственной воле, смело ища управляемую систему поддержания состояния. Или вы могли просто наткнуться на него, читая о JavaScript и других веб-инструментах.

Независимо от того, как люди в конечном итоге смотрят на Redux, по моему опыту, у них обычно возникают две мысли:

  • «Я чувствую, что должен использовать это, потому что все говорят, что я должен это использовать».
  • «Я не совсем понимаю, почему это лучше».

Вообще говоря, Redux предоставляет абстракцию, которая позволяет управлению состоянием соответствовать реактивной природе React. Перенося всю информацию о состоянии на такую ​​систему, как Redux, вы сохраняете чистоту React. Таким образом, у вас будет гораздо меньше головной боли и, как правило, вам будет намного легче рассуждать.

Для новичков в Clojure

Хотя это может и не помочь вам полностью изучить ClojureScript с нуля, здесь я, по крайней мере, вкратце расскажу о некоторых основных концепциях состояния в Clojure[Script]. Не стесняйтесь пропускать эти части, если вы уже опытный кложур!

Вспомните одну из основ Clojure, которая применима и к ClojureScript: по умолчанию данные неизменяемы. Это отлично подходит для разработки и обеспечения гарантий того, что то, что вы создаете на временном шаге N, остается тем же самым на временном шаге > N. ClojureScript также предоставляет нам удобный способ иметь изменяемое состояние, если нам это нужно, с помощью концепции atom .

atom в ClojureScript очень похож на AtomicReference в Java: он предоставляет новый объект, который блокирует свое содержимое с гарантиями параллелизма. Как и в Java, вы можете поместить в этот объект все, что захотите, — с этого момента этот атом будет атомарной ссылкой на все, что вы хотите.

Когда у вас есть свой atom , вы можете атомарно установить в него новое значение с помощью reset! функция (обратите внимание на ! в функции — в языке Clojure это часто используется для обозначения того, что операция имеет состояние или нечиста).

Также обратите внимание, что, в отличие от Java, Clojure не волнует, что вы помещаете в свой atom . Это может быть строка, список или объект. Динамическая типизация, детка!

 (def my-mutable-map (atom {})) ; recall that {} means an empty map in Clojure (println @my-mutable-map) ; You 'dereference' an atom using @ ; -> this prints {} (reset! my-mutable-map {:hello "there"}) ; atomically set the atom (reset! my-mutable-map "hello, there!") ; don't forget Clojure is dynamic :)

Реагент расширяет эту концепцию атома своим собственным atom . (Если вы не знакомы с Reagent, ознакомьтесь с постом перед этим.) Он ведет себя идентично ClojureScript atom , за исключением того, что он также запускает события рендеринга в Reagent, как и встроенное хранилище состояний React.

Пример:

 (ns example (:require [reagent.core :refer [atom]])) ; in this module, atom now refers ; to reagent's atom. (def my-atom (atom "world!")) (defn component [] [:div [:span "Hello, " @my-atom] [:input {:type "button" :value "Press Me!" :on-click #(reset! My-atom "there!")}]])

Это покажет один <div> , содержащий <span> с надписью «Hello, world!» и кнопка, как и следовало ожидать. Нажатие этой кнопки атомарно изменит my-atom , чтобы оно содержало "there!" . Это вызовет перерисовку компонента, в результате чего в диапазоне появится сообщение «Привет, там!» вместо.

Обзор управления состоянием в Redux и Reagent.

Это кажется достаточно простым для локальной модификации на уровне компонентов, но что, если у нас есть более сложное приложение с несколькими уровнями абстракции? Или если нам нужно разделить общее состояние между несколькими подкомпонентами и их подкомпонентами?

Более сложный пример

Давайте рассмотрим это на примере. Здесь мы будем реализовывать грубую страницу входа:

 (ns unearthing-clojurescript.login (:require [reagent.core :as reagent :refer [atom]])) ;; -- STATE -- (def username (atom nil)) (def password (atom nil)) ;; -- VIEW -- (defn component [on-login] [:div [:b "Username"] [:input {:type "text" :value @username :on-change #(reset! username (-> % .-target .-value))}] [:b "Password"] [:input {:type "password" :value @password :on-change #(reset! password (-> % .-target .-value))}] [:input {:type "button" :value "Login!" :on-click #(on-login @username @password)}]])

Затем мы разместим этот компонент входа в наш основной app.cljs , например:

 (ns unearthing-clojurescript.app (:require [unearthing-clojurescript.login :as login])) ;; -- STATE (def token (atom nil)) ;; -- LOGIC -- (defn- do-login-io [username password] (let [t (complicated-io-login-operation username password)] (reset! token t))) ;; -- VIEW -- (defn component [] [:div [login/component do-login-io]])

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

  1. Мы ждем, пока пользователь введет свое имя пользователя и пароль и нажмет «Отправить».
  2. Это вызовет нашу функцию do-login-io в родительском компоненте.
  3. Функция do-login-io выполняет некоторые операции ввода-вывода (такие как вход на сервер и получение токена).

Если эта операция блокируется, то у нас уже куча проблем, так как наше приложение зависло, а если нет, то нам нужно беспокоиться об асинхронности!

Кроме того, теперь нам нужно предоставить этот токен всем нашим подкомпонентам, которые хотят выполнять запросы к нашему серверу. Рефакторинг кода стал намного сложнее!

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

Учебное пособие по ClojureScript: войдите в Redux

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

Внутренняя работа Redux (и теория, стоящая за ней) несколько выходит за рамки этой статьи. Вместо этого я перейду к рабочему примеру с ClojureScript, который, надеюсь, каким-то образом продемонстрирует, на что он способен!

В нашем контексте Redux реализуется одной из многих доступных библиотек ClojureScript; этот называется ре-фрейм. Он предоставляет Clojure-оболочку для Redux, что (на мой взгляд) делает его абсолютным удовольствием в использовании.

Основы

Redux поднимает состояние вашего приложения, оставляя ваши компоненты легкими. Компонент Reduxified должен думать только о:

  • На что это похоже
  • Какие данные он потребляет
  • Какие события он вызывает

Остальное обрабатывается за кулисами.

Чтобы подчеркнуть этот момент, давайте уменьшим нашу страницу входа выше.

База данных

Перво-наперво: нам нужно решить, как будет выглядеть наша модель приложения. Мы делаем это, определяя форму наших данных, данных, которые будут доступны во всем приложении.

Хорошее эмпирическое правило заключается в том, что если данные необходимо использовать в нескольких компонентах Redux или они должны быть долговечными (как наш токен), то они должны храниться в базе данных. Напротив, если данные являются локальными для компонента (например, наши поля имени пользователя и пароля), то они должны жить как локальное состояние компонента, а не храниться в базе данных.

Давайте создадим шаблон нашей базы данных и укажем наш токен:

 (ns unearthing-clojurescript.state.db (:require [cljs.spec.alpha :as s] [re-frame.core :as re-frame])) (s/def ::token string?) (s/def ::db (s/keys :opt-un [::token])) (def default-db {:token nil})

Здесь стоит отметить несколько интересных моментов:

  • Мы используем библиотеку spec Clojure, чтобы описать , как должны выглядеть наши данные. Это особенно уместно в динамическом языке, таком как Clojure[Script].
  • В этом примере мы отслеживаем только глобальный токен, который будет представлять нашего пользователя после входа в систему. Этот токен представляет собой простую строку.
  • Однако до того, как пользователь войдет в систему, у нас не будет токена. Это представлено ключевым словом :opt-un , которое означает «необязательный, неквалифицированный». (В Clojure обычным ключевым словом может быть что-то вроде :cat , а квалифицированным ключевым словом может быть что-то вроде :animal/cat . Квалификация обычно происходит на уровне модуля — это предотвращает стирание ключевых слов в разных модулях друг друга.)
  • Наконец, мы указываем состояние нашей базы данных по умолчанию, то есть то, как она инициализируется.

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

Подписки

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

С Redux мы не обращаемся к нашей базе данных напрямую — это может привести к проблемам с жизненным циклом и параллелизмом. Вместо этого мы регистрируем наши отношения с аспектом базы данных через подписки .

Подписка сообщает reframe (и Reagent), что мы зависим от части базы данных, и если эта часть изменена, то наш компонент Redux должен быть перерендерен.

Подписки очень просто определить:

 (ns unearthing-clojurescript.state.subs (:require [re-frame.core :refer [reg-sub]])) (reg-sub :token ; <- the name of the subscription (fn [{:keys [token] :as db} _] ; first argument is the database, second argument is any token)) ; args passed to the subscribe function (not used here)

Здесь мы регистрируем одну подписку — на сам токен. Подписка — это просто имя подписки и функция, которая извлекает этот элемент из базы данных. Мы можем делать с этим значением все, что захотим, и изменять представление здесь сколько угодно; однако в этом случае мы просто извлекаем токен из базы данных и возвращаем его.

С подписками можно делать гораздо больше — например, определять представления для подразделов базы данных для более жесткой области повторного рендеринга — но пока мы не будем усложнять!

События

У нас есть наша база данных, и у нас есть свое представление о базе данных. Теперь нам нужно вызвать некоторые события! В этом примере у нас есть два типа событий:

  • Чистое событие ( без побочных эффектов) записи нового токена в базу данных.
  • Событие ввода-вывода ( имеющее побочный эффект) выхода и запроса нашего токена посредством некоторого взаимодействия с клиентом.

Начнем с легкого. Re-frame даже предоставляет функцию именно для такого рода событий:

 (ns unearthing-clojurescript.state.events (:require [re-frame.core :refer [reg-event-db reg-event-fx reg-fx] :as rf] [unearthing-clojurescript.state.db :refer [default-db]])) ; our start up event that initialises the database. ; we'll trigger this in our core.cljs (reg-event-db :initialise-db (fn [_ _] default-db)) ; a simple event that places a token in the database (reg-event-db :store-login (fn [db [_ token]] (assoc db :token token)))

Опять же, здесь все довольно просто — мы определили два события. Первый предназначен для инициализации нашей базы данных. (Видите, как он игнорирует оба своих аргумента? Мы всегда инициализируем базу данных с нашим default-db !) Второй предназначен для хранения нашего токена, как только мы его получили.

Обратите внимание, что ни одно из этих событий не имеет побочных эффектов — ни внешних вызовов, ни ввода-вывода! Это очень важно для сохранения святости процесса Redux. Не делайте его нечистым, чтобы не навлечь на себя гнев Редукса.

Наконец, нам нужно наше событие входа в систему. Разместим его под остальными:

 (reg-event-fx :login (fn [{:keys [db]} [_ credentials]] {:request-token credentials})) (reg-fx :request-token (fn [{:keys [username password]}] (let [token (complicated-io-login-operation username password)] (rf/dispatch [:store-login token]))))

Функция reg-event-fx во многом похожа на reg-event-db , хотя есть некоторые тонкие отличия.

  • Первый аргумент — это уже не просто сама база данных. Он содержит множество других вещей, которые вы можете использовать для управления состоянием приложения.
  • Второй аргумент очень похож на reg-event-db .
  • Вместо того, чтобы просто возвращать новый db , мы возвращаем карту, которая представляет все эффекты («fx»), которые должны произойти для этого события. В этом случае мы просто вызываем эффект :request-token , определение которого приведено ниже. Одним из других допустимых эффектов является :dispatch , который просто вызывает другое событие.

Как только наш эффект был отправлен, вызывается наш эффект :request-token , который выполняет нашу длительную операцию входа в систему ввода-вывода. Как только это будет завершено, он с радостью отправляет результат обратно в цикл событий, тем самым завершая цикл!

Учебник по ClojureScript: конечный результат

Так! Мы определили нашу абстракцию хранилища. Как сейчас выглядит компонент?

 (ns unearthing-clojurescript.login (:require [reagent.core :as reagent :refer [atom]] [re-frame.core :as rf])) ;; -- STATE -- (def username (atom nil)) (def password (atom nil)) ;; -- VIEW -- (defn component [] [:div [:b "Username"] [:input {:type "text" :value @username :on-change #(reset! username (-> % .-target .-value))}] [:b "Password"] [:input {:type "password" :value @password :on-change #(reset! password (-> % .-target .-value))}] [:input {:type "button" :value "Login!" :on-click #(rf/dispatch [:login {:username @username :password @password]})}]])

И наш компонент приложения:

 (ns unearthing-clojurescript.app (:require [unearthing-clojurescript.login :as login])) ;; -- VIEW -- (defn component [] [:div [login/component]])

И, наконец, получить доступ к нашему токену в каком-либо удаленном компоненте так же просто, как:

 (let [token @(rf/subscribe [:token])] ; ... )

Собираем все вместе:

Как локальное состояние и глобальное (Redux) состояние работают в примере входа.

Без суеты, без суеты.

Разделение компонентов с помощью Redux/Re-frame означает чистое управление состоянием

Используя Redux (через рефрейминг), мы успешно отделили наши компоненты представления от беспорядка обработки состояния. Расширение нашей абстракции состояния теперь проще простого!

Redux в ClojureScript действительно настолько прост — у вас нет оправдания, чтобы не попробовать его.

Если вы готовы заняться взломом, я бы порекомендовал ознакомиться с фантастической документацией по рефреймингу и нашим простым рабочим примером. Я с нетерпением жду ваших комментариев к этому руководству по ClojureScript ниже. Удачи!

Связанный: Управление состоянием в Angular с использованием Firebase