«Это худший день в вашей жизни. Может быть, пережить его снова?»
Введение
"Ух-ты! Какая интересная задача! И оценка времени на разработку хорошая! ..."
2 часа спустя: "Какой же это ужас, ещё 10 редьюсеров создать, ещё 10 раз описать зависимости состояний. Типы, компоненты... Сколько же бесполезной рутины... Вот бы можно было писать только декларативную логику, всегда."
Если вам хоть отчасти близок текст выше, не переживайте, вы не одни такие. Я - человек который не один раз произнес сказаное выше.
Поэтому сегодня я поделюсь своими мыслями о том, как в моих глазах можно многое упростить, чтобы наконец начать получать хороший Developer Experience.
Хочу отметить, что эта статья нацелена в основном на разработчиков, у которых основной стек React + Redux.
А также дополню, что сказанное далее не рекомендуется использовать для начала нового проекта, или же его пепеписывания. Мы берем за пример ситуацию, когда проект уже существует довольно долгое время, и отказываться от Redux нет веских причин. Я от себя скажу - что в таких случаях Redux я бы ни за что не выбрал, на текущий день.
Автоматизация создания State и его изменение
Первое о чем хотелось бы поговорить - потребность каждый раз создавать reducer, при добавлении новых свойств в state, для их изменения.
Да, казалось бы, это логично. Но есть ли в этом смысл, если нет никаких конвертеров для payload и изменений соседних свойств?
Пример ниже:
export type State = {
tableData: {
rows: any[],
pageCount: number,
},
modals: {
create: {open: boolean},
delete: {open: boolean},
},
toolbar: {
date: Date,
resourceType: string
}
}
const initialState: State = {
tableData: {
rows: [],
pageCount: 0,
},
modals: {
create: {open: false},
delete: {open: false},
},
toolbar: {
date: new Date(),
resourceType: 'base'
}
}
export const simpleSlice = createSlice({
name: 'simpleSlice',
initialState,
reducers: {
setTableData: (state, action) => {
state.tableData = action.payload;
},
setModalsCreateOpen: (state, action) => {
state.modals.create.open = action.payload;
},
setModalsDeleteOpen: (state, action) => {
state.modals.delete.open = action.payload;
},
setToolbarDate: (state, action) => {
state.toolbar.date = action.payload;
},
setToolbarResourceType: (state, action) => {
state.toolbar.resourceType = action.payload;
},
},
})
Хотелось бы чтобы reducers в таких случаях генерировались автоматически, не правда ли?
Сказано - сделано, с полной типизацией:
А что если нам хочется всё таки описать свою логику? В этом нет проблемы, потому что API RTK остался тем же, плюс ts-автодополнение для автоматически-сгенерированных reducers также имеется
Использование redux внутри UI компонентов
Давайте теперь перейдем к использованию. Как мы обычно используем данные из Redux и диспатчим ActionCreator?
Наверное, примерно так:
// c помощью useSelector подписываемся и получаем доступ к состояниям
const {tableData, toolbar, modals} = useSelector((state: Store) => state.demoManager)
// получаем экспемляр dispatch функции
const dispatch = useDispatch();
// создаем обработчики
// Если обращаться напрямую
const handleDateChange = (date: Date) => {
dispatch(demoManager.actions.changeToolbarDate(date))
}
// Если заранее сделать реекспорт
// export const {changeToolbarDate} = demoManager.actions
// import {changeToolbarDate}
const handleDateChange = (date: Date) => {
dispatch(changeToolbarDate(date))
}
И так каждый раз. Да, мы выносим отдельно селекторы. Да, мы можем выносить создание handlers в отдельный хук. Там же в этом хуке делать useSelector. Да, да, да... И всё это также - каждый раз
Что если и это автоматизировать? Давайте попробуем
На выходе имеем [state, handlers] = useManager<YourStateType>(yourManager)
Теперь мы не задумываемся о том, что нужно что-то диспатчить и откуда тянуть данные. Внутри createSliceManager мы описали состояния - и просто используем их.
Зависимости состояний и side-effects
Что мы обычно делаем, когда появляется потребность подписаться на изменение состояния, чтобы в следствии используя его сделать запрос к api?
Скорее всего диспатчим thunk внутри useEffectt, подписавшись на нужные состояния.
const dispatch = useDispatch();
const [{toolbar},{changeToolbarDate}] = useManager<State>(demoManager)
useEffect(() => {
// диспатчим thunk
dispatch(getTableData(toolbar))
// следим за состояниями в тулбаре
}, [toolbar.date, toolbar.resourceType])
Опять же, внутри UI-компонента начинаем задумываться о состояниях, зависимостях...
Отличным решением было бы вынести эту логику в отдельный хук, например:
const useTableData = ({date, resourceType}: ToolbarParams) => {
const dispatch = useDispatch();
useEffect(() => {
// диспатчим thunk
dispatch(getTableData(toolbar))
// следим за состояниями в тулбаре
}, [date, resourceType])
}
export const Component = () => {
const [{toolbar},{changeToolbarDate, changeToolbarResourceType}] = useManager<State>(demoManager)
useTableData(toolbar)
return (
<>
<input type="date" value={toolbar.date} onChange={(e) => {
changeToolbarDate(e.target.value)
}} />
<input value={toolbar.resourceType} onChange={(e) => {
changeToolbarResourceType(e.target.value)
}} />
</>
)
}
И хранить его в отдельной папке с подобными effect-request хуками.
А может быть и это можно упростить?
Что если прямо в момент создания состояний можно будет задать зависимости и иметь доступ к dispatch и getState всего приложения?
Давайте проверим. Для начала определим getTableData
. Она будет только вызывать alert с новыми значениями тулбара
const getTableData = (params: State['toolbar']) => (dispatch, getState) => {
alert(`toolbar params ${JSON.stringify(params)}`)
}
Также, для того, чтобы убедиться в том, что зависимости работают верно, добавим возможность изменить поле modal.create.open
<button onClick={() => changeModalsCreateOpen(true)}>change modal create</button>
<p>modal create open
<b>
{JSON.stringify(modals.create.open)}
</b>
</p>
Как можно видеть, alert появился только после изменения toolbar.date или toolbar.resourceType.
Ещё есть интересный момент, если в зависимостях в watchers указать просто 'toolbar'
, то alert не покажется.
Причиной тому - подписка на изменение конкретной сущности, имя которой мы указали.
Например, если бы мы вызвали так, то у нас бы как раз изменился весь объект toolbar, и зависимость бы отработала.
changeToolbar({
date: '2022-01-01',
resourceType: 'newValue',
})
Заключение
Хочу отметить, что вышеперечисленные наработки пока что не использовались в реальном приложении с реальными задачами. Всё писалось и проверялось пока-что лично мною. Что имеется на данный момент:
unit-тесты для всех функций, которые учавствуют в генерации методов
небольшая документация, описывающая все основные моменты
желание упрощать и делать Developer Experience ещё лучше :D
Из возможных проблем пока что имеется только одна - типизация вложенных ключей возможна только на 9 уровней вниз. И реализованна с помощью перегрузки типов, а не рекурсии. Лично я считаю, что хранить в redux состояние на 9+ уровней вложенности - признак плохой нормализации данных. Но всё же было бы неплохо переписать это на рекурсию.
Пока что это можно считать идеей, которая требует внимания и критики для того, чтобы она смогла жить, или умереть. Буду рад любой обратной связи!
Комментарии (9)
markelov69
21.01.2022 18:14+5Мда, тяжелый случай ребята, о MobX мы разумеется не знаем и не слышали. Ну реально уже не смешно, 2022год на дворе.
Ynhito Автор
22.01.2022 14:28Спасибо что подняли этот вопрос, обязательно дополню в статье что данный способ рассматривает не случай выбора инструмента для начала проекта, или его переписывания. А вариант, когда проект уже существует достаточно давно, и нет либо желания, либо возможности отказываться от Redux.
markelov69
22.01.2022 14:52+3и нет либо желания, либо возможности отказываться от Redux.
Нет желания? Что же это тогда за разработчик такой? Меняйте профессию.
Нет возможности? Возможность есть всегда. Если в рамках текущего проекта принуждают использовать мертворожденный redux — нафиг такой проект, это всё равно что писать на Delphi по сей день. Все ведь легко и просто. На рынке просто тонны предложений где уже есть проекты с mobx'ом или на крайняк Vue.js / React Native + MobX.
Ynhito Автор
22.01.2022 15:10+1Нет возможности? Возможность есть всегда.... Все ведь легко и просто.
Данные слова отлично показывают ваш опыт и экспертизу. Мне ясна ваша позиция.
Просто повторюсь, что это не статья не о том, что нужно использовать. А эксперимент в рамках одной технлогии.
markelov69
22.01.2022 15:17+4А эксперимент в рамках одной технлогии.
Зачем этот "эксперимент" выкладывать сюда? Ведь эта "технология" мертва и оказывает чрезвычайно пагубное воздействие на проекты. От перестановки слагаемых сумма не изменится. Просто очередной раз упоминание этого шлака создает иллюзию что до сих пор можно использовать redux на проектах и якобы все будет более менее ок. И у новичков формируется ошибочное мнение и они сходу начинают идти по кривой дорожке, вместо того, чтобы сразу встать на прямую.
amakhrov
24.01.2022 02:03Зачем этот "эксперимент" выкладывать сюда?
А я вот всегда полагал, что Хабр замечательно подходит для выкладывания всяких экспериментов.
(простите, просто мимо проходил)
dark_gf
23.01.2022 20:47В своё время я писал такое же что и вы в этой статье (упростить использование редакса) https://github.com/dark0gf/redux-slicer и ипользовал на реальных проектах. Но быстро понял что это скользкая дорожка. Тогда еще мало кто слышал о redux-toolkit. А теперь подумайте о такой логике: в начале мы добавляем в проект редакс, потом понимаем что им не удобно пользоватся и добавляем thunk/toolkit/saga и т.д. (https://redux.js.org/introduction/ecosystem только посмотрите сколько
овнакода написали, простите если кого то обидел), потом оказывается что и тут нам чего то не хватает и мы начинаем придумывать свои костыли поверх всего этого. Ладно если это проект с циклом жизни пару лет, а бывают случаю когда идея выстреливает и требуется быстрый рост, приходят новые программисты и начинают офигевать от этого зоопарка.Может стоит просто выпилить редакс и не мучаться?
laisto
24.01.2022 09:33недавно написал универсальный редюсер, использовал в одной задаче (на80+ разнородных переменных) и перенес в новый проект. собственно, уже где-то отписывал, что хочу написать статью, но карма не позволяет. под тем сообщением еще -10 с копейками. на самом деле писать редюсеры под каждый чих - порнография.
hf35
Вместо того чтобы чинить дорогу - мы придумываем вездеход