Skip to content

Instantly share code, notes, and snippets.

@Akiyamka
Last active September 7, 2018 12:17
Show Gist options
  • Select an option

  • Save Akiyamka/4a779d8f368655ff08f21774148f99cf to your computer and use it in GitHub Desktop.

Select an option

Save Akiyamka/4a779d8f368655ff08f21774148f99cf to your computer and use it in GitHub Desktop.

Reducing Boilerplate

Упрощаем action creator:

Центральный элемент redux - action. Выглядит он примерно так:

{
  type: ADD_TODO,
  text: 'Build my first Redux app'
}

Как мы видим состоит он из:

  • action type: ADD_TODO
  • данных передваемых при диспатче: text: 'Build my first Redux app'

Чтобы его создать обычно используется action creator:

function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  }
}

Который использует action type

const ADD_TODO = 'ADD_TODO'

Наличие action type требутет FLUX, хранение его отдельной константой бережет нас от опечато. И это все очень здорово в крупных приложениях работает, но на начальных этапах это скорее нам мешает чем помогает.

Давайте для нашего небольшого приложения будем генерировать этот довольно незамысловатый код сами. Генерировать не в рантайме а еще на этапе сборки. Так мы в любой момент может отключить генераторы и оказатся с стандарным "ручным" redux. Так же условимся, что action type всегда будет называтся точно так же как action creator. Так их проще будет найти.

function addTodo(text) {
  return text
}

Это намного короче. Пока мы ничего не поменяли в логике, просто сделали нудную работу в автоматическом режиме. Вызвать action creator по прежнему можно с помощью

dispatch(addTodo(text))

Упрощаем асинхронные action creator (thunk):

Часто наши экшены асинхронны. Чтобы это было возможно используется Redux Thunk middleware благодоря чему мы можем из экшена возвращать не только обьекты action

return {
    type: 'addTodo',
    text
}
// Это то что возвращается экшеном после нашего генератора, сами же мы возвращаем просто text как примере выше

но и асинхронные функции

return fetch(`https://www.reddit.com/r/${subreddit}.json`)
  .then(
    response => response.json(),
    error => console.log('An error occurred.', error)
  )
  .then(json =>
    dispatch(receivePosts(subreddit, json))
  )

В таком классическом варианте появляется некоторая путаница.

  • Во первых часть методов синхронная часть асинхронная. Тяжело разобратся в где синхронные а где асинхронные action creator, а где синхронные по их названиям requestPost, fetchPost, receivePost. Понять можно только заглянув внутрь. По этой причине многие команды договариваются ставить особые префиксы в названиях и сваято блюсти codestyle или вовсе хранить их отдельно.

  • При этом надо не забывать что асинхронный вызов должен закончится синхронным (если конечно нам не все равно что ответил бекенд). Есть внегласное правило называть асинхронные методы с особым префиксом, или хранить их в отдельном файле.

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

  • при этом 99% случаев в одном асинхронном запросе испольуется максимум 3 синхронных:

    • Запрос отправлен
    • Запрос завершен
    • Произошла ошибка

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

Так поччему бы не генерировать все три состояния для каждого асинхронного запроса, так же как мы генерируем action creator. По принципу - если метод вернул Promise - нам нужно создать три action:

  • methodNameREQUEST(timestamp запроса)
  • methodNameERROR(обьект ошибки)
  • methodName(ответ бекенда)

Если метод вернул НЕ промис, а что-то иное, мы понимаем это обычный action и делаем обычный dispatch() этого action

  • methodName(данные)

В итоге, теперь нам не нужно раз за разом писать этих три одинаковых диспатча, мы можем генерировать одинаковый код автоматически. А это значит что вместо:

export function fetchPosts(subreddit) {
  return function (dispatch) {
    dispatch(requestPosts(subreddit))
    return fetch(`https://www.reddit.com/r/${subreddit}.json`)
      .then(
        response => response.json(),
        error => dispatch(errorInPosts(subreddit, error))
      )
      .then(json =>
        dispatch(receivePosts(subreddit, json))
      )
  }
}

Мы можем писать:

export function fetchPosts(subreddit) {
  return fetch(`https://www.reddit.com/r/${subreddit}.json`)
}

Из чего сгененируется код вида: (еще не реализовано)

export function asyncfetchPosts(subreddit) {
  return function (dispatch) {
    dispatch(fetchPostsREQUEST(subreddit))
    return fetch(`https://www.reddit.com/r/${subreddit}.json`)
      .then(
        response => response.json(),
        error => dispatch(fetchPostsERROR(subreddit, error))
      )
      .then(json =>
        dispatch(fetchPosts(subreddit, json))
      )
  }
}

Или если мы напишем:

export function fetchPosts(subreddit) {
  return { data: 'foo' }
}

Cгененируется код вида:

export function syncfetchPosts(subreddit) {
  dispatch(fetchPosts(subreddit))
}

Кода стало намного меньше, как и путаницы с синхронными / асинхронными методами.

Критика

Пожалуйста, покритикуйте мою идею, предложите неучтенные мной кейсы здесь, мы что-нибудь обязательно придумаем!

  • У меня синхронный метод, а я хочу сгенерировать ошибку и словить ее в редюсере
    • Просто заверните ответ в промис: return Promise.reject(new Error('упс'));
@wegorich
Copy link

wegorich commented Sep 7, 2018

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

.then(
        response => response.json(),
        error => dispatch(fetchPostsERROR(subreddit, error))
      )
      .then(json =>
        dispatch(fetchPosts(subreddit, json))
      )

Не совсем понятно зачем два респонса

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment