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

Введение

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

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 в таких случаях генерировались автоматически, не правда ли?

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

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

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

Исходный код

Комментарии (9)


  1. hf35
    21.01.2022 16:20
    +9

    Вместо того чтобы чинить дорогу - мы придумываем вездеход


  1. markelov69
    21.01.2022 18:14
    +5

    Мда, тяжелый случай ребята, о MobX мы разумеется не знаем и не слышали. Ну реально уже не смешно, 2022год на дворе.


    1. Ynhito Автор
      22.01.2022 14:28

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


      1. markelov69
        22.01.2022 14:52
        +3

        и нет либо желания, либо возможности отказываться от Redux.

        Нет желания? Что же это тогда за разработчик такой? Меняйте профессию.


        Нет возможности? Возможность есть всегда. Если в рамках текущего проекта принуждают использовать мертворожденный redux — нафиг такой проект, это всё равно что писать на Delphi по сей день. Все ведь легко и просто. На рынке просто тонны предложений где уже есть проекты с mobx'ом или на крайняк Vue.js / React Native + MobX.


        1. Ynhito Автор
          22.01.2022 15:10
          +1

          Нет возможности? Возможность есть всегда.... Все ведь легко и просто.

          Данные слова отлично показывают ваш опыт и экспертизу. Мне ясна ваша позиция.

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


          1. markelov69
            22.01.2022 15:17
            +4

            А эксперимент в рамках одной технлогии.

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


            1. amakhrov
              24.01.2022 02:03

              Зачем этот "эксперимент" выкладывать сюда?

              А я вот всегда полагал, что Хабр замечательно подходит для выкладывания всяких экспериментов.

              (простите, просто мимо проходил)


  1. dark_gf
    23.01.2022 20:47

    В своё время я писал такое же что и вы в этой статье (упростить использование редакса) https://github.com/dark0gf/redux-slicer и ипользовал на реальных проектах. Но быстро понял что это скользкая дорожка. Тогда еще мало кто слышал о redux-toolkit. А теперь подумайте о такой логике: в начале мы добавляем в проект редакс, потом понимаем что им не удобно пользоватся и добавляем thunk/toolkit/saga и т.д. (https://redux.js.org/introduction/ecosystem только посмотрите сколько овна кода написали, простите если кого то обидел), потом оказывается что и тут нам чего то не хватает и мы начинаем придумывать свои костыли поверх всего этого. Ладно если это проект с циклом жизни пару лет, а бывают случаю когда идея выстреливает и требуется быстрый рост, приходят новые программисты и начинают офигевать от этого зоопарка.

    Может стоит просто выпилить редакс и не мучаться?


  1. laisto
    24.01.2022 09:33

    недавно написал универсальный редюсер, использовал в одной задаче (на80+ разнородных переменных) и перенес в новый проект. собственно, уже где-то отписывал, что хочу написать статью, но карма не позволяет. под тем сообщением еще -10 с копейками. на самом деле писать редюсеры под каждый чих - порнография.