Знакомство с библиотекой Solid JavaScript

От автора: Solid — это библиотека JavaScript для создания пользовательских интерфейсов без виртуальной DOM. Она компилирует шаблоны до реальных узлов DOM один раз и обертывает обновления, так что при обновлении состояния выполняется только обновленный код.

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

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

Вот финальная демонстрация, если вам не терпится увидеть окончательный код и результат:

Как и для большинства фреймворков, мы можем начать с установки npm пакета. Чтобы использовать фреймворк с JSX, запустите:

npm install solid-js babel-preset-solid

Затем нам нужно добавить babel-preset-solid в наш конфигурационный файл Babel, webpack или Rollup:

"presets": ["solid"]

Или, если вы хотите создать небольшое приложение, вы также можете использовать один из их шаблонов:

# Create a small app from a Solid template
npx degit solidjs/templates/js my-app # Change directory to the project created
cd my-app # Install dependencies
npm i # or yarn or pnpm # Start the dev server
npm run dev

Существует поддержка TypeScript, поэтому, если вы хотите запустить проект TypeScript, измените первую команду на npx degit solidjs/templates/ts my-app.

Создание и рендеринг компонентов

Для визуализации компонентов синтаксис похож на React.js, поэтому он может показаться знакомым:

import { render } from "solid-js/web"; const HelloMessage = props => <div>Hello {props.name}</div>; render( () => <HelloMessage name="Taylor" />, document.getElementById("hello-example")
);

Нам нужно начать с импорта функции render, затем мы создаем div с некоторым текстом и свойством, и вызываем render, передавая компонент и элемент контейнера.

Затем этот код компилируется в реальные выражения DOM. Например, приведенный выше пример кода, скомпилированный Solid, выглядит примерно так:

import { render, template, insert, createComponent } from "solid-js/web"; const _tmpl$ = template(`<div>Hello </div>`); const HelloMessage = props => { const _el$ = _tmpl$.cloneNode(true); insert(_el$, () => props.name); return _el$;
}; render( () => createComponent(HelloMessage, { name: "Taylor" }), document.getElementById("hello-example")
);

Solid Playground довольно крутая штука, она показывает, что Solid имеет разные способы рендеринга, включая клиентский, серверный и клиентский с гидратацией.

Отслеживание изменения значений с помощью Signals

Solid использует хук createSignal, который возвращает две функции: геттер и сеттер. Если вы привыкли использовать такой фреймворк, как React.js, это может показаться немного странным. Обычно вы ожидаете, что первым элементом будет само значение; однако в Solid нам нужно явно вызвать геттер, чтобы перехватить, где считывается значение, для отслеживания его изменений. Например, если мы напишем следующий код:

const [todos, addTodos] = createSignal([]);

Отслеживание todos вернет не значение, а функцию. Если мы хотим использовать значение, нам нужно вызвать функцию, как в todos(). Для небольшого списка todo это будет так:

import { createSignal } from "solid-js"; const TodoList = () => { let input; const [todos, addTodos] = createSignal([]); const addTodo = value => { return addTodos([...todos(), value]); }; return ( <section> <h1>To do list:</h1> <label for="todo-item">Todo item</label> <input type="text" ref={input} name="todo-item" id="todo-item" /> <button onClick={() => addTodo(input.value)}>Add item</button> <ul> {todos().map(item => ( <li>{item}</li> ))} </ul> </section> );
};

В приведенном выше примере кода будет отображаться текстовое поле, и после нажатия кнопки «Добавить элемент», todos будут обновлены новым элементом и отображены в списке.

Это может показаться очень похожим на использование useState, так чем отличается использование геттера? Рассмотрим следующий пример кода:

console.log("Create Signals");
const [firstName, setFirstName] = createSignal("Whitney");
const [lastName, setLastName] = createSignal("Houston");
const [displayFullName, setDisplayFullName] = createSignal(true); const displayName = createMemo(() => { if (!displayFullName()) return firstName(); return `${firstName()} ${lastName()}`;
}); createEffect(() => console.log("My name is", displayName())); console.log("Set showFullName: false ");
setDisplayFullName(false); console.log("Change lastName ");
setLastName("Boop"); console.log("Set showFullName: true ");
setDisplayFullName(true);

Результатом выполнение приведенного выше будет:

Create Signals My name is Whitney Houston Set showFullName: false My name is Whitney Change lastName Set showFullName: true My name is Whitney Boop

Заметьте, что My name is …не регистрируется после установки новой фамилии. Это потому, что на данный момент ничего не отслеживает изменения lastName(). Новое значение displayName() устанавливается только при изменении значения displayFullName(), поэтому мы можем видеть новую фамилию, отображаемую, когда значение setShowFullName установлено обратно на true.

Это дает нам более безопасный способ отслеживать обновления значений.

Примитивы

В последнем примере кода я представил createSignal, а также пару других примитивов: createEffect и createMemo.

createEffect

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

// Don't forget to import it first with 'import { createEffect } from "solid-js";'
const [count, setCount] = createSignal(0); createEffect(() => { console.log("Count is at", count());
});

Count is at…регистрирует каждое изменение значения count().

createMemo

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

Например, если мы хотим отображать счетчик 100 раз и обновлять значение при нажатии на кнопку, использование createMemo позволит выполнить пересчет только один раз за клик:

function Counter() { const [count, setCount] = createSignal(0); // Calling `counter` without wrapping it in `createMemo` would result in calling it 100 times. // const counter = () => { // return count(); // } // Calling `counter` wrapped in `createMemo` results in calling it once per update.
// Don't forget to import it first with 'import { createMemo } from "solid-js";' const counter = createMemo(() => { return count() }) return ( <> <button onClick={() => setCount(count() + 1)}>Count: {count()}</button> <div>1. {counter()}</div> <div>2. {counter()}</div> <div>3. {counter()}</div> <div>4. {counter()}</div> <!-- 96 more times --> </> );
}

Методы жизненного цикла

Solid предоставляет несколько методов жизненного цикла, например onMount, onCleanup и onError. Если мы хотим, чтобы какой-то код запускался после первоначального рендеринга, нам нужно использовать onMount:

// Don't forget to import it first with 'import { onMount } from "solid-js";' onMount(() => { console.log("I mounted!");
});

onCleanup аналогичен componentDidUnmount в React. onError выполняется, когда есть ошибка в ближайшей дочерней области. Например, мы могли бы использовать его при сбое выборки данных.

Хранилища

Для создания хранилищ данных Solid использует createStore которое возвращает прокси-объект и функцию сеттера. Например, если бы мы изменили наш пример задачи, чтобы использовать хранилище вместо state, это выглядело бы примерно так:

const [todos, addTodos] = createStore({ list: [] }); createEffect(() => { console.log(todos.list);
}); onMount(() => { addTodos("list", [ ...todos.list, { item: "a new todo item", completed: false } ]);
});

Приведенный выше пример кода начинается с регистрации прокси-объекта с пустым массивом, за которым следует прокси-объект с массивом, содержащим объект {item: «a new todo item», completed: false}.

Следует отметить, что объект состояния верхнего уровня не может быть отслежен без доступа к его свойству — поэтому мы используем todos.list а не todos.

Если бы мы отслеживали todo в createEffect, мы бы увидели начальное значение списка, а не то значение, которое было после обновления, сделанного в onMount.

Чтобы изменить значения в хранилищах, мы можем обновить их, используя функцию настройки, которую мы определяем при использовании createStore. Например, если мы хотим обновить элемент списка задач на «завершено», мы могли бы обновить хранилище следующим образом:

const [todos, setTodos] = createStore({ list: [{ item: "new item", completed: false }]
}); const markAsComplete = text => { setTodos( "list", i => i.item === text, "completed", c => !c );
}; return ( <button onClick={() => markAsComplete("new item")}>Mark as complete</button>
);

Поток управления

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

Некоторые из них: For — перебрать элементы, Show -условно показать и скрыть элементы, Switch и Match — показать элементы, которые соответствуют определенному условию, и многое другое! Вот несколько примеров, показывающих, как их использовать:

<For each={todos.list} fallback={<div>Loading...</div>}> {(item) => <div>{item}</div>}
</For> <Show when={todos.list[0].completed} fallback={<div>Loading...</div>}> <div>1st item completed</div>
</Show> <Switch fallback={<div>No items</div>}> <Match when={todos.list[0].completed}> <CompletedList /> </Match> <Match when={!todos.list[0].completed}> <TodosList /> </Match>
</Switch>

Демонстрационный проект

Это было быстрое введение в основы Solid. Если вы хотите поэкспериментировать с ним, я создал стартовый проект, который вы можете автоматически развернуть на Netlify и клонировать в свой GitHub, перейдя по ссылке.

Этот проект включает в себя настройку по умолчанию для Solid проекта, а также образец приложения Todo с базовыми концепциями, которые я упомянул в этом посте.

Структура проекта гораздо больше, чем та, что я здесь рассмотрел, поэтому не стесняйтесь знакомиться с документацией для получения дополнительной информации!

Автор: Charlie Gerard

Источник: css-tricks.com

Редакция: Команда webformyself.

Читайте нас в Telegram, VK, Яндекс.Дзен