«День с̶у̶р̶к̶а̶ Redux» — как бороться с рутиной, применяя автоматизацию

Kate

Administrator
Команда форума
«Это худший день в вашей жизни. Может быть, пережить его снова?»

Введение​

"Ух-ты! Какая интересная задача! И оценка времени на разработку хорошая! ..."

2 часа спустя: "Какой же это ужас, ещё 10 редьюсеров создать, ещё 10 раз описать зависимости состояний. Типы, компоненты... Сколько же бесполезной рутины... Вот бы можно было писать только декларативную логику, всегда."


Если вам хоть отчасти близок текст выше, не переживайте, вы не одни такие. Я - человек который не один раз произнес сказаное выше.

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

Хочу отметить, что эта статья нацелена в основном на разработчиков, у которых основной стек React + 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 в таких случаях генерировались автоматически, не правда ли?

Сказано - сделано, с полной типизацией:

Пример автоматически-сгенерированных actions
Пример автоматически-сгенерированных actions
А что если нам хочется всё таки описать свою логику? В этом нет проблемы, потому что API RTK остался тем же, плюс ts-автодополнение для автоматически-сгенерированных reducers также имеется

Пример переопределения сгенерированных reducers
Пример переопределения сгенерированных 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)

Пример использования useManager
Пример использования useManager
Теперь мы не задумываемся о том, что нужно что-то диспатчить и откуда тянуть данные. Внутри createSliceManager мы описали состояния - и просто используем их.

Проверка работы useManager
Проверка работы useManager

Зависимости состояний и 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 всего приложения?

Пример определения watchers
Пример определения watchers
Давайте проверим. Для начала определим 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 &nbsp;
<b>
{JSON.stringify(modals.create.open)}
</b>
</p>
Проверка работы watchers
Проверка работы watchers
Как можно видеть, alert появился только после изменения toolbar.date или toolbar.resourceType.

Ещё есть интересный момент, если в зависимостях в watchers указать просто 'toolbar', то alert не покажется.

Причиной тому - подписка на изменение конкретной сущности, имя которой мы указали.

Например, если бы мы вызвали так, то у нас бы как раз изменился весь объект toolbar, и зависимость бы отработала.

changeToolbar({
date: '2022-01-01',
resourceType: 'newValue',
})
Проверка вызова handler от определенных fields внутри watchers
Проверка вызова handler от определенных fields внутри watchers

Заключение​

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

  • npm-пакет
  • unit-тесты для всех функций, которые учавствуют в генерации методов
  • небольшая документация, описывающая все основные моменты
  • желание упрощать и делать Developer Experience ещё лучше :D
Из возможных проблем пока что имеется только одна - типизация вложенных ключей возможна только на 9 уровней вниз. И реализованна с помощью перегрузки типов, а не рекурсии. Лично я считаю, что хранить в redux состояние на 9+ уровней вложенности - признак плохой нормализации данных. Но всё же было бы неплохо переписать это на рекурсию.

Пока что это можно считать идеей, которая требует внимания и критики для того, чтобы она смогла жить, или умереть. Буду рад любой обратной связи!

Исходный код
 
Сверху