Автор: Маслов Андрей, Front-end разработчик.
Время чтения: ~10 минут

Содержание:
-
О статье
-
Инструментарий
-
Демо приложения
-
effector/reflect
-
effector-forms
-
Итоги
О статье
Важно!
Это вторая часть серии статей по менеджеру состояний Effector. Перед ознакомлением с этой статьей настоятельно рекомендую перейти к первой части, лишь затем вернуться к текущей.
Первая часть: Effector — убийца Redux? Туториал с нуля. Часть 1
На примере небольшого приложения с заметками мы рассмотрим основные инструменты при работе с Effector, затронем типизацию.
Инструментарий
Основной упор в этой статье сделан на использование методов из effector/reflect.
Этот инструмент позволит вам внести ясность в ваш код, а так же избавиться от множества рутинных вещей.
Так же затронем работу с формами, с effector-forms.
Используем React :3
Демо приложения
Небольшое показательное приложение с возможностью добавления, удаления, редактирования заметок.

GitHub, код можно развернуть и посмотреть по ссылке
@effector/reflect
Рассмотрим основные возможности библиотеки:
-
reflect
-
list
-
variant
reflect
Инициализируем следующую проблему:
import {$notes, deleteNote} from './model' const NotesList: React.FC<NotesListProps> = () => { const styles = useStyles() const notes = useStore($notes) return ( <div className={styles.container}> {notes.map((note, id) => ( <NoteItem onDelete={() => deleteNote({id})}>{note}</NoteItem> ))} </div> ) }
Заметили ? Нам приходится постоянно тащить за собой в компонент useStore, и чем больше данных вам нужно — тем сильнее разрастается компонент, и это мы еще даже не обрабатываем данные…
Давайте перейдем к первому этапу — воспользуемся reflect.
reflect принимает в себя объект, со следующими свойствами:
-
view (Наш UI)
-
bind (Объект с набором необходимых данных, которые собираемся прокинуть в view)
-
hooks (Хуки обработки при mount, unmount компонента)
Применяем и смотрим на разницу:
import {$notes, deleteNote} from './model' interface NotesListProps { notes: string[] deleteNote: ({ id }: {id: number}) => void } const NotesListView: React.FC<NotesListProps> = () => { const styles = useStyles() return ( <div className={styles.container}> {notes.map((note, id) => ( <NoteItem onDelete={() => deleteNote({id})}>{note}</NoteItem> ))} </div> ) } export const NotesList = reflect({ view: NotesListView, bind: { notes: $notes, deleteNote } })
Результат: мы отвязали наш UI компонент от данных, которые ранее были привязаны к компоненту. Кажется, будто бы мы наживаем себе больше проблем, да и к тому же кода стало больше… Но таких NotesList вы можете создать несколько, наследуя базовый view, в зависимости от тех данных, которые вы хотите видеть.
Если вы хотите типизировать reflect и обезопасить себя при байндинге данных, то стоит передать в дженерик первым аргументом ваш интерфейс view
export const NotesList = reflect<NotesListProps>({ view: NotesListView, bind: { notes: $notes, deleteNote, someProp: 1 //TS ERROR! } })
variant
Продолжаем наш «рефакторинг».
variant позволяет нам очень просто обрабатывать состояние наших компонент.
Принимает объект со следующими свойствами:
-
source (Принимает case — состояние компонента, например, $store<string> = ‘loading’ | ’empty’ | ‘ready’)
-
bind (Принцип как и в reflect)
-
cases (обработчик ваших состояний, принимает объект ключ case — значение component)
-
default (можете обезопасить себя, если вдруг source окажется пустым)
-
hooks (Принцип как и в reflect)
Обработаем кейс, когда заметок нет.
//model.ts // event на добавление заметки в список export const addNewNote = createEvent<string>() // event на удаление заметки из списка export const deleteNote = createEvent<{id: number}>() // event на редактирование заметки export const editNote = createEvent<{id: number, value: string}>() // store заметок export const $notes = createStore<string[]>([]) .on(addNewNote, (store, payload) => ([ ...store, payload ])) .on(deleteNote, (store, payload) => ( store.filter((_note, id) => id !== payload.id) )) .on(editNote, (store, payload) => ( store.map((note, id) => { if (payload.id === id) return payload.value return note }) )) // store с состоянием стора с заметками // (с помощью map мы создаем производный стор, на основе $notes) export const $notesTypeState = $notes.map(store => { if (store.length) { return 'data' } return 'empty' }) //index.tsx export const NotesListVariant = variant({ source: $notesTypeState, cases: { data: NotesListView, empty: () => <Typography variant='h6' color='secondary' style={{ marginLeft: '78px' }}>List is empty</Typography> }, bind: { notes: $notes, deleteNote } })
Думаю вы заметили вот такую запись createEvent<string>(), дело в том, что event может принимать в себя payloads, вызывая этот event мы обязаны передать ему данные, которые обязательно попадут в стор через .on
.on(addNewNote, (store, payload) => ([
…store, payload
]))
В коде мы по клику на кнопку вытаскиваем значение из target инпута и передаем его в event, который ожидает в аргументах строку, после чего эта строка попадает в стор как новая заметка.
Вернемся к примеру выше, мы создали производный стор, который реагирует на родительский и меняет свое значение, в нашем случае мы создаем два кейса: data, empty (в первом случае — $store имеет длину, а значит имеет заметки, во втором — заметок нет).
В variant мы прокидываем cases: {case1: View1, case2: View2…}, effector автоматически подтянет типы из стора, и применит их к полю cases, ничего лишнего отдать не выйдет, но как и в первом случае вы можете жестко типизировать и контролировать этот момент, необходимо в дженерик прокинуть первым аргументом — интерфейс view (тем самым типизируя bind), вторым — тип стора (тем самым типизируя cases).
export const NotesListVariant = variant<NotesListProps, 'data' | 'empty', {}>()
Результат: мы избавляемся от написания оберток, с обработчиками стора, функциями рендера по условию, от лишних файлов и лишних строчек кода.
list
Казалось, что еще можно сделать ? Ответ: объединить наш reflect и view (где происходит map заметок). В этом нам поможет метод list.
List принимает объект, с чуть большим кол-вом свойств:
-
source (Отсюда черпаем данные, Store<any>)
-
view (Наш UI)
-
mapItem (Имитируем map + bind)
-
bind (Прокидываем дополнительные данные, которых нет в функции map)
-
hooks (Аналогично остальным методам)
-
getKey (Ключи для оптимизации)
В итоге наша картина складывается очень гармонично.
Было:
const NotesList: React.FC<NotesListProps> = () => { const notes = useStore($notes) if (!notes.length) { return <Typography variant='h6' color='secondary' style={{ marginLeft: '78px' }}>List is empty</Typography> } //if (some condition1...) {} //if (some condition2...) {} //if (some condition3...) {} return ( <div> {notes.map((note, id) => ( <NoteItem value={note} onDelete={() => deleteNote({ id })} onSave={(value) => editNote({ id, value }} key={id} /> ))} </div> ) }
Стало:
const NotesListView = list({ view: NoteItem, source: $notes, mapItem: { value: (note) => note, onDelete: (_, id) => () => deleteNote({ id }), onSave: (_, id) => (value) => editNote({ id, value }) }, getKey: () => React.Key }) export const NotesListVariant = variant({ source: $notesTypeState, cases: { data: NotesListView, empty: () => <Typography variant='h6' color='secondary' style={{ marginLeft: '78px' }}>List is empty</Typography> } })
Вуаля! Думаю результат на все сто. Из variant у нас ушли привязка пропов через bind — теперь этим занимается list.
Вы можете написать обертку, которая будет принимать начальный стор и генерировать производный с состоянием компонента, например, для самых популярных решений: loading, error, ready, empty. И работа упростится в разы (Нужды каждый раз писать кейсы, создавать сторы с типами кейсов и тд не будет).
effector-forms
Остановимся буквально на пару слов, для начала работы с формами вам стоит знать лишь о существовании effector-forms.
Пример ниже взят из документации, ибо его более чем достаточно, вопросов возникать не должно, мотивация — избавиться от создания сторов на каждое поле (в нашем приложении все завязано на сторе, т.к поле одно).
В материалах оставлю ссылку, где каждый кейс подробно разобран, прочитав, вы сразу же сможете пользоваться effector-forms.
import { createEffect } from "effector" import { createForm } from 'effector-forms' //инициализация формы export const loginForm = createForm({ fields: { //добавление филдов email: { init: "", //дефолтное значение rules: [ //в валидатор прокидывайте ваши yup схемы и наслаждайтесь { name: "email", validator: (value: string) => /\S+@\S+\.\S+/.test(value) }, ], }, password: { init: "", // field's store initial value rules: [ { name: "required", validator: (value: string) => Boolean(value), } ], }, }, validateOn: ["submit"],//тип валидации (submit, change..., аналогично react-hook-form) })
Итоги
Друзья, стоит понимать, что это лишь основная часть, с которой можно ознакомиться буквально за время выпитой чашки кофе. Со знаниями из первых двух частей статей уже можно работать и разрабатывать приложения, а effector в этом вам поможет. Обязательно ознакомьтесь с материалами для закрепления, там вы спокойно найдете ответы на интересующие вас вопросы. В статье не показал, где именно вызываются эвенты и где передаются данные, с этим вы можете разобраться, перейдя в репозиторий github.
В следующей и заключительной статье мы поговорим об архитектуре (так как текущая реализация собрана на коленке для наглядности функционала), и доработаем наше маленькое приложение.
Материалы для закрепления
ссылка на оригинал статьи https://habr.com/ru/post/701160/
Добавить комментарий