Redux - это крошечная библиотека Javascript, которая управляет состоянием вашего приложения более последовательным и практичным способом.

Считайте состояние репозиторием в памяти, в котором хранятся данные из базы данных, API, локального кеша, элемента пользовательского интерфейса на экране, такого как поле формы, и т. Д.

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

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

Redux - это более простая реализация паттерна Flux, архитектуры, которую Facebook использует для создания своих веб-приложений и мобильных приложений.

В нем реализованы основные концепции функционального программирования, вдохновленные Elm.

Он отлично работает с современными библиотеками и фреймворками, такими как React, Vue, Angular и многими другими.

Как это работает

Мы храним все в простом объекте JavaScript:

{
  todos: [{
    text: 'Eat food',
    completed: true
  }, {
    text: 'Exercise',
    completed: false
  }]
}

Действия

Чтобы внести изменения, мы должны отправить Action. Это также простые объекты JavaScript:

{ type: 'ADD_TODO', text: 'Go to swimming pool' }
{ type: 'TOGGLE_TODO', index: 1 }

Создатели действий

Обычно мы создаем функции, возвращающие эти объекты. Мы называем их создателями действий:

function addTodo(text = '') {
  return { type: 'ADD_TODO', text }
}
function toggleTodo(index = 0) {
  return { type: 'TOGGLE_TODO', index }
}

Редукторы

Для изменения состояния мы используем простые функции. Они возвращают новое состояние в соответствии с отправленным действием. Мы называем эти функции редукторами:

function todos(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return state.concat([{ text: action.text, completed: false }])
    case 'TOGGLE_TODO':
      return state.map(
        (todo, index) =>
          action.index === index
            ? { text: todo.text, completed: !todo.completed }
            : todo
      )
    default:
      return state
  }
}

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

function todoApp(state = {}, action) {
  return {
    todos: todos(state.todos, action),
    files: files(state.files, action),
    folders: folders(state.folders, action),
    notifications: notifications(state.notifications, action),
    filter: filter(state.filter, action),
    searchField: searchField(state.searchField, action),
    ...
  }
}

Магазин

Чтобы использовать Redux, вы должны создать магазин. Этот объект управляет всем за вас. Инициализируем его корневым редуктором:

import { createStore } from 'redux'
import todoApp from './reducers'
// create the store
let store = createStore(todoApp)
// read the state object
store.getState()
// dispatch actions that eventually modify the state
store.dispatch(addTodo('Learn about actions'))
store.dispatch(toggleTodo(0))

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

// log state to the console every time it gets updated
let unsubscribe = store.subscribe(() =>
  console.log(store.getState())
)
// stop listening to state updates
unsubscribe()

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

Знаменитые три принципа

Когда вы думаете о Redux, вы должны думать о следующем:

Единый источник истины. Состояние всего вашего приложения хранится в дереве объектов в едином хранилище.

Нет нескольких хранилищ, как в других библиотеках потоков. Это упрощает тестирование, отладку. Такие вещи, как путешествия во времени, универсальные приложения становятся реальностью.

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

Никто не пишет напрямую в государство. Никогда! Мы всегда используем действия. Они лучше нас умеют видоизменить состояние. И мы им доверяем. Всегда!

Изменения вносятся с помощью чистых функций: никогда не изменяйте состояние напрямую, всегда возвращайте новый объект.

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

Каждый редуктор должен возвращать новый объект, а не изменять существующий. Это более простой способ иметь неизменяемые данные в вашем приложении.

Как использовать Redux с React

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

Установить 👩‍💻

Существует пакет под названием react-redux, который обрабатывает привязку за вас:

npm install --save react-redux

Предоставить 🤗

Прежде всего вы должны использовать Provider:

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import App from './components/App'
import store from './pathToStore'

render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

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

Подключить 👪

Первый шаг - решить, какой компонент вы хотите связать с Redux. В React есть два вида компонентов:

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

Компоненты контейнера описывают, как все работает. Они просто передают состояние с помощью реквизита. Они подписываются на Redux.

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

Чтобы связать компонент React с Redux, используйте HOF connect():

import { connect } from 'react-redux'
import TodoList from './TodoList'
const TodoListContainer = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

export default TodoListContainer

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

Он также оптимизирует производительность, поэтому вам вообще не нужно использовать shouldComponentUpdate() в компонентах React.

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

const mapStateToProps = state => {
  return {
    todos: state.todos
  }
}

Второй отображает диспетчерские функции на реквизиты как функции обратного вызова:

const mapDispatchToProps = dispatch => {
  return {
    onTodoClick: id => {
      dispatch(toggleTodo(id))
    }
  }
}

В нашем примере компонент <TodoList> примет следующие реквизиты:

TodoList.propTypes = {
  onTodoClick: PropTypes.func.isRequired,
  todos: PropTypes.object.isRequired
}

Наслаждайтесь 🍾

Вы успешно соединили свой магазин с вашим представлением. Теперь помните:

  1. Каждый раз, когда что-то происходит, вы вызываете действие
  2. Редукторы решают, какие мутации они должны применить к своему состоянию.
  3. Ваши контейнеры получают обновленное состояние и передают данные в презентационные компоненты.

Асинхронный Redux

Redux по умолчанию синхронный. Есть несколько способов добавить асинхронное поведение.

Асинхронные действия

Каждое асинхронное поведение обычно имеет 3 различных типа действий:

  • FETCH_POSTS_REQUEST: Запрос начался. Вы можете сохранить в своем состоянии флаг isFetching и, если он верен, показывать индикатор загрузки.
  • FETCH_POSTS_SUCCESS: запрос успешно завершен. Поменяйте флаг isFetching на false, удалите загрузчик и покажите посты.
  • FETCH_POSTS_FAILURE: запрос не выполнен. Измените флаг isFetching на false, сохраните и отобразите ошибку на экране.

Создатели асинхронных действий

Самая простая реализация - использовать преобразователи . Вы должны установить и интегрировать промежуточное ПО под названием redux-thunk. Вот как это работает:

export const REQUEST_POSTS = 'REQUEST_POSTS'
function requestPosts(subreddit) {
  return {
    type: REQUEST_POSTS,
    subreddit
  }
}

export const RECEIVE_POSTS = 'RECEIVE_POSTS'
function receivePosts(subreddit, json) {
  return {
    type: RECEIVE_POSTS,
    subreddit,
    posts: json.data.children.map(child => child.data),
    receivedAt: Date.now()
  }
}

// This is a thunk action creator!
export function fetchPosts(subreddit) {
  return function (dispatch) {
    // dispatch the request
    dispatch(requestPosts(subreddit))

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

Мы отправим fetchPosts(), как и раньше. Он немедленно отправит requestPosts(), а затем receivePosts(), когда получит данные.

ПО промежуточного слоя

Промежуточное ПО - это код, который можно поместить между платформой, получающей запрос, и платформой, генерирующей ответ.

В Redux они предоставляют стороннюю точку расширения между отправкой действия и моментом его достижения редуктором.

Люди используют промежуточное ПО Redux для ведения журналов, отчетов о сбоях, взаимодействия с асинхронным API, маршрутизации и многого другого.

Вот как можно применить промежуточное ПО в действии:

import thunkMiddleware from 'redux-thunk'
import { createLogger } from 'redux-logger'
import { createStore, applyMiddleware } from 'redux'
import { selectSubreddit, fetchPosts } from './actions'
import rootReducer from './reducers'

const loggerMiddleware = createLogger()

const store = createStore(
  rootReducer,
  applyMiddleware(
    thunkMiddleware, // lets us dispatch() functions
    loggerMiddleware // neat middleware that logs actions
  )
)

В этом примере мы добавили регистратор, который регистрирует каждое действие в консоли. Мы также добавили redux-thunk, как описано выше.

DevTools

Redux имеет замечательный набор инструментов разработчика, которые улучшат ваш опыт отладки. Расширения доступны для Chrome и Firefox.

Чтобы включить эту функцию, вы должны установить пакет redux-devtools-extension. Это просто еще одно промежуточное программное обеспечение, которое вы должны применить в своем магазине.

Просто проверьте страницу и откройте вкладку Redux. Вы можете воспроизвести каждое действие, пропустить действия, проанализировать мутации состояний или даже экспортировать автоматизированные модульные тесты!

Тестирование

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

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

import * as actions from '../../actions/TodoActions'
import * as types from '../../constants/ActionTypes'

describe('actions', () => {
  it('should create an action to add a todo', () => {
    const text = 'Finish docs'
    const expectedAction = {
      type: types.ADD_TODO,
      text
    }
    expect(actions.addTodo(text)).toEqual(expectedAction)
  })
})

Когда дело доходит до редуктора, вы должны тестировать его поведение в каждом отдельном действии с двумя объектами, один для до и один после:

describe('todos reducer', () => {
  it('should return the initial state', () => {
    expect(reducer(undefined, {})).toEqual([
      {
        text: 'Use Redux',
        completed: false,
        id: 0
      }
    ])
  })

  it('should handle ADD_TODO', () => {
    expect(
      reducer([], {
        type: types.ADD_TODO,
        text: 'Run the tests'
      })
    ).toEqual([
      {
        text: 'Run the tests',
        completed: false,
        id: 0
      }
    ])

    expect(
      reducer(
        [
          {
            text: 'Use Redux',
            completed: false,
            id: 0
          }
        ],
        {
          type: types.ADD_TODO,
          text: 'Run the tests'
        }
      )
    ).toEqual([
      {
        text: 'Run the tests',
        completed: false,
        id: 1
      },
      {
        text: 'Use Redux',
        completed: false,
        id: 0
      }
    ])
  })
})

Советы и хитрости

  • Вместо Object.assign() вы можете использовать оператор распространения, чтобы упростить редукторы:
function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return { ...state, visibilityFilter: action.filter }
    default:
      return state
  }
}

Ресурсы

Цель этой статьи - дать вам более быстрый обзор библиотеки. Если вы решили использовать Redux, прочтите официальную документацию. Мы использовали большинство идей, примеров и определений непосредственно оттуда. 📖