Redux - это здорово, но для того, чтобы эффективно использовать redux в приложении React Native, вам нужно принять несколько решений. В этой статье я расскажу о некоторых библиотеках, которыми с удовольствием пользуюсь некоторое время. Предлагаемый мной стек поддерживает:

  • логирование
  • Асинхронные действия
  • Неизменяемое состояние (immutablejs)
  • Сохранение хранилища (асинхронное хранилище)

tl; dr; библиотеки, которые я буду использовать:

  • React-Redux
  • сокращение
  • redux-действие-буфер
  • редукс-действия
  • redux-immutablejs
  • редуктор-регистратор
  • Редукс-персистировать
  • redux-persist-неизменяемый
  • редукционный преобразователь

Мы начнем с более простых и перейдем к добавлению сложности в более крайних случаях.

логирование

Добавление журналирования в redux довольно стандартно и может быть легко выполнено с помощью промежуточного программного обеспечения.

// enhanders.js
import {
  applyMiddleware,
  compose
} from ‘redux’;
import createLoggerMiddleware from ‘redux-logger’;
let middlewares = [];
if (__DEV__ === true) {
  middlewares.push(createLoggerMiddleware({}));
}
export default compose(
  applyMiddleware(…middlewares)
);

Теперь каждый раз, когда вы запускаете свое собственное приложение, вы будете видеть состояние и действия в консоли:

Асинхронный

Для поддержки асинхронных потоков в redux, кажется, есть два очевидных варианта: redux-thunk и redux-sagas. Есть несколько очень интересных споров о том, что лучше в каких сценариях, но, как правило, я пришел к выводу, что, хотя redux-sagas могут быть более гибкими для сложных сценариев, библиотека, как правило, требует более высокой кривой обучения и заканчивается добавляя сложности к общему стеку. Так что, хотя я думаю, что оно того стоит для более крупных и сложных приложений, я бы попытался придерживаться более простого подхода, используя redux-thunk. В конце концов, оба решения приемлемы в большинстве сценариев, и почти всегда это дело вкуса. В этом случае я добавлю к смеси redux-thunk. Как и в случае с регистратором, это промежуточное ПО.

import {
  applyMiddleware,
  compose
} from ‘redux’;
import createLoggerMiddleware from ‘redux-logger’;
import thunkMiddleware from ‘redux-thunk’;
let middlewares = [ thunkMiddleware ];
if (__DEV__ === true) {
  middlewares.push(createLoggerMiddleware({}));
}
export default compose(
  applyMiddleware(…middlewares)
);

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

const init = () => ({ type: ‘APP_INIT’ })

Но также вот так:

const logout = (my_params) => (dispatch) => {
  dispatch({type: ‘APP_LOGOUT_STARTED’});
  // do your async stuff (ie: network calls)
  // in some callback, you can keep dispatching:
  dispatch({type: ‘APP_LOGOUT_ENDED’})
}

По сути, это функция, которая получает любые параметры, которые могут вам понадобиться, и возвращает функцию, которая получает в качестве параметра функцию отправки.

Было несколько интересных споров о том, что подход redux-thunk немного громоздок, и, безусловно, отправка нескольких действий для каждого создателя асинхронного действия может стать утомительным и подверженным ошибкам, если выполняется вручную. Чтобы избежать этого, вы можете использовать простой помощник, который принимает асинхронную функцию и соответствующим образом отправляет действия по мере выполнения функции. Я лично использовал следующую библиотеку (которая использует пакет redux-actions):

import { createAction } from 'redux-actions';
/**
 * Creates an async action creator
 *
 * @param  {String} TYPE            the type of the action
 * @param  {Function} executeAsync  the function to be called async
 * @return {Funtion}                the action creator
 */
export default function createAsyncAction(TYPE, executeAsync) {
const TYPE_STARTED = TYPE + '_STARTED';
    const TYPE_FAILED  = TYPE + '_FAILED';
    const TYPE_SUCCEED = TYPE + '_SUCCEED';
    const TYPE_ENDED   = TYPE + '_ENDED';
let actionCreators = {
        [ TYPE_STARTED ]: createAction(TYPE_STARTED),
        [ TYPE_FAILED  ]: createAction(TYPE_FAILED),
        [ TYPE_SUCCEED ]: createAction(TYPE_SUCCEED),
        [ TYPE_ENDED   ]: createAction(TYPE_ENDED)
    };
function create(...args) {
return async (dispatch, getState) => {
let result;
            let startedAt = (new Date()).getTime();
            dispatch(actionCreators[TYPE_STARTED]({ startedAt, ...args }));
            try {
                result = await executeAsync(...args, dispatch, getState);
                dispatch(actionCreators[TYPE_SUCCEED](result));
            }
            catch (error) {
                dispatch(actionCreators[TYPE_FAILED]({
                    errorMessage: error.message
                }));
                throw error;
            }
            let endedAt = (new Date()).getTime();
            dispatch(actionCreators[TYPE_ENDED]({
                endedAt: endedAt,
                elapsed: endedAt - startedAt
            }));
            return result;
        };
    }
Object.assign(create, {
        TYPE,
        STARTED: TYPE_STARTED,
        FAILED: TYPE_FAILED,
        SUCCEED: TYPE_SUCCEED
    });
return create;
}

Этот помощник можно использовать так:

createAsyncAction('CONFIG_ADD_CREDIT_CARD', async (userId, cc) => {
  let token = await stripe.createCreditCard(cc);
  await api.registerCreditCard(email, cc);
});

Это сгенерирует следующие действия: CONFIG_ADD_CREDIT_CARD_STARTED, CONFIG_ADD_CREDIT_CARD_FAILED, CONFIG_ADD_CREDIT_CARD_SUCCEED и CONFIG_ADD_CREDIT_CARD_ENDED соответственно.

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

middlewares.push(createLoggerMiddleware({
  predicate: (getState, action) => !action.type.endsWith('_ENDED')
})

Неизменяемое состояние (immutablejs)

При использовании redux вы не должны изменять состояние напрямую, а скорее генерируете новую копию состояния с новым (эммм…) состоянием. Существует множество примеров использования Object.assign и / или Array.slice(0) для создания новых копий, но есть более удобные способы сделать это. В этом случае я покажу вам, как использовать immutable.js для создания этих новых копий. Immutable.js - это пакет, поддерживаемый facebook и предоставляющий абстракцию словарей и массивов, которые нельзя изменять напрямую.

Для смешивания redux с immutable.js мы будем использовать redux-immutablejs. Этот пакет заботится о сантехнике, чтобы редукторы могли получить доступ к экземплярам immutable.js. Чтобы использовать его, вы просто используетеcreateReducer и combineReducers из redux-immutablejs вместо тех, которые упакованы с redux.

import { createReducer } from 'redux-immutablejs';
const reducer = createReducer(
  { loading: false},
  {
    'CONFIG_ADD_CREDIT_CARD_STARTED': (state) => state.merge({ loading: true })
    'CONFIG_ADD_CREDIT_CARD_ENDED': (state) => state.merge({ loading: false })
  }
);

Одно предостережение заключается в том, что при использовании connect redux вы получите экземпляр immutable.js, поэтому вам придется преобразовать его в простой объект javascript, прежде чем устанавливать значения для свойств:

export default connect(
    (state) => ({
      config: state.get('config').toJS() //NOTICE: .toJS() here
    }),
    (dispatch, props) => ({
      onAdd: () => dispatch(add())
    })
)(Home);

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

createLoggerMiddleware({
  stateTransformer(state) {
   return state.toJS()
  }
});

На этом этапе у вас есть все, что вам нужно для большинства сценариев. Я бы счел это минимальной настройкой для собственного приложения, использующего redux. Добавление reselect к миксу тоже определенно неплохая идея.

Сохранение хранилища (асинхронное хранилище)

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

  • Локальные настройки, предпочтения и т. Д.
  • Токены авторизации, ключи и т. Д.
  • Кэширование данных (ускорение загрузки и т. Д.)
  • Сохраняйте прогресс (не позволяйте пользователям вводить что-либо снова)
  • Автономная поддержка (аналогично кешированию)
  • так далее…

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

Существует популярный пакет для сохранения хранилища под названием redux-persist, однако этот пакет больше подходит для работы с простыми объектами javascript. Поскольку мы используем immutable.js, нам нужно добавить redux-persist-immutable в смесь.

Первое, что нам нужно сделать, это добавить усилитель редукции:

import {
  applyMiddleware,
  compose
} from 'redux';
import { autoRehydrate } from 'redux-persist-immutable';
// set middlewares...
let enhacers = [ autoRehydrate() ];
export default compose(
  applyMiddleware(...middlewares),
  ...enhacers
);

Во-вторых, нам нужно инициализировать redux-persist-immutablejs.

import { createStore } from 'redux';
// react-native
import { AsyncStorage } from 'react-native';
import { persistStore } from 'redux-persist-immutable'
const store = createStore(
    combinedReducers,
    initialState,
    composedEnhacers
);
//initialize redux-persist-immutable
persistStore(store, {storage: AsyncStorage});
export default store;

На данный момент у нас есть работающее постоянное хранилище. Но нам может понадобиться еще кое-что. Чтобы не переопределить текущее хранилище хранилищем, полученным из постоянного хранилища (помните, что это асинхронный в RN), вам придется либо отложить все вызовы диспетчеризации до тех пор, пока хранилище не будет извлечено, либо вы можете «буферизовать» все действия до тех пор, пока магазин получен. Несмотря на то, что альтернатива буфера кажется сложной (а это отчасти так), есть пакет, который делает именно это redux-action-buffer. Вы можете установить его как еще одно промежуточное ПО:

//...
import createActionBuffer from 'redux-action-buffer'
import {REHYDRATE} from 'redux-persist/constants'
let middlewares = [ thunkMiddleware, createActionBuffer(REHYDRATE) ];
export default compose(
  applyMiddleware(...middlewares),
  ...enhacers
);

В этом случае REHYDRATE - это специальное действие, отправляемое redux-persist, которое уведомляет, что хранилище было успешно получено и повторно гидратировано.

Заключение

Настройка проекта React Native, который использует redux с нуля, может показаться долгой и утомительной (и это так!), Но важно, чтобы вы понимали некоторые решения, принимаемые на каждом этапе. Вот почему, если вы решите использовать стартовый комплект, я настоятельно рекомендую вам убедиться, что он тщательно задокументирован и что вы понимаете природу используемых библиотек.