Как стать автором
Обновить

Комментарии 73

Очень много геморроя с данными исчезает, когда открываешь для себя react-redux.

А как в редаксе сегодня модно менять одну ветку стора в ответ на изменение другой ветки? Например, вам нужно при любом изменении фильтров сбрасывать текущую страницу выдачи, но не сбрасывать её, если фильтры фактически не поменялись.

Если фильтры поменялись и прошла проверка на реальное изменение, будет вызвано событие по изменению, на это же событие вешается extraReducers в redux-toolkit

Кем проверка? Кем вызвано?

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

Редакс-тулкит сейчас это умолчание для приложений, использующих редакс.

- Пап, а почему небо голубое?
- Ну там свет, преломление, все дела.
- Так а голубое-то почему?
- Слушай, на изначальный вопрос я ответил, а что там дальше уже дело десятое.

-Пап, а как сделать воду в стакане голубой?

-Добавь в стакан краску, краска в ящике

-Чем размешивать?

-А чем размешивать, это уже дело десятое, можешь вилкой, можешь пикой.

все очень просто

как раз для этого и придумали редакс

Для чего именно "этого" придумали редакс?

Например, вам нужно при любом изменении фильтров сбрасывать текущую страницу выдачи, но не сбрасывать её, если фильтры фактически не поменялись.

useEffect(() => {
  resetFilters();
  await search(filters);
}, [filters]);

Где resetFilters это store.dispatch({ type: 'resetFilters' });, а filters это скажем store.blabla.filters. Где-то будет reducer, который ловит resetFilters и делает return { list: [] };


Наверное ближайший аналог из мира observable:


// somewhere
st.filters = newFilters;

// somewhere else
st.filters.subscribe(async filters => {
  st.list = [];
  await search(filters);
});

В обоих случаях может потребоваться дополнительная обвязка которая выяснит что новый объект фильтров в точности равен старому (и тогда не задавать новое значение). Насколько я помню в $mol у вас это по-умолчанию. Но в большинстве observable решений либо этого нет, либо там shallow comparison. Но и там проблема решается (просто мануально).


В случае redux такая же картинка. Нужно будет проверить что filters реально изменились. Это можно легко сделать в reducer-е.

Хех, а обещали без геморроя..

В redux и без геморроя? Это едва ли.

Мне кажется, создание автоматических триггеров смены одних данных от других, как раз и вызывает запутанность кода.

Сделайте 2 простых экшена: setFilter(filter) и setPage(page) , а в форме фильтров на onSubmit вызывайте оба.

И теперь везде, где мы будем менять тот или иной фильтр (а их много разных, а не одна форма на всё), сортировку, группировку и прочее, нам нужно не забыть сбросить страницу, скроллинг, номер редактируемого элемента и прочее.

Ну да, если действие слишком частое, можно сделать дополнительный экшен setFilterAndPage({filter, page}).

Это намного лучше, чем сначала сделать автоматический сброс страницы, а потом появится кейс, где нужно поменять фильтр, а страница не должна сбрасываться.

Какой, например, кейс?

А что вы будете делать, когда от одного фильтра будет зависеть несколько списков? Зашивать в него инфу обо всех состояниях, что зависят от его изменения?

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

Редакс - это хранилище данных, в его задачи не входит автообновление.

Код получается непереиспользуемым, ибо один компонент знает про соседние компоненты (откоторых не зависит) и управляет их поведением.

Ну то есть геморрой с данными никуда не исчезает, ибо хата редакса - с краю.

Код получается непереиспользуемым, ибо один компонент знает про соседние компоненты (откоторых не зависит) и управляет их поведением.

Спорно. Не то что ответственности должны быть ограничены, а то, что тут с этим проблема.


<Filters onChange={updateFilters} filter={filters}/>

Наш Filters не заведует своим state-ом. Он uncontrolled. Он не знает про соседние компоненты, от которых он не зависит. А сама эта зависимость находится уровнем выше, там где есть доступ и к page и к filters. И именно там решаются все data вопросы.


Т.е. <Filters/> переиспользуемый и не знает о том, что в updateFilters может быть зашита отправка астронавтов на Марс. Это не его ума дело.


Обычно на React стараются избегать любых down-up взаимодействий. В итоге state лежит так высоко, как нужно.


Т.е. условно в observable мире можно так:


const fitlers = new FitlersModel();
const page = new PageModel();

// вытаскиваем всеми правдами и неправдами 
// filters и page модели туда куда нужно

filters.subscribe(() => {
  page.set(0); // reset page on every filters update
});

computed(() => {
  // autotrack to `filters` & `page`, re-query on updates
  list.set(await query(filters.get(), page.get());
});

Модели лежат где-то снизу. А использование их сбоку\снизу\слева\справа\где-угодно. В React экосистеме это считается dick-move. Такое организовать можно, но это считается wrong way.


В случае Redux там по сути есть некая глобальная переменная и компоненты обычно с ней напрямую не работают. А работают через прослойку, которая map-ет доступные в store действия на uncontrolled компоненты. И тогда они внутри уже не знают что произойдёт при updateFilters, т.к. это зарыто не в Filters а во wrapper-е для Filters. Суть примерно такая. В остальном как в React. Ну и обсуждаемый вопрос можно сделать и как:


const FormComp = ({ filters, page, setFilters, setPage }) => {
  useEffect(() => {
    setPage(0);
  }, [filters /* зависимость для вызова эффекта */]);
  // ...
}

и так:


const FormComp = ({ setFiltersAndPage, ... }) => {

Т.е. все совместные телодвижения находятся на том уровне где есть доступ ко всему. Не ниже.


P.S. перечитал комментарий и кажется он ничерта непонятный. Зря наверное я его написал :)

Меня всё-равно смущает, что сброс страницы привязывается именно к обновлению вильтров. Если бы эти сущности были настолько связаны, то их следовало бы делать в одной модели. Можно передать <Filters onChange={updateFiltersAndResetPage} filter={filters}/> и не будет проблем.)

Если бы эти сущности были настолько связаны, то их следовало бы делать в одной модели.

Я об этом выше и пишу.


updateFiltersAndResetPage

Именно об этом я и пишу, да. Наверное просто плохо выразил свою мысль.

Просто есть ещё такая альтернатива:


const [list, setList[ = useState([]);
const [formData, setFormData] = useState(
  { filters: null, page: null });
const onSetPageRef = useRef(null);

const onPaginationInit = (paginationCompApi) => {
  onSetPageRef.current = paginationCompApi.setPage;
}

const onUpdatePage = (newPage) => {
  setFormdata(prev => ({ ...prev, page: newPage }));
};

const onUpdateFilters = (newFilters) => {
  setFormData(prev => ({ ...prev, filters: newFilters }));
};

useEffect(() => {
  onSetPageRef.current(0); // ~> onUpdatePage ~> upd. page
}, [formData.filters]);

useEffect(() => { 
  setList(await debouncedQuery(formData.filters, formData.page));
}, [formData.filters, formData.page]);

return <Form>
  <Filters onUpdate={onUpdateFilters}/>
  <Pagination onUpdate={onUpdatePage} onRef={onPaginationInit} />
  <List list={list}/>
</Form>

Тут вроде и модели снизу сами по себе живут, а родительский компонент только уведомляют о смене значения. Т.е. они controlled. И родительский компонент имеет сведения о том что там снизу считается актуальными данными. Помимо этого имеется педаль для обновления страницы из родительского компонента. Ну и два эффекта. По одному сбрасываем страницу мануально при обновлении фильтров. Во втором эффекте обновляем список при изменении страницы и\или фильтров. Но debounce, чтобы не слать сразу два запроса при смене фильтров.


Как не трудно заметить это какая-то лютая ахинея и так жить нельзя. React не любит down-top связи. А top-down любит.


В случае redux-react у нас такая бизнес-логика лежит в global store, а он "типа" отдельно живёт от react-component tree. Поэтому reducer для пагинации может отлавливать action-ы, которые для reducer-а от filters, и сам себя сбрасывать в ноль. Это будет нарушением принципа ограниченной ответственности. Но redux это в целом про "леплю что хочу как хочу", так же известный как pub/sub. Более "легально" такое можно сделать на уровне где combineReducers({ pagination, filters }) сделав его более сложным. Но тоже такое себе. Ну и можно просто это держать в 1-ом state (наверное лучшее решение).


Думаю это касается далеко не только redux, а вообще любых внешних store-ов, включая mobX.

Чтобы приложение не гулючило вам придётся везде менять фильтры через updateFiltersAndResetPageAndScrollToTopAndDisableEditing.

Потом у вас появится экшен updateSortingAndResetPageAndScrollToTopAndDisableEditing.

А вишенкой на торте станет updateCategoryAndResetFiltersAndResetSortingAndResetPageAndScrollToTopAndDisableEditing

И это ещё далеко не конец..

Какое решение предлагаете вы?

Названия же не обязательно именно такими делать, верно?

Реакт - это прям сборник кривых архитектурных решений. Так что лучше не пинать эту дохлую лошадь, а взять любую вменяемую систему реактивности.

React + MobX — единственная жизнеспособная связка. Откройте уже глаза. Либо берите Vue 3 и не мучайтесь.

В итоге state лежит так высоко, как нужно.

Обычно в Реакте он быстро улетает куда выше, чем нужно (и Редакс - это апогей). Вот и в вашем примере, номер страницы улетел из компонента "список с постраничной навигацией" куда-то наверх, чтобы вручную закатывать солнце сбрасывать страницу, когда этот список перезагружается. И я подчеркну: сбрасывать надо не только страницу, но и другие состояния, про которые за пределами списка знать совсем не обязательно.

Вообще, разделение компонент на контролируемые и нет - это антипаттерн, ибо плохо масштабируется, тормозит и провоцирует копипасту.

В каком-то смысле всё программирование антипаттерн. Что up-down, что down-up. Везде будет много граблей. Я как-то уже свыкся с тем, что не то чтобы silver bullet нет, а вообще, что нашёл работающее хорошо в конкретном случае решение — молодца, так держать. Нет? Ищи лучше.

А тут какие грабли?

class FilterForm extends $mol_object {

    // inner mutable state
    @ $mol_mem
    filter( next = {} ) { return next }

}

class PaginatedList extends $mol_object  {

    // input prop
    filter() { return {} }
    
    // inner mutable state
    @ $mol_mem
    page( next = 0 ) {
        this.filters() // reset on filters change
        return next
    }

    // inner mutable state
    @ $mol_mem
    scroll_top( next = 0 ) {
        this.filter() // reset on filter change
        this.page() // reset on page change too
        return next
    }

}

class App extends $mol_object {
    
    // delegated state
    filter( next ) {
        return this.Form().filter( next )
    }

    // standalone inner component
    @ $mol_mem
    Form() { return new FilterForm }
    
    // bound inner component
    @ $mol_mem
    List() {
        return new PaginatedList({
            // bind with FilterForm state
            filter: ()=> this.filter()
        })
    }
    
}

Понятия не имею. Это глубоко вникать надо. Т.е. взять $mol, изучить основы, пописать на нём месяц что-нибудь средних размеров. Начать вообще понимать, что это такое (все эти return next, значения конкретных декораторов, принципы работы, и т.д… $mol по-умолчанию весьма cryptic вещь. Даже без view-языка.


Я не исключаю, что он может быть в итоге хорошим вложением времени. Но не спешу бросаться на амбразуру.

Это ж обычный JS. Декоратор $mol_mem просто мемоизирует последнее возвращённое методом значение и автоматически сбрасывает кеш при его устаревании. Можете поиграться с этим в песочнице.

а потом появится кейс, где нужно поменять фильтр, а страница не должна сбрасываться.

YAGNI. Не появится. Это мало того, что звучит странно, так ещё более странно закладывать архитектуру под очень странный case, который может никогда не наступить.


Если вы сбросили фильтры у вас точно новая выборка данных. Там даже страниц может оказаться меньше.

Поправлю сам себя. Если есть кнопка "искать", т.е. фильтры не мгновенны, то да, можно менять фильтры без установки страницы. Был неправ в этом пункте.


Тут возникает вопрос что это за такой интересный интерфейс когда и мануальная кнопка есть, и какие-то "особые" фильтры которые подразумевают нажатие этой кнопки при изменении (и соответственно остальные фильтры, которые не подразумевает). Но учитывая, что интерфейсы бывают странными, такое возможно.


nin-jin Какой, например, кейс?

^

Изначально вопрос был об обновлении одних данных, при изменении других. Пример с фильтром не я привёл, наверное он действительно не очень удачный. Но если мы делаем автообновление, то оно будет срабатывать всегда, даже если это не нужно. Поэтому лучше всегда делать атомарные экшены, а если какие-то повторяются чаще, то можно сделать дополнительные экшены, которые будут объединять несколько. Так не потеряется гибкость. Я уже приводил примеры выше.

а в форме фильтров на onSubmit вызывайте оба.

До 18-го React-а такое потребуется батчить недокументированным API. Ну или рендерить промежуточное неконсистентное состояние. Привет прыгающие страницы (и в худшем случае баги и падения).


Тут лучше срабает useReducer, имхо

Возможно. Плохо, когда технические детали влияют на логику работы.

React такой React. Неспроста многие к нему всякие MobX сбоку прикручивают

Что за глупости? Вы сделали 11 ошибок в слове MobX.

Зато появляется геморрой с redux)

Какой, например?

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

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

Я вижу необходимость в redux и других хранилищах только в одном случае: когда приложению нужен глобальный стейт, например, всякие чаты.

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

(А вы знали, что useState - это обертка над redux-подобным диспетчером, как и useReducer?)

Ну каждый может делать что угодно, мы же не про различные маргинальные способы использования технологий тут рассуждаем :) Можно и отвёрткой хлеб нарезать так-то, но это не проблема отвёртки.

Во-первых, redux хранилищ может быть несколько, и это очень удобно, когда надо отделить сложный модуль в свою инфраструктуру. Например, это может быть виджет или страница. Это что касается "глобальных стейтов".

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

Без сомнения, useState вне конкуренции, когда нужно работать с локальными состояниями компонента. Но речь как я понимаю именно про состояния, выходящие за пределы компонента.

Какой, например?

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

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

Он просто местный mobX тролль. Появляется в каждой 2-й теме про JS и пишет про то, что mobX — серебряная пуля, а все вокруг идиоты. Причём это уже не 1-й аккаунт, прошлый видимо слили и он теперь удалён. В принципе и у нового уже -10. Но mobX джихад не остановить, и, я думаю, нас ждёт новый nickname :)

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

Redux головного мозга, так же печально известна как не излечимая в большинстве случаев болезнь. Соболезную конечно, но что ж теперь, жизнь злая штука.

Например, можно сравнить два варианта из сценария проверки на валидность текста. Есть input, есть стейт value и есть стейт isValid.

isValid это ни стейт, ни проп, а производная от первых двух. Поэтому его нужно не хранить в useState, а вычислять на лету (декларативность же):

function Form({required}) {
  const [value, setValue] = useState('')
  
  // Добавьте useMemo, если переживаете за производительность
  const isValid = validate(value, required)
  
  return (
    <div>
      <input
        value={value}
        onInput={event => setValue(event.target.value}
      />
      {!isValid && 'Не валидно'}
    </div>
  )
}

Если нужно не проводить валидацию только после взаимодействия пользователя с формой, добавьте стейт isTouched.

Если нужно не проводить валидацию только после взаимодействия пользователя с формой, добавьте стейт isTouched

У вас есть два сценария: "пользовательский ввод" и "компонент был смонтирован". Так и опишите их. Поместите в didmount функцию валидации и тогда вам не понадобится в сценарии "пользовательский ввод" проставлять флажок для сценария "компонент был смонтирован". Оба сценария станут короче и изолированнее.

В моём примере минимальное необходимое состояние и 1 короткий сценарий: пользовательский ввод. Остальное это декларативный рендер. Переключение флажка isTouched будет в том же коллбеке onInput. Кстати говоря, состояния value и isTouched зависимые, поэтому их можно поместить в один useState, чтобы при событии onInput происходил ровно 1 рендер.

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

Поместите в didmount функцию валидации и тогда вам не понадобится в сценарии "пользовательский ввод" проставлять флажок для сценария "компонент был смонтирован".

Я не говорю про изменение состояния сразу после маунта. Я говорю об изменении состояния в ответ на действие пользователя. Если бы мне нужно было изменить состояние при маунте, я бы изначально в useState передал нужное состояние.

Давайте рассмотрим сценарий побольше, нежели валидация инпута. Например, имеется список запланированных дел (типичная тудушка), у каждого запланированного действия есть кнопка «удалить», которая вызывает попап, который спрашивает «уверены ли вы, что хотите удалить запланированное действие».

Возможно у вас компонент алерты засунут внуть тудушки?

В своем проекте я делал алерты которые монтируются как отдельный компонент глобально почти что в самый root компонент. У алертов есть свой стор. Как только в этот стор тудушка кладет алерт он сразу отображается (алерты кстати точь в точь как вы и сказали сделаны: popout: { type, data }).



Я так сделал из принципа DRY — потому что вызвать алерт может не только тудушка.

В моей реализации компонент алерта общается с компонентом тудушки через стор (как вы сказали проброс пропсов сверху), но вот возникла задача как через стор прокидывать события из попапа обратно в компонент? (например событие удаления)

И здесь я решил остановится на варианте что в самих алертах уже буду запускать удаление. Удаление обновит стор и значит обновит тудушку.

Я писал в конце про библиотеки для работы с порталами. Это выглядело бы примерно так:

есть список:

const ToDoList = (props) => {
	const { items } = props
  
  const handleDelete = (item) => ...

	return items.map((el) => <ToDo onDelete={handleDelete} {...el} />)
}

и есть запланированное действие:

const ToDo = (props) => {
	const { onDelete, ...el } = props
  const [isAlertOpen, setIsAlertOpen] = useState(false)
  
  ...

	return (
  <div>
  ...
  {isAlertOpen && <Portal>
   <НекийКомпонентАлерт onDelete={() => onDelete(el)} />
</Portal>}
	...
	</div>
  )
}

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

Абсолютно согласен с вашим решением. Если нужно чтобы алерты были частью тудушки, тогда ваше решение самое лучше. В моем случае пришлось бы вешать событие на анмаунт тудушки, а в алерте подписываться на это событие чтобы не пропустить удаление.

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

Для простоты можете использовать библиотеку "react-portal" - он прикрепляет содержимое к document.body. Это не плохо подойдёт для всплывающих окон. Для сценариев, где layout сложный и прикрепление компонента к document.body не отвечает потребностям, я написал библиотеку "react-jsx-portal" (ссылка). Вторая часть readme на русском. Я думаю многим она подойдёт.

Удачи вам потом с таким подходом делать менеджер таких вот алертов. Ну когда там всякие анимации списков, лимиты и закреплённость. Я тоже таким баловался по первой. Потом пришёл к совсем другой схеме:


  • на верхнем уровне приложения есть HoC, который wrap-ает древо context-ом, в котором есть императивный метод showError(msg: ReactNode | Component, isFixed?: boolean): Promise<void> (и другие методы).
  • этот метод вызывается в тех самых useEffect или колбеках вроде onClick по мере необходимости ниже по древу
  • тот самый HoC сам всё это дело отображает. Сам решает в каком порядке показать. Когда удалить. Какие анимации задействовать и т.д.

Это очень упрощает (раз в 10) работу с такими временные явлениями как alert-ы. Внизу по месту не требуется хранить никакого state-а. Не требуется никаких порталов. Точка входа одна. Возможностей реорганизовать менеджмент таких сообщений — какие угодно, никаких ограничений с точки зрения архитектуры.


А самое главное почему это упрощает? Потому что эти алерты ничерта не декларативные по своей природе. Ну во всяком случае в тех проектах, с которыми я в последнее время работал. И если не натягивать сову на глобус, решая выдуманную проблему, то всё получается довольно удобно.


Подобным же образом у нас работают разные shared диалоги. Такие которые связаны не с конкретным местом в бизнес логики приложения, а "общего" характера. Ну скажем в facebook это может быть какой-нибудь:


const { openStoryPlayer } = useStoryPlayer();
const onClick = () => openStoryPlayer(story.id);

return <div {...{ onClick }}>...</div>;

По клику откроется плеер сразу для нужно сущности. При желании можно даже сделать так, чтобы этот императивный метод возвращал promise. Скажем если нужно что-то сделать сразу по закрытию диалога\popover-а\popup-а.


Стандартный декларативный React подход с тем чтобы везде по месту рендерить их top-down и руками handle-ить соответствующие стейты ничего кроме боле не вызывает. А учитывая что в 99% случаев активным может быть только 1 такой диалог, это ещё и бессмысленные страдания.

Вот кстати именно потому по тому что мне нужно было контролировать анимацию, и кол-во отображаемых алертов, я сделал в точности как вы и описали. Наверно только отличие что у меня сами алерты завязаны на сторе а у вас как я понял на событиях. Но сама суть одна и таже.

У меня была жесткая задача: 40 алертов а их надо по очереди показать, какие то алерты с возможностью выбора сценария, какие то сами схлопываются спустя время, а какие то при повторном вызове должне закрывать предыдущий. С помощью стора для алертов очень просто удалось это все порешать.

на верхнем уровне приложения есть HoC, который wrap-ает древо context-ом, в котором есть императивный метод showError(msg: ReactNode | Component, isFixed?: boolean): Promise (и другие методы).

Можете по подробнее объяснить этот момент?
Можете по подробнее объяснить этот момент?

Вы добавляете в global store новую запись, которую показывает по итогу некий <Alert/>, да? Примерно так:


// in a component
dispatch(addAlert("For the horde"));

// in a reducer
st.list.push({ msg: action.msg, id: genUniqId() });

// in a <AlertManager/>
{alerts.map(alert => <Alert key={alert.id} msg={alert.msg}/>}

Если да, то у меня похожая схема, но без глобальных сторов. Те самые alert: Alert[] у меня храняется в самом <AlertManager/> в local state. А вместо dispatch компоненты ниже по древу просто вызывают:


const showAlert = useContext(alertManagerContext);

Концептуально тоже самое. Просто я не храню всё это в сторе. Я вообще сторонник того чтобы все global store-ы горели в аду в глобальном store лежали ТОЛЬКО те вещи, которые нужны в нескольких местах приложения сразу. Например данные о зарегистрированном пользователе. Но это уже совсем другой холивар.


Ну и небольшой бонус хранения этого в локальном сторе менеджера — можно хранить ссылки на компоненты (которые будут .children для <Alert/>) или ссылки на JSX elements. В настоящий глобальный store такие вещи пихать не рекомендуется (обычно), т.к. такие вещи типа не очень сериализуемы. Но это уже тоже всякие догмы.

Вау! Реально круто. Я как раз столкнулся с тем что в глобальном сторе неудобно хранить данные которые все равно сильно привязаны к вьюшке. В вашем случае получается вообще все по красоте получается, если прокидывать прямо компонент через контекст. Реально потом подумаю как потом переделать под такую модель.

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

Вам спасибо за разъяснение!
const [value, setValue] = useState('');
const isValid = value.length !== ''; // or any other validator

Всё. В подавляющем большинстве сценариев вам не нужно хранить производные значения в state. Это своего рода путь к ад.


Конечно всё поломается, если вам нужен асинхронный валидатор. Но тут ничего нового. Асинхронные валидаторы при любой архитектуре это боль.


Возможно вы возразите, дескать isValid должен быть true при выполнении не одного, а сразу двух условий (isTouched). Ежели так — то городите логику для поддержки isTouch. А isValid оставьте производным значением. Это сильно упрощает логику.


Касательно форм и всех этих isValid, isTouched, isWhatever, resetForm, setError, asyncValidator и прочих штук из популярных библиотек по работе с формами… Они там неспроста. Как только вы захотите сделать форму по всем канонам идеального UX, вы упрётесь в то, что оно вынужденно такое всё сложное. Ибо сама задача очень сложная. И разумеется не надо её решать по месту в каком-то отдельно взятом useEffect или onClick. Закопаетесь.


Но главный вывод — вещи которые можно высчитать из значений в любой момент, нужно делать ленивыми и декларативными. А не как в обоих ваших примерах. Это путь в никуда.

 Асинхронные валидаторы при любой архитектуре это боль.

@ $mol_mem
isValid() {
  const mail = $mol_data_email( this.value() )
  const uri = '/check/email=' + encodeURIComponent( mail )
  return this.$.$mol_fetch.json( uri ).free
}

 isTouched, isWhatever, resetForm, setError, asyncValidator и прочих штук из популярных библиотек по работе с формами… Они там неспроста. Как только вы захотите сделать форму по всем канонам идеального UX, вы упрётесь в то, что оно вынужденно такое всё сложное.

На самом деле всякие isTouched - плохой UX. Проявляется обычно на предзаполненных формах (редактирование, автозаполнение), когда не видишь в форме ошибок, жмёшь "отправить" и получаешь в лучшем случае сообщение "ой, забыли вам сказать - поправьте вот тут, и вот тут".

На самом деле всякие isTouched — плохой UX

Ха. Вы предлагаете показывать голую форму сразу с ошибками "is required"? Юзер только открыл форму и уже видит 100500 ошибок валидации? В общем UX дизайнеры, которых я знаю, с вами не согласятся.


@ $mol_mem

К чему был этот пример и что он должен был показать? Заодно как решаются race?

Не надо call to action оформлять как ошибку и всё будет хорошо.

Что в правильной архитектуре там нет никакой боли. Предыдущие запросы отменяются автоматически.

Что такое "call to action"? Что покажет isValid на момент совершения запроса? Как происходит reset form? Ничего не понятно из этих пары строк.

Призыв к действию: "это поле заполнять обязательно". Не текстом, разумеется.

На время запроса автоматически рисуется индикатор ожидания.

Меняем редактируемую модель на новую - форма сбрасывается автоматически.

Обязательные поля помечаются звездочкой, но всё равно надо покрасить красной рамкой, если не заполнены. Причем покрасить либо при условии isTouched (после потери фокуса), либо при попытке засабмитить (здесь тоже будет какой-то стейт, например, shownErrors).

Почему? Да потому что хоть 100 звездочек нарисуй рядом с полем, юзер всё равно забудет, или понадеется, что "а вдруг поле на самом деле необязательное", если нет желания его заполнять.

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

Не надо call to action оформлять как ошибку и всё будет хорошо.

Не надо призыв к действию: "это поле заполнять обязательно" оформлять как ошибку и всё будет хорошо.

Если честно я всё ещё ничего не понимаю. Какое действие? Какой призыв? Что такое оформлять как ошибку? Как оформлять по-другому? И откуда вообще мысль что вы, как разработчик, будете решать что и как оформлять как ошибку? :)


upd. Раз 20 прочитал и кажется понял. Речь про то что "поле обязательно для заполнения" с вашей точки зрения это не ошибка, а некая просьба к юзеру и её надо показывать как-то иначе?


Нууу… такая точка зрения имеет право на существование. Но мало относится к теме. Так почти никто не делает (или никто). Стандартный привычный UX трактует такое как ошибки.


Меняем редактируемую модель на новую — форма сбрасывается автоматически.

Понятно. Пересоздаём всю форму? Ну как вариант, но как-то шибко ультимативно. Может убежать куда-нибудь скролл, закрыться какой-нибудь spoiler, может ещё какое-нибудь не-value состояние отвалится.

Мы же говорили про идеальный UX, а не "привычный" говно-UX, который везде видим.

Нет, пересоздаём модель, форма остаётся прежней. Впрочем, порой лучше и всю фрму пересоздать, чтобы сросить скроллы и прочие спойлеры автоматически, а не вручну.

Не сильно относится к вашему ответу и вообще всей теме, но захотелось сказать, что практически все популярные библиотеки для работы с формами прибиты к реактовским компонентам и из-за этого с ними очень-очень сложно работать, как только начнет действительно сложная, но довольно типовая логика к ним добавляться. В Ангуляре вон из коробки предлагают примитивы для работы с контролами, которые в себе содержат всю эту isValid, isTouched, isWhatever, reset и т.д. функциональность, но они с вьюхой соединяются через прослойку без какой-либо логики, т.е. вьюха просто перерисовывается, когда value и всякие состояния меняются, а всю реальную логику работы с формой можно вынести в отдельный слой и писать вообще (практически) не думая о компонентах. Кажется, что было бы здорово во фронтовых сообществах продвигать идею того, что работа с формами очень удобна, когда это отдельный слой, никак не привязанный к рендерингу и жизненному циклу компонентов

В случае React, все решения что я рассматривал, работают по примерно такому принципу (возможно минимальные отклонения):


  • ты подключаешь какой-нибудь hook, или HoC, или сразу и то и другое
  • ты wrap-аешь все свои контролы каким-нибудь HoC-ом
  • весь state формы лежит "где-то там" и управляется где-то там
  • если тебе нужно тоже получить доступ к state или вручную его изменить, то тебе доступны для этого какие-нибудь специальные методы

По сути это всё имеет околонулевое отношение к React-way и тому, что мы (не все) в нём любим. Но взамен нам выдают все эти isPristine, isTouched, validation, asyncValidation и прочие normal form flow компоненты.


Мне в какой-то момент настолько это всё осточертело, что я решил попробовать реализовать такое самостоятельно, но наиболее дубовым способом. Главным было сделать, чтобы state у формы лежал в моём коде и был максимально привычным (скажем const [login, setLogin] = useState(''). А form-либа должна уже используя эти значения\setter-ы добавлять желанную мне функциональность.


В принципе всё получилось. Особенно учитывая, что требования к либе у меня были простые (без асинх. валидаторов, без динамически меняющегося кол-ва полей, плоская структура и пр.). Но если это всё сделать используя именно React-way, то там получается целая куча лишних rerender-ов. Потому что нужны реакции, а они делаются в effect-ах, а они перезапускаются только при render-е. Меня такая производительность устроила, но стало понятно, что если нужно более промышленное сложное решение, то придётся мудрить.


В целом ваша идея, что прибитость form-flow к компонентам тут является большим злом, мне кажется, полностью верна. И надо сразу от этого отказываться, вероятно, делая почти всю чёрную работу за пределами React. При этом сам state формы оставить в React (тогда мы сохраняем привычный flow работы с данными). Пожалуй, столкнись я ещё раз с такой задачей, я так и сделаю.

Я только начинаю учить этот стек, и вот статья без примеров кода вызывает вопросы.

onChange вызывает обработчик события с аргументом value, который и установит в стейт value и isValid

вот как ф-ия onChange установит в стейт что-либо? Прокидывать в неё ф-ии setState аргументами?

Или так

onChange = (value) => { 
  setValue(value);
  setIsValid(validate(value));
}

что, вроде, тоже антипаттерн, каждый рендер реакт будет создавать новую функцию.

Именно так (как вы написали в коде). Насчёт создания функции при каждом рендере — кушайте слона по частям. В крайнем случае обернёте в useCallback.

Сразу же чтобы не наступать на грабли и не плеваться от реакта — используйте реакт только как view, а все логику и работу с данными прогоняйте через MobX. Это единственный вариант чтобы с реактом можно было работать по человечески.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории