Как Flowtype может спасти вашу жизнь

Одним из самых популярных примеров использования Flowtype с Redux является приложение F8, исходный код которого был открыт Facebook в прошлом году.

Он глубоко интегрируется с Flow, в частности:

  • Он заменяет константы на единый тип объединения Action, который описывает тип и полезную нагрузку каждого действия, которое может быть отправлено,
  • Позже в редукторах набранный Action помогает разработчикам понять цель действия и то, что можно сделать с его полезной нагрузкой,
  • У каждой ветви государства есть соответствующий тип, который помогает его трансформировать.

В последнее время я использую аналогичный подход в большинстве своих приложений. Однако я быстро заметил, что некоторые из них плохо масштабируются по мере роста приложения, особенно когда над приложением работают несколько разработчиков. Настройка, которой я хочу поделиться с вами сегодня, является результатом настроек и улучшений, которые мы со временем добавили в исходный F8 App подход.

Надеюсь, вы найдете это полезным!

# 1 - Храните типы в одном файле

Наши кодовые базы имеют тенденцию быстро расти по мере добавления новых редукторов, действий и селекторов. Распределение типов по нескольким файлам затрудняет их быстрый просмотр и импорт в компоненты. Он также создает множество перекрестных ссылок между файлами, когда вам нужно создать тип, который объединяет другие внутри.

В большинстве наших приложений мы стараемся хранить все типы в одном файле. Обычно мы называем его types.js и помещаем в корневую папку. Типы, которые он содержит, называются просто экспортами, как показано ниже:

/**
 * @flow
 */
export type Dispatch = Function;

которые впоследствии можно очень легко импортировать:

import type { Dispatch } from '../types';

# 2 - Опишите свое состояние

Хотя мы продвигаем редукторы небольшими и атомарными, они все же содержат логику, необходимую для изменения состояния приложения в ответ на действие. Когда мы получаем детали из API, все становится непросто. Нам нужно описать начальное состояние и отсутствие значения, поскольку запрос находится на рассмотрении.

Возьмем следующий пример состояния двух редукторов:

type Friend = {
  name: string,
};
type FriendsState = {
  list: Array<Friend>,
  loading: boolean,
};
type AppState = {
  isMenuOpen: boolean,
};

Он имеет типы потока для редукторов friends.js и app.js. Все хорошо - теперь мы можем использовать их внутри наших редукторов, как показано ниже:

/**
 * @flow
 */
import type { FriendState } from '../types';
type State = FriendsState;
const initialState = {
  loading: false,
  list: null,
};
export function friends(
   state: State = initialState,
   action: Object
): State {
   return state;
}

Вы действительно заметили, что в приведенном выше коде есть ошибка типа?

src/reducers/friends.js:9
 9: list: null,
 ^^^ null. This type is incompatible with
 2: list: Array<{ name: string }>,
 ^^^^^^^^^^^^^^^^^^^^^ array type. See: src/types.js:2

Мы объявили, что состояние friends.list in всегда будет массивом, либо пустым, либо содержащим некоторые извлеченные детали. Однако в нашем initialState значение по умолчанию - null. Это потенциально может вызвать исключение, так как это изменяет контракт, который был заключен в начале.

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

# 3 - Подключайтесь как профессионал

При работе с Redux мы используем connect для сопоставления состояния свойствам наших контейнеров и позволяем им реагировать на изменения, когда они происходят.

Простым примером может быть:

export default connect(
  (state) => ({
    list: state.friends.list,
  })
)(Container);

Сколько раз при написании чего-то подобного приходилось отступать, проверять редукторы и видеть форму состояния? Думаю, многое.

Теперь, поскольку каждый из наших редукторов имеет свой собственный тип, например FriendsState, как мы видели в предыдущем абзаце, мы можем легко ввести еще один, называемый State:

type State = {
  friends: FriendsState,
  app: AppState,
};

и используем его в нашем подключении:

/**
 * @flow
 */
import { connect } from 'react-redux';
import type { State } from '../types';
export default connect(
  (state: State) => ({
    list: state.friends.list,
  })
)(Container);

Этот шаблон помогает нам анализировать состояние всего приложения, а также устранять общие проблемы, такие как неправильное написание имен свойств. Благодаря этому мы можем поймать их на раннем этапе, когда мы печатаем, вместо того, чтобы отлаживать их во время выполнения.

# 4 - Будьте осторожны при вводе текста

Следуя подходу F8 app, мы описываем наши действия как единый тип объединения, называемый Action:

type Action =
 | { key: 'LOAD_FRIENDS' }
 | { key: 'LOAD_FRIENDS_SUCCESS', payload: { list: Array<Object> } }

Однако тип объединения, содержащий информацию обо всех действиях в приведенной выше форме, перестал работать для нас по мере роста приложения.

Основная проблема заключалась в том, что мы использовали redux-promise-middleware для автоматической отправки успешных и неудачных случаев, когда обещание было либо выполнено, либо отклонено.

На самом деле мы хотели, чтобы каждое действие, особенно то, которое является результатом вызова API, было полностью типизировано, чтобы было легче сопоставить тело ответа HTTP-запроса с состоянием. В то же время мы не хотели повторять такие вещи, как key и payload каждый раз, поскольку фактически меняется только тело ответа.

Итак, вместо того, чтобы сосредоточиться на вводе действий, мы решили создать один общий тип с именем ApiAction:

type ApiAction<T> = {
  key: string,
  payload: T,
};

которые можно использовать внутри редукторов для сопоставления действия с полезной нагрузкой.

Благодаря этому мы могли сосредоточиться на вводе полезной нагрузки, как в этом примере:

type Friends = Array<Friend>

Примечание. У нас уже есть типы Friend и Friends, которые мы используем внутри FriendsState, поэтому для той же безопасности требуется меньше печатать!

Наконец, реализовать его в нашем редукторе очень просто:

/**
 * @flow
 */
import type {
  Friends,
  Friend,
  ApiAction,
  Handler,
  FriendsState,
} from '../types';
const initialState = {
  loading: false,
  list: null,
};
const handlers: Handler<FriendsState> = {
  ['LOAD_FRIENDS_SUCCESS'](state, action: ApiAction<Friends>) {
    // here, action is typed with response
    return s;
  },
};
export createReducer(initialState, handlers);

В этом примере мы определяем обработчики как объект, где каждый ключ - это тип действия, которое нужно обработать, а функция - это преобразование, которое будет применено к состоянию. Его можно описать следующим типом потока:

type Handler<T> = {
  [key: string]: (state: T, action: Action<*>): T
};

Если вас интересует реализация createReducer, проверьте суть!

# 5 - Использовать константы или нет

Последнее важное понятие - константы. Я пока не уделял им слишком много внимания, в основном потому, что они не являются основным узким местом внутри приложения по мере его роста.

Мы всегда фокусировались на том, чтобы разработчику было так же легко работать с полезной нагрузкой действия внутри reducer, а не на проверке правильности ключа действия.

Сказав это, до этого момента мы, как правило, определяли константы в отдельном файле по старинке, однако иногда мы определяем перечисление, называемое ActionType, которое может использоваться во всех вышеупомянутых типах. в качестве замены string:

type ActionType = 
  | 'LOAD_FRIENDS'
  | 'LOAD_FRIENDS_SUCCESS'
  | 'LOAD_FRIENDS_FAILURE';

Flow предупредит нас в случае орфографической ошибки.

Подведение итогов

Когда дело доходит до ввода Redux с помощью Flow, определенно очень весело. Количество возможных решений, особенно с действиями, константами и редукторами, открывает пространство для множества обсуждений и улучшений. Как всегда в этом случае, выберите решение, которое работает для вас и дает вам наибольшие преимущества в том самом проекте, над которым вы работаете. Я надеюсь, что советы, которыми я поделился сегодня, дадут вам еще одну точку зрения на использование Flow с Redux!

Мир, Майк.

Спасибо Max Stoiber и Nader Dabit за то, что они были понятны не только мне.