Многим уже известен стейт-менеджер effector, кто-то его уже не только смотрел, но и использует в проде. С конца осени его автор активно разрабатывает девтулзы для эффектора, и в процессе этой работы у него получилось написать очень интересную библиотеку для рендера приложения — effector-dom.
С этим рендером и познакомимся — в этом туториале мы с вами будем создавать простое Todo приложение.

Для работы с логикой будем использовать effector, для рендера приложения — effector-dom.
Для визуальной части возьмем за основу уже готовый тимплейт todomvc-app-template со стилями todomvc-app-css за авторством tastejs.
1. Подготовка проекта
Предполагаю, что вы уже знакомы с вэбпаком и npm, поэтому шаг установки npm, создание проекта с webpack и запуск приложения мы пропускаем (если не знакомы — то в гугле webpack boilerplate).
Устанавливаем необходимые пакеты с версиями, которые использовались на момент написания статьи:
npm install effector@20.11.5 effector-dom@0.0.10 todomvc-app-css
2. Начнем
Для начала определимся со структурой приложения.
Сразу договоримся, что создаем самое простое приложение, только чтобы понять принцип работы, здесь не будет каких-то best practices по именованию компонентов и разделению на фичи.
Мы создадим очень простую структуру приложения, максимум что отделим отображение от логики и немного вынесем шаблонный код.
Для нашего маленького приложения этого более чем достаточно:
srs/ view/ app.js // вся вьюха приложения title.js // заголовок footer.js // счетчик задач, кнопки фильтрации, и удаления выполненных header.js // создание новой задачи, выбор всех задач main.js // список задач todoItem.js // отдельная задача, выбор и удаление model.js // логика работы index.js // вход в приложение
3. Создание логики
Посмотрев на темплейт приложения, быстро определим возможные действия: задачу можно создать, отметить выполнение одной или всех сразу, отфильтровать и удалить выполненные.
В эффекторе для хранения любых (кроме undefined) данных используется Store, для событий — Event.
Также возможно создание комбинированных сторов с помощью функции combine, это мы используем для фильтрации задач.
// src/model.js import {createStore, createEvent, combine} from 'effector'; // сторы // все задачи export const $todos = createStore([]); // текущий фильтр, для простоты будет null/true/false export const $activeFilter = createStore(null); // отфильтрованные задачи export const $filteredTodos = combine( $todos, $activeFilter, (todos, filter) => filter === null ? todos : todos.filter(todo => todo.completed === filter) ); // события // добавление новой задачи export const appended = createEvent(); // выполнение/снятие выполнения задачи export const toggled = createEvent(); // удаление задачи export const removed = createEvent(); // выполнение всех задач export const allCompleted = createEvent(); // удаление выполненных задач export const completedRemoved = createEvent(); // фильтрация задач export const filtered = createEvent();
Теперь созданные сторы и ивенты необходимо связать и создать логику их взаимодействия.
Сторы могут реагировать на события и изменение других сторов, сделав подписку через store.on
// src/model.js ... $todos // добавление новой задачи .on(appended, (state, title) => [...state, {title, completed: false}]) // удаление задачи. Для простоты будем проверять title .on(removed, (state, title) => state.filter(item => item.title !== title)) // выполнение/снятие выполнения .on(toggled, (state, title) => state.map(item => item.title === title ? ({...item, completed: !item.completed}) : item)) // выполнение всех задач .on(allCompleted, state => state.map(item => item.completed ? item : ({...item, completed: true}))) // удаление выполненных задач .on(completedRemoved, state => state.filter(item => !item.completed)); $activeFilter // фильтрация .on(filtered, (_, filter) => filter);
Вот и вся логика нашего приложения. Несколько сторов для хранения данных и несколько событий для работы с ними.
4. Создание общей view
Создавая view приложения мы воспользуемся effector-dom. По словам автора, он вдохновлялся SwiftUI и визуально это чувствуется.
Особенностью данной библиотеки является то, что он основан на стэке, вложенные колбэки выполняются строго внутри своего контекста и один раз. Вся реактивность основана на прямом биндинге dom-элементов к юнитам эффектора и не вызывает повторный запуск колбэков.
Для работы с effector-dom требуется связать его с конкретным dom элементом, внутри которого и будут создаваться новые элементы. Для этого используется функция using, которая принимает сам dom-элемент и колбэк, выполняемый внутри указанного контекста:
// src/index.js import {using} from 'effector-dom'; import {App} from './view/app'; using(document.body, () => { App(); });
Создание новых элементов происходит с помощью функции h, которая принимает строку с типом dom-элемента и колбэк, выполняемый внутри созданного элемента.
Для установки параметров dom-элемента используется функция spec, которая должна быть вызвана внутри переданного колбэка:
// src/view/app.js import {h, spec} from 'effector-dom'; import classes from 'todomvc-app-css/index.css'; import {Header} from './header'; import {Main} from './main'; import {Footer} from './footer'; export const App = () => { // создадим section элемент h('section', () => { // и укажем ему класс spec({attr: {class: classes.todoapp}}); // также выведем остальные части приложения Header(); Main(); Footer(); }); };
Таким же образом создадим остальные вьюхи.
5. Создание заголовка
По факту это будет просто h1 элемент с текстом.
Если создание нового dom-элемента включает в себя только установку параметров, без вложенных элементов, то можно воспользоваться сокращенным синтаксисом. Для этого spec можно не вызывать, а просто передать объект с параметрами вторым аргументом при создании:
// src/view/title.js import {h} from 'effector-dom'; export const Title = () => { h('h1', {text: 'todos'}); };
6. Создание новой задачи
Благодаря своему устройству, в effector-dom можно создавать различные юниты effector прямо в колбэке, не беспокоясь о подписках, сборщике мусора и т.д.
Сейчас мы создадим форму добавления новой задачи и заодно кнопку выбора всех задач, все обернем в элемент header. Обработчики событий можно присоединить к dom-элементу также в spec (или в сокращенном варианте), в ключе handler.
Перед созданием новой задачи нам необходимо проверить поле ввода и очистить его после добавления, для этого сделаем input контролируемым, добавим стор $value и событие input.
Для проверки ввода создадим новое событие с помощью специальной функции sample и заодно отфильтруем на наличие данных. В api эффектора sample — это довольно мощный инструмент, но мы используем только его простейшую форму — создадим новое событие, в которое придет значение указанного стора при вызове триггера: event = sample($store, triggerEvent).
// src/view/header.js import {h, spec} from 'effector-dom'; import {createEvent, createStore, forward, sample} from 'effector'; import classes from 'todomvc-app-css/index.css'; import {Title} from './title'; import {appended} from '../model'; export const Header = () => { h('header', () => { Title(); h('input', () => { const keypress = createEvent(); const input = createEvent(); // создадим фильтруемое событие, const submit = keypress.filter({fn: e => e.key === 'Enter'}); // стор с текущим значением инпута const $value = createStore('') .on(input, (_, e) => e.target.value) .reset(appended); // заодно очистим при отправке // для перенаправления события в другое в эффекторе есть forward({from, to}) forward({ // возьмем текущее значение $value по триггеру submit, // и сразу сделаем фильтрацию для проверки значения from: sample($value, submit).filter({fn: Boolean}), to: appended, }); spec({ attr: { class: classes["new-todo"], placeholder: 'What needs to be done?', value: $value }, handler: {keypress, input}, }) }); }); };
7. Создание списка задач
Для вывода списка элементов, в effector-dom есть специальная функция list.
Способ вызова не сложный — передаем объект со стором, ключем и списком нужных полей, а также колбэк для отдельного элемента. Есть еще и более простой вызов list($store, itemCallback) но он нам сейчас не очень подойдет.
Заодно выведем кнопку выполнения всех задач.
К сожалению, в стилях todomvc-app-css эта кнопка сделана почему-то в одной секции со списком, хотя визуально находится в заголовке. Поэтому оставим здесь же.
// src/view/main.js import {h, spec, list} from 'effector-dom'; import classes from 'todomvc-app-css/index.css'; import {TodoItem} from './todoItem'; import {$filteredTodos, allCompleted} from '../model'; export const Main = () => { h('section', () => { spec({attr: {class: classes.main}}); // выбор всех задач h('input', { attr: {id: 'toggle-all', class: classes['toggle-all'], type: 'checkbox'} }); h('label', {attr: {for: 'toggle-all'}, handler: {click: allCompleted}}); // список задач h('ul', () => { spec({attr: {class: classes["todo-list"]}}); list({ source: $filteredTodos, key: 'title', fields: ['title', 'completed'] // в fields окажутся сторы с их значениям }, ({fields: [title, completed], key}) => TodoItem({title, completed, key})); }); }); };
8. Создание отдельной задачи
Благодаря своему устройству, в effector-dom можно создавать наследуемые сторы и события без каких-либо проблем с подписками, утечками памяти и т.д.
Так как задач может быть несколько, то нам необходимо в события модели toggled и removed как-то передать признак конкретной задачи — ключ.
Для этого воспользуемся возможностью effector создать событие с предустановкой параметров базового — делается это с помощью метода event.prepend.
Для указания класса выполненным задачам создадим наследуемый стор с помощью store.map
// src/view/todoItem.js import {h, spec} from 'effector-dom'; import classes from 'todomvc-app-css/index.css'; import {toggled, removed} from '../model'; // title и completed - сторы с конкретными значениями export const TodoItem = ({title, completed, key}) => { h('li', () => { // новый наследуемый стор с классом по флагу spec({attr: {class: completed.map(flag => flag ? classes.completed : false)}}); h('div', () => { spec({attr: {class: classes.view}}); h('input', { attr: {class: classes.toggle, type: 'checkbox', checked: completed}, // новое событие с предустановкой параметров handler: {click: toggled.prepend(() => key)}, }); h('label', {text: title}); h('button', { attr: {class: classes.destroy}, // новое событие с предустановкой параметров handler: {click: removed.prepend(() => key)}, }); }); }); };
9. Создание футера
Используя уже известные возможности создадим футер с каунтером задач, кнопками фильтрации и удаления выполненных задач
// src/view/footer.js import {h, spec} from 'effector-dom'; import classes from 'todomvc-app-css/index.css'; import {$todos, $activeFilter, filtered, completedRemoved} from '../model'; export const Footer = () => { h('footer', () => { spec({attr: {class: classes['footer']}}); h('span', () => { // Каунтер активных задач spec({attr: {class: classes['todo-count']}}); const $activeCount = $todos.map( todos => todos.filter(todo => !todo.completed).length ); h('strong', {text: $activeCount}); h('span', {text: $activeCount.map(count => count === 1 ? ' item left' : ' items left' )}); }); h('ul', () => { // кнопки фильтров, ничего нового spec({attr: {class: classes.filters}}); h('li', () => { h('a', { attr: {class: $activeFilter.map(active => active === null ? classes.selected : false )}, text: 'All', handler: {click: filtered.prepend(() => null)}, }); }); h('li', () => { h('a', { attr: {class: $activeFilter.map(completed => completed === false ? classes.selected : false )}, text: 'Active', handler: {click: filtered.prepend(() => false)}, }); }); h('li', () => { h('a', { attr: {class: $activeFilter.map(completed => completed === true ? classes.selected : false )}, text: 'Completed', handler: {click: filtered.prepend(() => true)}, }); }); }); h('button', { attr: {class: classes['clear-completed']}, text: 'Clear completed', handler: {click: completedRemoved}, }); }); };
10. Уточнения
Вот такое простое приложение получилось, специально делалось как можно проще, с минимальным разделением на отдельные сущности.
Конечно, при желании те же кнопки фильтрации можно вынести в отдельную сущность, которой можно передать тип фильтра и название.
const FilterButton = ({filter, text}) => { h('li', () => { h('a', { attr: {class: $activeFilter.map(completed => completed === filter ? classes.selected : false )}, text: text, handler: {click: filtered.prepend(() => filter)}, }); }); }; h('ul', () => { spec({attr: {class: classes.filters}}); FilterButton({filter: null, text: 'All'}); FilterButton({filter: false, text: 'Active'}); FilterButton({filter: true, text: 'Completed'}); });
Таким же образом, благодаря своей работе на основе стэка, effector-dom позволяет свободно выносить не только отдельные элементы, но и общее поведение.
Вынесенный код будет применяться именно к нужным элементам, например:
const WithFocus = () => { const focus = createEvent(); focus.watch(() => console.log('focused')); spec({handler: {focus}}); }; h('input', () => { ... WithFocus(); ... });
11. Итого
Меня лично впечатлила простота работы с новым рендером, тем более у меня уже был опыт работы с effector в большом приложении.
Жду с нетерпением стабильной версии, какой-нибудь популяризации для возможности использовать рендер в проде.
ссылка на оригинал статьи https://habr.com/ru/post/487938/
Добавить комментарий