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

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

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

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

Введение

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

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+ уровней вложенности - признак плохой нормализации данных. Но всё же было бы неплохо переписать это на рекурсию.

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

Исходный код

Источник: https://habr.com/ru/post/647049/


Интересные статьи

Интересные статьи

На работе я занимаюсь поддержкой пользователей и обслуживанием коробочной версии CRM Битрикс24, в том числе и написанием бизнес-процессов. Нужно отметить, что на самом деле я не «чист...
Я 18 лет в IT. Последние 10 из них руковожу: под моим подчинением в разное время были 200 человек.  Интересно, что я помню каждого, кто из них уволился и по какой причине. Помню не потому, что...
На сегодняшний день у сервиса «Битрикс24» нет сотен гигабит трафика, нет огромного парка серверов (хотя и существующих, конечно, немало). Но для многих клиентов он является основным инструментом ...
Эта статья посвящена одному из способов сделать в 1с-Битрикс форму в всплывающем окне. Достоинства метода: - можно использовать любые формы 1с-Битрикс, которые выводятся компонентом. Например, добавле...
Сегодня мы поговорим о перспективах становления Битрикс-разработчика и об этапах этого пути. Статья не претендует на абсолютную истину, но даёт жизненные ориентиры.