В этой статье познакомимся с SolidJS − JavaScript-библиотекой для создания пользовательских интерфейсов без виртуального DOM. Мы создадим легкий список задач с использованием TypeScript и разберем некоторые особенности библиотеки.
SolidJS
Что такое SolidJS?
SolidJS является JS библиотекой с открытым исходным кодом. Сами разработчики пишут на своем гитхабе: «Solid − это декларативная библиотека JavaScript для создания пользовательских интерфейсов. Вместо использования виртуального DOM он компилирует свои шаблоны в реальные узлы DOM и обновляет их с помощью детализированных реакций».
Уже в описании можно понять с какой популярной библиотекой идет сравнение про виртуальный DOM. SolidJS похож на React, предоставляя нам возможности компонентной архитектуры, хранение и обновление данных с помощью сигналов (реактовские хуки), а также пару интересных возможностей.
У SolidJS достаточно хорошая документация на русском языке и имеет ряд туториалов. Однако, в русскоязычном сегменте мало применения этой библиотеки на практике, поэтому эта статья посвящена именно этому вопросу.
Начало работы
Нам нужен пакетный менеджер npm и среда разработки VS Code. Также будем использовать сборщик Vite (можно использовать и webpack).
В терминале перейдем в директорию, где будет размещена папка с проектом, и запустим следующую команду: npm create vite@latest todo-list -- --template solid-ts Эта команда создаст нам директорию todo‑list и некоторые начальные файлы.
Перейдем в папку проекта: cd todo-list и выполним стандартную команду установки пакетов: npm install
Запустим проект и проверим, что все работает: npm run dev
Если мы все сделали правильно, то на адресе http://localhost:5173/ увидим эту страницу:
Стартовая страница
Дополнительно
Для стилизации добавим свободный набор с готовыми стилями − Bootstrap. В файле index.html вставим следующие строчки:
Перед закрывающим тегом :
Перед закрывающим тегом
:
Также удалим файл App.css. В index.css удалим содержимое и вставим свое:
Наше будущее приложение будет состоять из трех компонентов:
Задача − флажок «выполнена/не выполнена» и название задачи.
Текстовое поле для ввода задачи.
Кнопка для добавления/удаления задачи.
В итоге у нас должно получиться вот такое готовое приложение.
Первые компоненты
Задача
В папке src создадим новый файл components/task/index.tsx и в нем создадим компонент для отображения одной задачи:
export default function Task() {
return (
);
}
В input мы будем передавать значение выполнена или не выполнена задача и рисовать на основе этого флажок, а в тег label мы передадим название задачи. Эти значения будем получать из параметров (далее − пропсов) компонента, представлены в объекте props (код ниже).
Теперь добавим адаптивности. В пропсы будем передавать название и статус (выполнена/не выполнена). Здесь наступает первый интересный момент в SolidJS.
Все динамические изменения в SolidJS − это реактивность. Здесь она нам нужна, чтобы мы могли динамически менять статус задачи без перерисовки страницы. Чтобы реактивность работала правильно, нельзя деструктуризировать объект props, как в React. Пропсы в SolidJS доступны только для чтения и уже имеют реактивные свойства, при деструктуризации эти свойства теряются, поэтому очень важно сохранить объект props.
Но если мы хотим задать некоторые параметры по умолчанию, то как это сделать? В таком случае нам необходим метод − mergeProps. В mergeProps необходимо передать объект со значениями пропсов по умолчанию и объект входящих пропсов, в итоге у нас получится новая переменная:
Теперь перейдем в App.tsx, удалим весь код, что там есть, и вставим свой:
import Task from "./components/task";
function App() {
return (
);
}
export default App;
Опять перейдем на http://localhost:5173/ и увидим наш результат (проект должен быть запущен):
Результат вывода одной задачи с параметрами по умолчанию
Отлично! Полдела сделано, осталось только создать еще парочку компонентов.
Поле ввода задачи
Создадим файл components/input-field/index.tsx и запишем следующее:
type Props = {
onChanged: () => void;
};
export default function InputField(props: Props) {
return (
);
}
В input будем записывать наши задачи, поэтому необходим метод для отслеживания изменений − onChanged. В этом компоненте уже не будем определять пропсы по умолчанию, сделаем функцию onChanged обязательной для вызова этого компонента.
Кнопка для удаления или добавления задачи
Создадим один компонент для двух действий и в пропсах будем передавать тип кнопки. В файле components/button/index.tsx запишем:
Здесь также в просы прокидываем название для кнопки, обработчик события-клик и тип кнопки. У нас только два типа кнопок: удаление и добавление, поэтому можем записать их просто строчками (на будущее лучше вынести в отдельную константу). Тип кнопки нужен для добавление стилей.
Как вы наверное уже заметили, SolidJS предоставляет нам целых два свойства для указания класса: class и classList. Первый принимает на вход строку с названием класса, последний − объект, у которого слева находится класс, а справа − значение типа boolean, которое в случае true добавит класс к элементу.
Теперь наша папка с проектом должна выглядеть примерно так:
Директория проекта
Добавляем новые компоненты в App:
import Button from "./components/button";
import InputField from "./components/input-field";
import Task from "./components/task";
function App() {
return (
console.log("input changed")} />
);
}
export default App;
Пока в компоненты передаем заглушки с выводом в консоль, позже это изменим.
Теперь наше приложение выглядит так:
Отображение наших компонентов на странице
Мда, пока не очень красиво, сейчас это исправим.
Добавим файл components/task-row/index.tsx с компонентом для отрисовки задачи и кнопки «Удалить» на одной строке:
import Button from "../button";
import Task from "../task";
export default function TaskRow() {
return (
console.log("btn click")}
type="delete"
/>
);
}
И добавим в App несколько тегов:
import Button from "./components/button";
import InputField from "./components/input-field";
import TaskRow from "./components/task-row";
function App() {
return (
console.log("input changed")} />
console.log("btn click")}
type="add"
/>
);
}
export default App;
Теперь наше приложение будет выглядеть более опрятно:
Наш результат легкой стилизации
Первая реактивность
Теперь перейдем к самому интересному − логика нашего приложения. Добавим обработку добавления новой задачи.
В компоненте App создадим сигнал для хранения всех наших задач и определим тип ITask:
Вызов функции createSignal возвращает пару: значение + функция для изменения значения (тот же самый useState). Однако сигналы не привязаны к компонентам и эти строчки можно спокойно разместить перед компонентом App.
Мы создали несколько сигналов:
tasks − для хранения всех задач,
taskId − индекс для новой задачи (этот сигнал для учебных целей, можно обойтись и без него),
newTask − название для новой задачи.
Для создания новой задачи добавим в App.tsx следующую функциональность:
В функции добавления новой задачи, проверяем значение newTask, вызванного с помощью геттер-метода (все значения сигналов в SolidJS получаются с помощью круглых скобок => newTask ()). Если у нас непустая строка, то в setTasks добавляем новую задачу в конец списка tasks, увеличиваем индекс для следующей задачи и очищаем поле newTasks.
Для изменения значения сигнала используем сеттеры (методы установки), как setTaskId. Внутрь можно передать новое значение (как в setTasks) или преобразовать прошлое значение: setTaskId((prev) => prev + 1).
Обновим компонент InputField:
import type { JSX } from "solid-js";
type Props = {
value: string;
onChanged: (value: string) => void;
};
export default function InputField(props: Props) {
const handleOnChanged: JSX.EventHandler = (
e: Event
) => {
const target = e.target as HTMLInputElement;
if (target) props.onChanged(target.value);
};
return (
);
}
Здесь обновили пропсы и добавили функцию-обработчик, которая из объекта события получает ссылку на DOM элемент input (e.target) и его значение передает в родительскую функцию, переданную через props.
Здесь мы просто фильтруем значения по индексу задачи, сохраняем в итоговый сигнал те задачи, которые не равны переданному индексу.
А в компоненте TaskRow добавим обработчик нажатия на кнопку и передачу id задачи в функцию:
import { ITask } from "../../App";
import Button from "../button";
import Task from "../task";
type Props = {
task: ITask;
onDeleteClick: (id: number) => void;
};
export default function TaskRow(props: Props) {
const handleDeleteClick = () => {
props.onDeleteClick(props.task.id);
};
return (
);
}
Ну и самое главное − вывести все наши задачи на экран. Для этого воспользуемся специальным тегом For и добавим его в App вместо строчки :
{(item, _index) => }
В атрибут each необходимо передать массив, потом, как в функции map, определить элемент и индекс. Только в нашем случае index будет являться сигналом для отслеживания перемещение строки независимо от изменений внутри элемента.
Почему рекомендуется использовать именно For для массивов?
В SolidJS рекомендуется также использовать именно встроенный инструмент For, нежели метод map, по причине реактивности. Готовый инструмент позволяет не перерисовывать несколько раз одни и те же данные и сохраняет реактивность.
Перейдем на наш сайт, теперь список задач пустой. Попробуем добавить новую задачу:
Добавляем задачу
Добавилась :)
Вроде все работает!
Ну и конечно же, добавим переключение статуса задачи.
В компоненте Task обновим пропсы, добавив функцию обработчик:
Добавим в input атрибут onChange и нашу функцию из пропсов:
А теперь выведем зачеркнутый текст, если задача выполнена:
Продолжая об особенностях SolidJS − компонент Show. Этот компонент оптимально обрабатывает условие в шаблонах (хотя SolidJS знает что такое &&, но в документации советуют использовать именно встроенный компонент).
Пропс fallback выполняет функцию else и будет показан в том случае если условие, которое мы передали в when вернет false значение.
В компоненте TaskRow также добавляем обработчик и обновляем пропсы:
Для хранения всех задач мы используем сигнал с массивом, хотя каждая вложенная задача может менять значение. Вложенная реактивность реализуется в SolidJS с помощью специального метода createStore. Используем его для наших задач, вместо createSignal для tasks напишем следующее:
const [store, setStore] = createStore([] as ITask[]);
Функции handleAddClick и handleDeleteClick оставим примерно как было, а в методе handleIsDoneChanged реализуем нашу вложенность. В первый параметр setStore передаем функцию для выбора нужного объекта, вторым аргументов − поле, которое необходимо изменить и последний аргумент − изменение самого поля. Насколько лаконично это выглядит!
И в тег For передадим наш стор:
{(item, _index) => (
)}
Теперь мы реализовали все согласно рекомендациям SolidJS и добились более простой реактивности в наших вложенных задачах.
Полный код проекта есть в git-репозитории, а итоговый проект доступен по ссылке.
В этой статье мы рассмотрели лишь часть возможностей библиотеки, но, надеюсь, после прочтения библиотека SolidJS стала для вас более понятной :) Пишите в комментариях, если у вас остались какие-либо вопросы.