Добавление интерактивности с помощью состояния¶
В предыдущем уроке мы узнали, как создать приложение Solid с использованием компонентов и JSX. Затем мы создали приложение Bookshelf, используя наши новые знания, но оно не совсем закончено. В этом уроке мы узнаем, как добавить состояние в наше приложение. Затем мы применим эти знания к нашему приложению Bookshelf, чтобы воплотить его в жизнь.
Замечание о примитивах¶
По мере изучения этих уроков мы начнем слышать о примитивах Solid. В то время как компоненты являются строительными блоками видов в приложениях Solid, примитивы являются строительными блоками взаимодействия. Первый примитив, с которым мы познакомимся, - это сигнал.
Управление базовым состоянием с помощью сигналов¶
В Solid самым основным способом управления состоянием нашего приложения является использование сигнала. Для создания сигнала Solid предоставляет функцию createSignal
:
1 2 3 |
|
Здесь происходит много всего: сначала мы вызываем createSignal
с начальным значением state. В данном случае count
будет начинаться с 0
. Функция createSignal
возвращает двухэлементный массив, и мы используем JavaScript деструктурирующее присваивание для распаковки этого массива. В данном случае мы присваиваем первый элемент переменной count
, а второй элемент - переменной setCount
.
Первый элемент, count
, представляет собой акцессорную функцию (также называемую геттером), которая возвращает текущее значение состояния.
Важно отметить, что это именно функция получения текущего значения, а не само значение. Попутно этот вызов функции сообщает Solid, что мы получили доступ к сигналу.
1 2 3 4 5 |
|
svelte
Это эквивалентно let count = 0
в Svelte.
В Svelte вы можете использовать реактивное значение, не вызывая его как функцию. Это связано с тем, что реактивность Svelte использует компилятор, в то время как реактивность Solid является частью библиотеки. Поскольку мы работаем в рамках ограничений JavaScript, нам необходимо запускать код при обращении к значению, чтобы сообщить реактивной системе: "Это значение используется здесь!".
react
Это эквивалентно const [count, setCount] = useState(0)
в React.
В React можно использовать реактивное значение, не вызывая его как функцию. Это связано с тем, что система рендеринга React будет перезапускать весь компонент при любом изменении состояния - нет ничего особенного в настройке отдельных частей состояния, поэтому нет необходимости запускать какой-либо код при использовании реактивного значения.
vue
Это эквивалентно const count = ref(0)
в Vue.
Вместо того чтобы разделять count на функцию-геттер и функцию-сеттер, Vue предлагает использовать count.value
как для получения, так и для установки значения. Реактивная система Vue похожа на систему Solid: нам необходимо запускать код за кулисами при каждом обращении к ref
.
В Vue этот код находится внутри функции .value
getter function.
angular
В Angular это может быть просто свойство класса, которое вы мутируете. Это одно из самых больших различий между Solid и Angular. В то время как Solid полагается на явное обновление свойств, Angular позволяет мутировать переменные, чтобы вызвать обнаружение изменений.
Хотя это может показаться чисто умственным излишеством, у Solid есть два основных преимущества разделения чтения и записи:
- Не требуется дополнительных затрат производительности на обнаружение изменений
- Не требуется никакой дополнительной логики (например,
runOutsideAngular
), чтобы избежать обнаружения изменений.
Второй элемент, setCount
, представляет собой функцию setter. Если мы хотим увеличить count
, мы можем передать count() + 1
в setCount
:
1 2 3 4 5 6 7 |
|
Обратите внимание, что для того, чтобы увидеть новое значение count
, мы добавили отчет console.log
после использования setCount
.
Ключевым моментом в реактивной системе Solid является то, что на самом деле нам не нужно этого делать. Вместо этого мы можем слушать — и мгновенно реагировать — на любые изменения сигнала, используя наш следующий примитив - эффект.
Реакция на изменения с помощью эффектов¶
Возможность реагировать на изменения сигналов лежит в основе реактивной системы Solid. Самый простой способ сделать это - использовать эффект. Мы можем создать эффект с помощью хука createEffect
:
1 2 3 4 5 6 7 8 9 |
|
Чтобы использовать createEffect
, мы передаем ему функцию. При обновлении сигналов, используемых в этой функции, функция будет перезапущена.
В данном примере наш эффект зависит от count
, поэтому он запускается при изменении count
. Соответственно, мы, как и раньше, выводим в консоль сообщение 1
.
Автоматическое отслеживание зависимостей эффектов стало возможным благодаря тому, что count
является функцией. Когда функция count
вызывается внутри эффекта, этот эффект регистрируется как слушатель сигнала. Вот почему так важно, чтобы наши сигналы были функциями!
svelte
Это эквивалентно $: console.log(count)
в Svelte.
react
В React зависимости объявляются в явном виде с помощью массива зависимостей:
1 2 3 |
|
В противном случае эффект будет запускаться заново при изменении любого состояния компонента. В Solid зависимости отслеживаются автоматически, и вам не нужно беспокоиться о лишних повторных запусках.
vue
Это эквивалентно watchEffect(() => console.log(count.value))
в Vue.
angular
Хотя Angular не может сравниться 1:1 с createEffect
в Solid, они по касательной похожи на хуки жизненного цикла Angular.
Однако createEffect
имеет ряд основных преимуществ перед методами жизненного цикла:
- Они могут выполняться вне компонентов.
- Они имеют более консолидированный API в результате отделения от компонентов.
- Они являются "композитными" (эффект можно поместить внутрь другого эффекта).
Рендеринг с помощью сигналов¶
Прежде чем мы вернемся к нашей книжной полке, давайте посмотрим пример использования этих примитивов внутри компонентов.
1 2 3 4 5 |
|
Мы видим, что, как и другие переменные, мы можем использовать сигналы в нашем JSX-коде, заключая их в фигурные скобки. Пока этот компонент не слишком интересен, поэтому давайте добавим возможность увеличивать счетчик. Для этого добавим элемент <button>
и зададим ему обработчик клик с помощью атрибута onClick
. Этот обработчик щелчка будет увеличивать наш счетчик с помощью функции setCount
:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
И теперь у нас есть работающий счетчик! Примечательно, что наш текст обновляется каждый раз, когда увеличивается count
. Не напоминает ли это вам эффект? При изменении сигнала код, управляющий этой частью DOM, запускается заново, подобно тому, как код в нашем эффекте запускается при изменении count
.
За кулисами компилятор Solid создает эффекты на основе нашего JSX. Он видит, что мы используем count()
в определенной части DOM, и создает эффект, который обновляет именно эту часть DOM при повторном выполнении сигнала.
Философия Solid заключается в том, что, рассматривая все как сигнал или эффект, мы можем лучше рассуждать о нашем приложении.
Пересмотр книжной полки¶
Теперь у нас есть инструменты, необходимые для того, чтобы сделать наше приложение "Книжная полка" интерактивным. В качестве иллюстрации приведем текущее состояние приложения со следующими компонентами:
BookList
, список книг на нашей Книжной полкеAddBook
- форма, позволяющая добавлять книги на полку.Bookshelf
, наш основной компонент приложения, который содержит два других компонента
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
В качестве первого шага к добавлению интерактивности давайте добавим сигнал, который будет отслеживать список книг. Назовем его books
, и он будет находиться в компоненте BookList
. Каждая книга будет иметь title
и author
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
Здесь следует отметить несколько моментов:
Во-первых, хотя до сих пор мы использовали createSignal
только для сохранения значения числа в состоянии, он может управлять всеми видами состояния. В нашем приложении "Книжная полка" сигналом является массив объектов.
Во-вторых, теперь мы используем books
непосредственно в нашем JSX-коде. Мы вызываем books()
для доступа к массиву сигналов, затем обращаемся к элементу с индексом 0
(ноль) этого массива в первом элементе списка и к элементу с индексом 1
этого массива во втором элементе списка. Это работает, но не является гибким: мы хотим обрабатывать динамическое количество книг.
Перебор элементов¶
Лучшим способом перебора элементов в Solid является компонент <For />
. Компонент <For />
имеет пропс each
, которому мы можем передать наш массив books()
.
1 |
|
Внутри компонента For
мы используем функцию callback, которая будет применяться к каждому элементу массива. В данном случае мы хотим, чтобы каждая книга
отображалась внутри <li>
.
1 2 3 4 5 6 7 8 9 |
|
react
В React мы бы использовали array.map
:
1 2 3 4 5 6 7 |
|
Если бы мы использовали здесь array.map
в Solid, то каждый элемент внутри книги пришлось бы перерисовывать при каждом изменении сигнала books
.
Компонент For
проверяет массив при его изменении и обновляет только необходимый элемент. Это та же самая проверка, которую выполняет система рендеринга VDOM в React, когда мы используем .map
.
Обратите внимание, что, в отличие от React, нам не нужно указывать ключ
компоненту For
: он сравнивает каждый элемент по ссылке.
vue
В Vue вышеописанное будет выглядеть следующим образом:
1 2 3 |
|
Обратите внимание, что, в отличие от Vue, нам не нужно указывать key
компоненту For
: он сравнивает каждый элемент по ссылке.
svelte
В Svelte вышеизложенное будет выглядеть следующим образом:
1 2 3 |
|
Обратите внимание, что, в отличие от Svelte, нам не нужно предоставлять 'key' компоненту For
: он сравнивает каждый элемент по ссылке.
Теперь наш компонент BookList
выглядит следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
|
Производное состояние¶
Solid позволяет легко отслеживать производное состояние. Вы можете представить себе производное состояние как вычисления, основанные только на другой информации, которую вы уже отслеживаете в состоянии. В нашем приложении "Книжная полка" примером производного состояния может служить количество книг в списке: это длина массива books
в любой момент времени.
В Solid для вычисления производного состояния достаточно создать производный сигнал: функцию, которая опирается на другой сигнал:
1 |
|
Теперь при каждом вызове totalBooks()
Solid будет регистрировать базовый сигнал (books
) как зависимость, поэтому вычисляемое значение всегда будет актуальным.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
|
vue
В Vue можно написать:
1 |
|
При этом в памяти создается место для вычисленного значения, которое постоянно обновляется при изменении books
. Таким образом, если вы дважды использовали {{totalBooks}}
в своем шаблоне, то books.length
будет вызван только один раз. Производные сигналы в Solid не создают место в памяти; при каждом вызове totalBooks()
будет повторно выполняться код books().length
.
Для этого в Solid есть другая функция: createMemo
, которая будет повторно выполнять вычисления только при изменении зависимости.
1 2 3 |
|
svelte
В Svelte можно написать:
1 |
|
При этом в памяти создается место для вычисленного значения, которое постоянно обновляется при изменении books
. Таким образом, если вы дважды использовали {totalBooks}
в своем шаблоне, то books.length
будет вызвана только один раз.
Производные сигналы в Solid не создают место в памяти; при каждом вызове totalBooks()
будет повторно выполняться код books().length
.
Для этого в Solid есть другая функция: createMemo
, которая будет повторно выполнять вычисления только при изменении зависимости.
1 2 3 |
|
Поднятие состояния¶
Мы хотим добавить книгу в список с помощью компонента AddBook
. Однако есть одна проблема: как сделать так, чтобы сеттер setBooks
был доступен компоненту AddBooks
?
Мы знаем, что родители могут передавать пропсы дочерним компонентам, но как родственные компоненты могут передавать пропсы друг другу? Это распространенная проблема в Solid, и решение обычно заключается в том, чтобы поднимать состояние вверх до общего родителя. В данном случае наш сигнал books
может находиться в компоненте Bookshelf
. Затем компоненту BookList
можно передать данные из геттера.
Давайте начнем с того, что поднимем наш сигнал books
в компонент Bookshelf
и передадим его значение обратно в компонент BookList
. Вы можете увидеть изменения, которые мы сделали в файлах App.tsx
и BookList.tsx
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
Теперь наш массив книг находится в компоненте Bookshelf
. Затем мы передаем books()
компоненту BookList
. Теперь мы можем получить доступ к нашим книгам внутри компонента BookList
с помощью пропса props.books
.
Вы, наверное, заметили, что при передаче компоненту BookList
сигнала books()
мы вызвали books()
— это не опечатка! В Solid принято вызывать аксессор сигнала при передаче его компоненту. В фоновом режиме Solid делает это реактивным реквизитом, и реактивность будет отслеживаться в JSX дочернего компонента. (TODO: хорошее место для ссылки на обсуждение/руководство по пропсам и реактивности).
Добавление книг в список¶
Теперь, когда у нас есть поднятое состояние, мы можем добавить несколько книг в список. Передадим наш сеттер компоненту AddBook
и вызовем setBooks
при нажатии на кнопку Add Book
. Эти изменения можно увидеть в файлах App.tsx
и AddBook.tsx
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
Внутри AddBook
мы создали функцию addBook
, которая используется в качестве обработчика клик для кнопки нашей формы. Поскольку мы отправляем настоящую HTML-форму, мы используем event.preventDefault()
, чтобы предотвратить стандартное поведение формы, заключающееся в выполнении post-запроса. Далее мы вызываем props.setBooks
, но мы не совсем понимаем, что передать нашему сеттеру.
Мы знаем, что хотим сохранить существующие книги в списке, а затем добавить новую книгу, полученную из нашей формы. Чтобы получить существующие книги, мы могли бы использовать два различных подхода: мы могли бы передать сигнал books
нашему компоненту AddBook
. Хотя это и сработает, стоит рассмотреть второй вариант: использование формы callback function в сеттере. Мы его еще не использовали, а синтаксис выглядит следующим образом:
1 2 3 |
|
Используя эту форму, наш сеттер получает доступ к текущему значению сигнала.
Такая форма для нашей функции setBooks
решает первую проблему: наша функция addBook
может быть записана следующим образом:
1 2 3 4 5 6 |
|
Теперь нам необходимо добавить в этот список текст из вводимых форм. Для этого мы можем создать новый сигнал внутри компонента AddBook
, который будет отслеживать значения вводимых данных. Для того чтобы этот сигнал всегда был равен значениям вводимых данных, мы используем его обработчик onInput
. Кроме того, мы привяжем сигнал newBook()
к атрибуту value
нашего input
, чтобы убедиться, что наш input
всегда отражает значение сигнала.
Наконец, мы хотим добавить newBook
в список книг, а затем очистить поле ввода на случай, если пользователь захочет ввести еще несколько книг.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
Мы использовали оператор spread для создания нового массива books внутри нашего сеттера books. Это общий паттерн для Solid, который позволяет убедиться в том, что мы создаем новый массив, а не обновляем (или мутируем) существующий массив сигналов. По умолчанию Solid использует проверку ссылочного равенства при определении обновления сигнала.
Тестирование нашего приложения¶
Теперь у нас есть динамическое приложение "Книжная полка"! Попробуйте сами: вы должны иметь возможность добавлять книги с помощью компонента AddBook
и видеть, как эти книги добавляются в список в компоненте BookList
.