Redux

В процессе создания всё более сложных и крупных приложений в React начинаешь понимать, что управление общим состоянием всего приложения невозможно только при помощи класса React.Component, использующего конструктор constructor() с вызовом setState(). Нам нужен контейнер состояний, такой как Redux, чтобы можно было запустить его в разных средах, централизовав состояние/логику приложения, и легко проводить отладку.

Итак, вот некоторые особенности Redux, с которыми вы, наверняка, уже сталкивались, но не совсем понимали, что и как они делают… и, конечно же, примеры.

“Посмотрим, как эти части согласуются друг с другом”.

Мы рассмотрим 15 малоизвестных особенностей Redux:

  1. Ключевые принципы Redux.
  2. Компромиссы при использовании Redux вместо Flux.
  3. Различие между mapStateToProps и mapDispatchToProps.
  4. Диспетчеризация действия в редукторе.
  5. Диспетчеризация действия при загрузке.
  6. Сброс состояния в Redux.
  7. Использование символа @в функции декораторе connect.
  8. Различие между React context и React-Redux.
  9. Создание AJAX-запроса в Redux.
  10. Лучший способ получить доступ в хранилище Redux.
  11. Зачем использовать константы? 
  12. Зачем использовать ownProps в mapStateToProps() и mapDispatchToProps()?
  13. Различие между call() и put() в Redux-Saga.
  14. Различие между Redux-Saga и Redux-Thunk.
  15. Установка начального состояния в Redux. 

1. Ключевые принципы Redux 

В основе работы Redux лежат 3 главных принципа:

1️⃣ Единственный источник истины

Это означает, что состояние всего приложения содержится в хранилище в виде дерева объектов.

Преимущества: 

  • Единственное дерево состояния облегчает процесс отладки и проверки приложения. 
  • Позволяет сохранять состояние приложения для ускорения процесса разработки. 
  • Облегчает создание универсальных и крупномасштабных приложений. 

Пример: 

// При выполнении:console.log(store.getState())

// В результате вы получите: 
{
  visibilityFilter: 'SHOW_ALL',
  todos: [
    {
      text: 'Consider using Redux',
      completed: true,
    },
    {
      text: 'Keep all state in a single tree',
      completed: false
    }
  ]
}

2️⃣ Состояние только для чтения 

Состояние можно изменить только при отправке действия

Так как все состояния централизованы и происходят последовательно друг за другом, то нет необходимости следить за состоянием гонки. 

Пример: 

// Способ отправки действий: 
store.dispatch({
  type: 'COMPLETE_TODO',
  index: 1
})

store.dispatch({
  type: 'SET_VISIBILITY_FILTER',
  filter: 'SHOW_COMPLETED'
})

3️⃣ Изменения выполняются чистыми функциями 

Вы пишите редукторы, чтобы указать на изменение состояния при помощи действий. 

Так как редукторы являются чистыми функциями, вы можете контролировать порядок их вызова, отправлять дополнительные данные или даже писать переиспользуемые редукторы.

Пример:

// Способ создания чистых функций/редукторов 
function visibilityFilter(state = 'SHOW_ALL', action) {
  switch (action.type) {
    case 'SET_VISIBILITY_FILTER':
      return action.filter
    default:
      return state
  }
}

function todos(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return [
        ...state,
        {
          text: action.text,
          completed: false
        }
      ]
    case 'COMPLETE_TODO':
      return state.map((todo, index) => {
        if (index === action.index) {
          return Object.assign({}, todo, {
            completed: true
          })
        }
        return todo
      })
    default:
      return state
  }
}

import { combineReducers, createStore } from 'redux'
const reducer = combineReducers({ visibilityFilter, todos })
const store = createStore(reducer)

2. Преимущества Flux перед Redux 

Flux — это шаблон, Redux — библиотека. При использовании Redux вам придется пойти на некоторые компромиссы:

  1. Придется избегать мутаций. Этогоможно добиться при помощи пакетов redux-immutable-state-invariant и Immutable.js. 
  2. Тщательно выбирать пакеты. УRedux есть точки расширения, такие как мидлвары и расширители хранилища, содержащие обширную экосистему пакетов. 
  3. Отсутствует удобная интеграция с Flow. 

3. Различие между mapStateToProps() и mapDispatchToProps() 

> mapStateToProps() 

Это утилита, которая помогает вашему компоненту обновлять состояние (которое обновляется некоторыми другими компонентами). Эта функция передается в качестве первого аргумента в connect и впоследствии будет вызываться каждый раз при изменении состояния хранилища Redux. 

Пример:

// TodoList.js
function mapStateToProps(state) {
  const { todos } = state
  return { todoList: todos.allIds }
}

export default connect(mapStateToProps)(TodoList)

> mapDispatchToProps() 

Это утилита, которая помогает вашему компоненту запускать событие действия (отправка действия, которое может вызвать изменение состояния приложения). Компонент получает [dispatch](https://react-redux.js.org/api/connect#dispatch) по умолчанию.

Пример:

import { addTodo, deleteTodo, toggleTodo } from './actionCreators'

const mapDispatchToProps = {
  addTodo,
  deleteTodo,
  toggleTodo
}

export default connect(
  null,
  mapDispatchToProps
)(TodoApp)

4. Диспетчеризация действия в редукторе

Причиной для внесения этого пункта в мой список стал следующий популярный вопрос со Stackoverflow: “Можно ли отправить действие в самом редукторе?” Отвечая на этот вопрос, скажу лишь одно: 

Отправка действия в редуктор — это анти-шаблон. 

5. Диспетчеризация действия при загрузке 

Вы можете отправить действие во время загрузки с помощью метода componentDidMount(), проверяя данные в методе render().

Пример:

class App extends Component {
  componentDidMount() {
    this.props.fetchData()
  }

  render() {
    return this.props.isLoaded
      ? <div>{'Loaded'}</div>
      : <div>{'Not Loaded'}</div>
  }
}

const mapStateToProps = (state) => ({
  isLoaded: state.isLoaded
})

const mapDispatchToProps = { fetchData }
export default connect(mapStateToProps, mapDispatchToProps)(App)

6. Сброс состояния в Redux 

Лучший способ — использовать исходный редуктор и передать действие редуктору, созданному при помощи combineReducers()

Пример:

const appReducer = combineReducers({
  // Здесь размещаются редукторы высшего уровня.
})

const rootReducer = (state, action) => {
  if (action.type === 'USER_LOGOUT') {
    state = undefined
  }

  return appReducer(state, action)
}

7. Использование символа @ в функции декораторе connect 

В JavaScript символ @ используетсядляобозначения декораторов, при помощи которых выдобавляете или видоизменяете классы и свойства

8. Различие между React context и React-Redux 

> React context 

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

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

> React-Redux 

React-Redux — это официальная библиотека, которая предоставляет привязки React для Redux. Она позволяет компонентам React считывать данные из хранилища Redux и отправлять туда действия для обновления данных. 

9. Создание AJAX-запроса в Redux 

AJAX позволяет отправлять асинхронные HTTP-запросы для передачи и извлечения данных с сервера. 

Чтобы выполнить вызов AJAX в Redux, можно использовать мидлвар redux-thunk для определения асинхронных действий. 

Пример:

export function fetchAccount(id) {
  return dispatch => {
    dispatch(setLoadingAccountState()) // Отображение спиннера загрузки
    fetch(`/account/${id}`, (response) => {
      dispatch(doneFetchingAccount()) // Скрытие спиннера загрузки
      if (response.status === 200) {
        dispatch(setAccount(response.json)) // Использование обычной функции для установки полученного состояния
      } else {
        dispatch(someError)
      }
    })
  }
}

function setAccount(data) {
 return { type: 'SET_Account', data: data }
}

10. Лучший способ получить доступ в хранилище Redux

Лучший способ — использовать функцию connect() с применением шаблона функций высшего порядка. Это позволяет отобразить креаторы состояния и действия в компонент и автоматически передать их, когда обновится хранилище

Пример:

// Вы можете передать контекст как опцию в функцию connect 
export default connect(
  mapState,
  mapDispatch,
  null,
  { context: MyContext }
)(MyComponent)

// или, как обычно, вызвать функцию connect, чтобы начать   
const ConnectedComponent = connect(
  mapState,
  mapDispatch
)(MyComponent)

// Затем передайте настроенный контекст как свойство присоединенному компоненту 
<ConnectedComponent context={MyContext} />

11. Зачем использовать константы? 

Использование констант позволяет легко находить все случаи применения конкретной выполняемой функции в проекте. Кроме того, они помогут избежать большого количества ошибок ReferenceError.

Пример:

// В constants.js
export const ADD_TODO = 'ADD_TODO'
export const DELETE_TODO = 'DELETE_TODO'
export const EDIT_TODO = 'EDIT_TODO'

// В actions.js
import { ADD_TODO } from './actionTypes';

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

// В reducer.js
import { ADD_TODO } from './actionTypes'

export default (state = [], action) => {
  switch (action.type) {
    case ADD_TODO:
      return [
        ...state,
        {
          text: action.text,
          completed: false
        }
      ];
    default:
      return state
  }
}

12. Зачем использовать ownProps в mapStateToProps() и mapDispatchToProps()

ownProps является необязательным параметром, который мы добавляем в mapStateToProps() или mapDispatchToProps()в качестве второго аргумента. Используйте его в том случае, если вашему компоненту нужны данные из его собственных свойств для извлечения данных из хранилища. 

Пример:

// Todo.js
function mapStateToProps(state, ownProps) {
  const { visibilityFilter } = state
  const { id } = ownProps
  const todo = getTodoById(state, id)

  // компонент получает дополнительно: 
  return { todo, visibilityFilter }
}

// Затем в вашем приложении родительский компонент отображает: 
<ConnectedTodo id={123} />
// и ваш компонент получает props.id, props.todo и props.visibilityFilter

Согласно документации: 

Вам необязательно включать значения из ownProps в объект, возвращаемый из mapStateToProps. connect автоматически объединит разные источники свойств в конечный набор. 

13. Различие между call() и put() в Redux-Saga 

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

> call() 

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

> put() 

Что же касается функции put(), то она создает эффект, который дает мидлвару команду отправить действие в хранилище. 

function* fetchUserSaga(action) {
  // Функция `call` получает оставшиеся аргументы, которые передаются в функцию `api.fetchUser`.  
  // Команда мидлвару вызвать промис, его разрешенное значение присваивается переменной `userData`    
  const userData = yield call(api.fetchUser, action.userId)

  // Команда мидлвару отправить соответствующее действие. 
  yield put({
    type: 'FETCH_USER_SUCCESS',
    userData
  })
}

Таким образом, call() и put() являются функциями креаторов эффектов. 

14. Различие между Redux-Saga и Redux-Thunk 

Мидлвар Redux Thunk позволяет написать креатор действия, который возвращает функцию вместо действия. Thunk можно использовать для задержки отправки действия или для отправки только при выполнении определенного условия.

  • Thunk использует промисы, тогда как Saga — генераторы. 
  • Thunk прост в использовании, да и промисы хорошо знакомы. Saga/Генераторы более мощные, но требуют изучения. 

15. Установка начального состояния в Redux 

Существуют 2 способа: 

1.Использовать метод createStore, который принимает в качестве второго аргумента необязательное значение preloadedState.

Пример:

const rootReducer = combineReducers({
  todos: todos,
  visibilityFilter: visibilityFilter
})

const initialState = {
  todos: [{ id: 123, name: 'example', completed: false }]
}

const store = createStore(
  rootReducer,
  initialState
)

2.Использовать явную проверку внутри редуктора. 

Пример:

function myReducer(state = someDefaultValue, action)

Надеюсь, я понятно объяснил эти 15 особенностей Redux. Интересно, а знали ли вы о них?

Читайте также:


Перевод статьи Vaibhav Khulbe: Demystifying lesser-known Redux terms and features (With examples)