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

Пользователь

Отправить сообщение
2. Необязательность означала бы перегружение компонента SplitPane логикой
4. Эту логику отображения пришлось бы писать заново для каждого компонента, принимающего пропсы.

Эти проблемы можно решить просто введя еще одну обертку:
function SafeDiv({children, ...props}) {
    return (
        children ?
            <div {...props}>
                {children}
            </div>
        :
            null
    );
}

Теперь можно смело писать:
function SplitPane(props) {
    return (
        <div className="SplitPane">
            <SafeDiv className="SplitPane-left">
                {props.left}
            </SafeDiv>
            <SafeDiv className="SplitPane-right">
                {props.right}
            </SafeDiv>
        </div>
    );
}

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

библиотека этому никак не мешает

С таким подходом можно писать на голом JS, ибо он тем более не мешает сделать все что хочешь. Обычно от библиотеки мы хотим чтобы она решала наши проблемы, а не просто не мешала делать это самостоятельно.

Как иначе-то?

Уменьшать число зависимостей, в том числе явно запрещая изменять "чужие" данные напрямую, а только через интерфейс. Т.е. я ожидаю увидеть что-то типа такого:

const createBearSlice: StoreSlice<IBearSlice, IFishSlice> = (set, get) => ({
  bearFull: false,
  eatFish: () => {
    // fishes недоступно и менять нельзя, можно только вызвать метод:
    fishSlice.fishDie();
    // а свои поля менять можно:
    set((prev) => ({ bearFull: true }));
  },
  sayHello: () => {
    // получать "чужие" поля можно, но указав источник:
    console.log(`hello ${get().fishSlice.fishes} fishes`);
  },
});

const createFishSlice: StoreSlice<IFishSlice> = (set, get) => ({
  fishes: 10,
  // интерфейсный метод для снижения поголовья рыб:
  fishDie: () =>
    set((prev) => ({ fishes: prev.fishes > 1 ? prev.fishes - 1 : 0 })),
});

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

Посмотрим на код из примера:

const createBearSlice: StoreSlice<IBearSlice, IFishSlice> = (set, get) => ({
  eatFish: () =>
    set((prev) => ({ fishes: prev.fishes > 1 ? prev.fishes - 1 : 0 })),
  sayHello: () => {
    console.log(`hello ${get().fishes} fishes`);
  },
});

const createFishSlice: StoreSlice<IFishSlice> = (set, get) => ({
  fishes: 10,
});

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

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

В общем, это все напоминает древнее процедурное программирование. Есть общий набор глобальных переменных, есть процедуры, которые как-то их меняют. Все остальное исключительно на совести программистов. От чего уходили, к тому и вернулись :)

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

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

Как предлагается решать данную проблему?

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

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

const App = ({loadingState, initializeData}) => {
    useEffect(() => {initializeData()}, []);

    return (
        loadingState === 'loading' ?
            <Spinner/>
        : loadingState === 'loaded' ?
            <Layout/>
        :
            <Forbidden/>
    );
};

export default connect(
    state => ({loadingState: state.loadingState}),
    {initializeData}
)(App);


В данном случае initializeData — redux-thunk-овский экшн примерно следующего вида:

export const initializeData = () => async dispatch => {
    try {
        const {data} = await api.loadData();
        dispatch(setLoadingState('loaded'));
        dispatch(setData(data));
    } catch {
        dispatch(setLoadingState('forbidden'));
    }
};


Надеюсь, что помог :)
Всегда пожалуйста)

Вы можете писать dispatch(doAction(args)). Все будет работать. Экшены передаются в connect чтобы превратиться в пропсы — для единообразия и возможного переиспользования компонента. Т.е. в этом случае для компонента нет разницы, подключен он к редаксу или нет, он оперирует исключительно своими пропсами и локальным стейтом, если есть. А вся связь с редаксом выносится в connect.

И да — thunk-овские экшены с точки зрения вызова не отличаются от обычных — в этом как раз и заключается его удобство.
Connect магию не сделает — он просто передаст указанный срез store-а в props-ы компонента. В итоге при изменении стора изменятся пропсы и react отреагирует ререндером.

В случае обращения к getState напрямую — не будет изменения пропсов, не будет ререндера. Может быть, можно как-то выкрутиться через прокси, обсерверы, но это будет уже совсем не в концепции redux.
Стремиться надо к тому, чтобы код был прост, и с ним было легко работать. К бездумному следованию каргокульту стремиться не надо.

Несомненно. И понимание, где искать тот или иной кусок кода — существенно упрощает работу с проектом. Когда логика в компонентах, приходится бегать по ним туда-сюда, чтобы найти, где она именно находится. Уже упомянутый пример расчета. Что-то будет в кнопке. Что-то в отдельной функции — мы же не любим говнокод и вынесем большой кусок, ведь верно? Что-то будет в формах, которые эти данные выводят. А уж если появляются какие-то всплывающие окошки, на которых тоже есть формочки и кнопочки… В итоге один и тот же, логически связанный функционал расползается по разным файлам и очень сложно собрать все в голове воедино.

Отсюда и вытекает данная максима. Компоненты — только для отрисовки и обработки действий. Любая бизнес логика должна быть вынесена с самого начала, а не когда у вас появится желание и возможность провести рефакторинг.

Да например — вот если у меня ф-я внутри компонента или там импортирована из какого-то файла, то я жму ctrl+click и попадаю на ее реализацию.

Согласен, в данном случае пропсы несколько усложняют жизнь. Но если следовать описанному выше правилу, таких вызовов в компоненте у вас будет 2-3. Иначе вы уже что-то делаете не так. Поэтому достаточно посмотреть в конец файла, где connect, и ctrl-click-нуть там.

Зато вы можете сразу видеть, с чем именно работает ваш компонент. Это все описано в connect. Вам не надо лазить по обработчикам чтобы увидеть, что же именно они там вызывают.

А еще любая ф-я может захватывать параметры из своего лексического контекста (ака замыкание).

Погодите. Если вам нужно замыкание — используйте замыкание. Оно прекрасно помогает сократить количество кода для вспомогательных компонентов. Вы же говорите не о нем. Вы, фактически, топите за использование глобальных объектов внутри функции.

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

Согласен, описанные мной сценарии довольно редки. Однако я не сказал бы, что как-то страдают типичные сценарии. Передача всех данных через пропсы (замыкания мы не берем) имеет определенные преимущества. Помимо перечисленных выше оно позволяет также их удобно типизировать.

перемещение локальной части стейта в стор — это элементарный рефакторинг

При чем здесь локальный стейт? Локальный стейт действительно, часто оправдан.

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

Вот с этого надо было начат, потому что я под эти понимаю совсем иное. Глупый компонент — который ничего не знает об окружающем мире, заточен под работу с пропсами и нацелен на переиспользование. Умный — понимает, зачем он здесь, в каком контексте будет использован. Общается с внешним миром и может выполнять дополнительные действия, не связанные с рендерингом (увы, это приходится делать).

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

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

Компонент — это слой представления. Все что он должен делать — рисовать UI и реагировать на действия пользователя. Так, конечно, не всегда получается, но к этому надо стремиться.

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

Можете обосновать? Как именно это усложняет?

Компонент должен получать через пропсы те и только те данные, которые он не может нормально получить по-другому.

Компонент — функция. Какой есть нормальный способ передачи данных в функцию? Ее параметры. В случае реакта это пропсы.

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

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

С каких пор введение дополнительных уровней абстракций и логического разделения по слоям затрудняет работу с кодом?

Но в целом, для начала стоит определиться, что мы понимаем под «умными» и «глупыми» компонентами.
А зачем, если можно просто написать...

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

Конечно, при грамотной архитектуре и четком разделении на «умные» и «глупые» компоненты можно действовать и таким образом, но в общем случае это приводит лишь к запутыванию.
Ну ж извините, ссылочку на проект я не кину. Могу написать название. Программный комплекс обработки документов, поступающих от налоговых органов в рамках Положения Банка России № 440-П для Альфа-банка. Конкретно — АРМ.

Вы требуете от меня потратить пару часов времени на то, чтобы вспомнить mobx, вспомнить или найти в гите какие возникали проблемы, оформить все на codesandbox.io… И ради чего?

Заметьте, я не говорю что mobx отстой или типа того. Я говорю что лично мне redux понравился больше. А крики «вам он не должен нравиться» — это уже какой-то детский сад.
Вокруг реакта есть куча старых и новых надстроек и библиотек и какие в каком виде применять для меня как для новичка во фронтенде — загадка

Увы, это так( Сложность освоения часто обратная сторона гибкости.

непонятно нужно ли искать описание конструкций вроде <...> на redux-toolkit.js.org или где-то еще.

Это redux-thunk. Просто в тулките он идет из коробки.

На мой взгляд, именно в thunk-овских action-ах надо концентрировать бизнес-логику, работу с АПИ и т.д. А из компонентов только их вызывать.

Вообще, складывается ощущение что будет не лишним запилить пост, где описать собственный опыт работы с redux.
Проект был года полтора назад, поэтому подробности уже забылись. Из того что помню — было сложно работать с мутабельными объектами. Если в redux я могу законнектить любое поле стейта, затем поменять сразу весь родительский объект и все будет работать, то с mobx постоянно приходилось следить, что можно менять, а из-за чего observer-ы поотваливаются.

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

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

const events = createSlice({
   name: 'events',
   initialState: {
      myEvent: null
   },
   reducers: {
      sendMyEvent: (state, {payload}) => {
         state.myEvent = payload;
      }
   }
});


Далее компоненты коннектят нужное событие, подписываясь на него и ставят в зависимость в useEffect, если надо.

Покажите, как вы начнете писать HOC, который передаст в компонент какие-то произвольные, нужные конкретному экземпляру компонента данные и коллбэки.
Или вы на каждый в-прошлом-редаксовский компонент пойдете писать свой HOC? Отлично. Или не совсем «свой», а связывающий некие поля в некоем хранилище с пропсами? Ну тогда вы свой редакс так напишете.

Ну, в какой-нибудь mobx это будет inject и observer.

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

Вообще, изначально разговор шел об redux-thunk. Вопрос стоял, как я понял, зачем его использовать, если можно прям из коллбэков вызывать dispatch, импортируя его из redux. И тут ответ — потому что таким образом мы или нагружаем компонент логикой или, по крайней мере, обязываем разбираться в специфике обработки коллбэков. Что повышает связность.
С концепцией middleware всё хорошо. Применение этой концепции там, где она слишком многословна, раздута, или же вообще полностью не нужна — вот это и есть «натягивание».

Замените слово middleware на любое другое — и ваше утверждение останется верным. Любая концепция хороша, когда совпадает с ожиданием и плоха, когда задачи приходится адаптировать под нее.

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

Эм… Никак?) Редакс управляет состоянием, отслеживание событий — это уже не к нему.

Если она вам нужна — это прекрасно. Если нет — вы написали гору разнесенного по разным частям приложения кода ни для чего.

Если вам не нужно централизованное управление состоянием — не используйте редакс. Используйте, например, хуки. А если надо его куда-то вынести — вы это и так сделаете.

Только каким-то API, очень похожим на connect().

Например, любым HOC.
Можете пояснить, в каком месте концепция middleware вообще и redux-thunk в частности — это натягивание совы на глобус?

На мой взгляд, это как раз грамотное разделение кода по слоям для уменьшения связности. Есть компонент:

const MyComponent = ({data, onUpdateData}) => (
   ...
);


Мы можем использовать его как отдельно, так и с редаксом. Во втором случае подключаем его:

connect(state => {
   data: state.data
}, {
   onUpdateData: updateData
})(MyComponent )


Если updateData — просто меняет состояние, делаем обычный экшн. Если нужен асинхронный код, описываем thunk-овский экшн:

const updateData = data => dispatch => {
  ...
};


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

На мой взгляд, это весьма удобно.
Удобно использовать Redux? — Смешно.
Удобно использовать redux-thunk? — Смешно в квадрате.

Поумерьте свой пыл. Говоря о целесообразности использования redux-thunk, мы предполагаем что redux уже есть в стеке. И да, я считаю что в правильных руках redux может быть удобен. Предвосхищая вопросы — mobx я пробовал, не понравилось.

Вот так и рождается говнопроекты, думать головой не хотим, просто смотрим как написано в примере и как рекомендуют «умные авторы».

Вот так и рождаются поделки с конфликтующим стеком из библиотек-однодневок, неоптимальным использованием ресурсов, запутанной структурой и кучей багов. Зато сделали не как в скучных примерах из документации.
Потому что удобно? Можно, конечно, импортировать getState и dispatch из redux, после чего дергать из обычных функций, но почему бы не пойти по рекомендованному пути middleware?
«Есть всего два типа языков программирования: те, на которые люди всё время ругаются, и те, которые никто не использует.»
Bjarne Stroustrup.

То же можно сказать и о библиотеках.
1

Информация

В рейтинге
Не участвует
Зарегистрирован
Активность