Это руководство является второй частью построения калькулятора заряда батареи Tesla с помощью React.

В части 1, после создания проекта с помощью приложения create-react-app, мы реализовали каждый компонент, разделив пользовательский интерфейс. Мы управляли состоянием и событием, используя локальное состояние и реквизиты, и завершили все приложение.

Если вы еще не читали, обязательно ознакомьтесь с частью 1, посвященной React, здесь:



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

Это последнее изображение нашего приложения во второй части:

🚀 Посмотрите живую демонстрацию части 2.

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

1. Какую проблему мы решаем?

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

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

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

  • компоненты разделяют состояние
  • состояние должно быть доступно из любого места
  • компоненты должны изменить состояние
  • компоненты должны изменять состояние других компонентов

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

Другими словами, с Redux у нас есть одно место, где мы можем ссылаться на состояние, изменять состояние и получать обновленное состояние.

Redux был написан с учетом React, но он также не зависит от фреймворка и даже может использоваться с приложениями Angular или jQuery.

Я рекомендую вам прочитать Дэна Абрамова Вам может не понадобиться Redux, прежде чем выбирать Redux.

2. Поток данных в Redux

Как вы видели в части 1, в React данные передаются через компонент с помощью свойств. Это называется однонаправленным потоком данных, который передается от родителя к потомку.

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

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

Согласно React docs:

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

Вот здесь и пригодится Redux.

Redux предоставляет решение для управления всем состоянием приложения в едином месте, называемом store.

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

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

Одним словом, Redux - это контейнер состояния, который представляет и управляет состоянием приложения как одного объекта из приложения на основе JavaScript.

3. Концепция Redux Core

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

Этот объект такой же, как и модель без сеттеров.

Чтобы изменить это состояние в Redux, вы должны отправить action.

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

Вот несколько примеров, которые скоро будут реализованы в нашем приложении.

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

Теперь нам нужна функция с именем reducer, чтобы связать эти состояния и действия вместе. Редуктор - это не что иное, как функция, которая принимает состояние и действие в качестве аргументов и возвращает новое состояние.

В мире:

(состояние, действие) = ›состояние

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

Вот один из примеров редуктора, который можно реализовать в нашем приложении:

4. Три принципа Redux

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

4.1 Единый источник истины

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

Поскольку все состояния существуют в одном месте, это называется single source of truth.

Этот one-store подход Redux является одним из основных отличий его от подхода Flux с несколькими хранилищами.

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

4.2 Состояние только для чтения

Единственный способ изменить состояние - это вызвать действие, описывающее произошедшее.

Другими словами, приложение не изменяет состояние напрямую, а вместо этого выражает намерение преобразовать состояние, передав действие.

Фактически, если вы посмотрите на Redux API, вы увидите, что он состоит всего из четырех методов:

store.dispatch(action)
store.subscribe(listener)
store.getState()
replaceReducer(nextReducer)

Как видите, метода setState () не существует. Следовательно, передача действия - единственный канал, который может изменить состояние приложения.

4.3 Изменения вносятся с помощью чистых функций

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

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

При тех же аргументах он должен вычислить следующее состояние и вернуть его. Без сюрпризов. Никаких побочных эффектов. Никаких вызовов API. Никаких мутаций. Просто расчет . - Redux Docs

Чистая функция имеет следующие характеристики:

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

5. Разделите приложение на контейнеры и компоненты.

Теперь давайте перестроим наше приложение-калькулятор Tesla, которое мы сделали в части 1, как версию Redux.

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

Размещение логики React и Redux внутри одного компонента может быть беспорядочным, поэтому рекомендуется создавать компонент Presentational только для целей презентации и компонент Container, компонент верхней оболочки, который обрабатывает Redux и отправляет действия.

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

6. Список состояний и действий для каждого компонента.

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

TeslaCar Container :
	state : wheels
	action : N/A
TeslaStats Container :
	state : carstats(array)
	action : N/A
	
TeslaSpeedCounter Container : 
	state : config.speed
	action : SPEED_UP, SPEED_DOWN
TeslaTempCounter Container : 
	state : config.temperature
	action : TEMPERATURE_UP, TEMPERATURE_DOWN
	
TeslaClimate Container : 
	state : config.climate
	action : CHANGE_CLIMATE
TeslaWheel Container : 
	state : config.wheel
	action : CHANGE_WHEEL

7. Настройте базу кода проекта для части 1.

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

После запуска npm убедимся, что приложение работает.

  • npm install

  • начало npm

8. Создавайте создателей действий для каждого действия.

Теперь, когда вы создали список действий, пора создать action creators.

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

Пример создания действия changeWheel:

const changeWheel = (value) => {
  return {
    type: 'CHANGE_WHEEL',
    value
  }
}

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

dispatch(changeWheel(size))

Доступ к функции диспетчеризации можно получить непосредственно из магазина через store.dispatch (), но в большинстве случаев доступ к ней можно получить с помощью вспомогательного средства, такого как connect() response-redux. Мы рассмотрим подключение позже.

8.1 Создание Action.js

Создайте индексный файл в каталоге src / actions и определите создателей действий следующим образом:

src / actions / index.js

import { counterDefaultVal } from '../constants/counterDefaultVal';
export const speedUp = (value) => {
  return {
    type: 'SPEED_UP',
    value,
    step: counterDefaultVal.speed.step,
    maxValue: counterDefaultVal.speed.max
  }
}
export const speedDown = (value) => {
  return {
    type: 'SPEED_DOWN',
    value,
    step: counterDefaultVal.speed.step,
    minValue: counterDefaultVal.speed.min
  }
}
export const temperatureUp = (value) => {
  return {
    type: 'TEMPERATURE_UP',
    value,
    step: counterDefaultVal.temperature.step,
    maxValue: counterDefaultVal.temperature.max
  }
}
export const temperatureDown = (value) => {
  return {
    type: 'TEMPERATURE_DOWN',
    value,
    step: counterDefaultVal.temperature.step,
    minValue: counterDefaultVal.temperature.min
  }
}
export const changeClimate = () => {
  return {
    type: 'CHANGE_CLIMATE'
  }
}
export const changeWheel = (value) => {
  return {
    type: 'CHANGE_WHEEL',
    value
  }
}
export const updateStats = () => {
  return {
    type: 'UPDATE_STATS'
  }
}

Поскольку нам нужны значения по умолчанию на основе создателя действия, мы определяем это постоянное значение в constants / counterDefaultVal в каталоге src и импортируем его.

src / constants / counterDefaultVal.js

export const counterDefaultVal = {
  speed: {
    title: "Speed",
    unit: "mph",
    step: 5,
    min: 45,
    max: 70
  },
  temperature: {
    title: "Outside Temperature",
    unit: "°",
    step: 10,
    min: -10,
    max: 40
  }
}

9. Создайте редукторы для каждого действия.

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

Здесь важно не изменять данное состояние напрямую. Редукторы должны быть чистыми функциями и должны возвращать новое состояние.

  • Функции-редукторы вызываются из контейнера, который создается при выполнении действия пользователя.
  • Когда Reducer возвращает состояние, Redux передает новое состояние каждому компоненту, а React визуализирует каждый компонент снова.

9.1 Неизменяемые структуры данных

  • Примитивный тип данных JavaScript (число, строка, логическое значение, undefined и null) = ›неизменяемый
  • Объект, массив и функция = ›изменяемый

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

Здесь есть три способа изменить состояние:

ES5

// Example One
state.foo = '123';
// Example Two
Object.assign(state, { foo: 123 });
// Example Three
var newState = Object.assign({}, state, { foo: 123 });

В приведенном выше примере первый и второй изменяют объект состояния. Второй пример изменен, поскольку Object.assign () объединяет все свои аргументы в первый аргумент.

Третий пример не изменяет состояние. Он объединяет содержимое состояния и {foo: 123} в новый пустой объект, который является первым аргументом.

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

ES6 (ES2015)

const newState = { ...state, foo: 123 };

Подробнее об операторе спреда см. Здесь.

9.2 Создание редуктора для ChangeClimate

Сначала мы создадим ChangeClimate с помощью метода разработки через тестирование.

В первой части наше приложение было создано с помощью create-response-app, поэтому мы в основном используем jest в качестве средства запуска тестов.

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

Files with .js suffix in __tests__ folders
Files with .test.js suffix
Files with .spec.js suffix

Создайте teslaRangeApp.spec.js в src / reducers и создайте тестовый пример.

describe('test reducer', () => {
  it('should handle initial stat', () => {
    expect(
      appReducer(undefined, {})
    ).toEqual(initialState)
  })
})

После создания теста запустите команду npm test. Вы должны увидеть следующее сообщение об ошибке теста. Это потому, что мы еще не написали appReducer.

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

src / redurs / teslaRangeApp.js

const initialState = {
  carstats:[
    {miles:246, model:"60"},
    {miles:250, model:"60D"},
    {miles:297, model:"75"},
    {miles:306, model:"75D"},
    {miles:336, model:"90D"},
    {miles:376, model:"P100D"}
  ],
  config: {
    speed: 55,
    temperature: 20,
    climate: true,
    wheels: 19
  }
}
function appReducer(state = initialState, action) {
  switch (action.type) {
    
    default:
      return state 
  }
}
export default appReducer;

Затем импортируйте teslaRangeApp.js из teslaRangeApp.spec.js и установите initialState.

src / redurs / teslaRangeApp.spec.js

import appReducer from './teslaRangeApp';
const initialState =  {
  carstats:[
    {miles:246, model:"60"},
    {miles:250, model:"60D"},
    {miles:297, model:"75"},
    {miles:306, model:"75D"},
    {miles:336, model:"90D"},
    {miles:376, model:"P100D"}
  ],
  config: {
    speed: 55,
    temperature: 20,
    climate: true,
    wheels: 19
  }
}
describe('test reducer', () => {
  it('should handle initial stat', () => {
    expect(
      appReducer(undefined, {})
    ).toEqual(initialState)
  })
})

Запустите тест npm еще раз, и тест будет успешным.

В текущем тестовом примере тип действия - {}, поэтому возвращается initialState.

Теперь давайте протестируем действие CHANGE_CLIMATE.

Добавьте в teslaRangeApp.spec.js следующие тестовые примеры climChangeState и CHANGE_CLIMATE.

const climateChangeState = {
  carstats:[
    {miles:267, model:"60"},
    {miles:273, model:"60D"},
    {miles:323, model:"75"},
    {miles:334, model:"75D"},
    {miles:366, model:"90D"},
    {miles:409, model:"P100D"}
  ],
  config: {
    speed: 55,
    temperature: 20,
    climate: false,
    wheels: 19
  }
}
it('should handle CHANGE_CLIMATE', () => {
    expect(
      appReducer(initialState,{
        type: 'CHANGE_CLIMATE'
      })
    ).toEqual(climateChangeState)
  })

Затем добавьте функции CHANGE_CLIMATE case, updateStats и calculateStats в teslaRangeApp.js. Затем импортируйте BatteryService.js, который использовался в части 1.

import { getModelData } from '../services/BatteryService';
function updateStats(state, newState) {
  return {
    ...state,
    config:newState.config,
    carstats:calculateStats(newState)
  }  
}
function calculateStats(state) {
  const models = ['60', '60D', '75', '75D', '90D', 'P100D'];
  const dataModels = getModelData();
  return models.map(model => {
    const { speed, temperature, climate, wheels } = state.config;
    const miles = dataModels[model][wheels][climate ? 'on' : 'off'].speed[speed][temperature];
    return {
      model,
      miles
    };
  });
}
function appReducer(state = initialState, action) {
  switch (action.type) {
    case 'CHANGE_CLIMATE': {
      const newState = {
        ...state,
        config: {
          climate: !state.config.climate,
          speed: state.config.speed,
          temperature: state.config.temperature,
          wheels: state.config.wheels
        }
      };
      return updateStats(state, newState);
    }
    default:
      return state
  }
}

Если вы проверите результаты теста, вы увидите, что оба тестовых примера выполнены успешно.

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

9.3 Создание Reducer для других

Если вы создадите остальные тестовые примеры, ссылаясь на вышеуказанный метод, вы, наконец, определите файл teslaRangeApp.js, в котором определены редукторы всех приложений, и teslaRangeApp.spec. js, чтобы протестировать их.

Окончательный код можно найти по адресу:

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

10. Взгляды: умные и глупые составляющие.

Как упоминалось в 5. Разделите приложение на контейнеры и компоненты, мы создадим презентационные компоненты (немые компоненты) для целей презентации и компоненты контейнера (интеллектуальные компоненты), которые являются оболочкой. компонент, отвечающий за действия при общении с Redux.

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

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

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

10.1 Привязка уровня представления

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

Технически компонент контейнера - это просто компонент React, который использует store.subscribe () для чтения части дерева состояний Redux и предоставления свойств визуализируемому компоненту представления.

Следовательно, мы можем вручную создавать компоненты контейнера, но это не рекомендуется для официальных документов Redux. Это связано с тем, что react-redux выполняет множество оптимизаций производительности, которые сложно выполнить вручную.

По этой причине вместо того, чтобы писать компонент-контейнер вручную, мы пишем его с помощью функции connect(), предоставляемой react-redux.

Давайте сначала установим необходимые пакеты.

  • npm install –save redux
  • npm install –save react-redux

10.2 Контейнер TeslarCar

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

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

После определения функции mapStateToProps мы определили функцию connect (), как показано ниже.

const TeslaCarContainer = connect(mapStateToProps, null)(TeslaCar)

connect () принимает mapDispatchToProps в качестве второго аргумента, который принимает метод диспетчеризации магазина в качестве первого аргумента. В компоненте TeslaCar нам не нужно действие, поэтому мы должны передать null.

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

Создайте src / container / TeslaCarContainer.js и напишите код.

Ознакомьтесь с TeslaCarContainer.js

10.3 Контейнер TeslaStats

Как и в случае с контейнером TeslaCar, определите только функцию mapStatToProps и передайте ее функции connect () в контейнере TeslaStats.

Создайте src / container / TeslaStatsContainer.js и напишите код.

Ознакомьтесь с TeslaStatsContainer.js

10.4 Контейнер TeslaSpeedCounter

Контейнер TeslaSpeedCounter определяет дополнительную mapDispatchToProps функцию для обработки действий пользователя, которые происходят в компоненте TeslaSpeedCounter.

Создайте src / container / TeslaSpeedCounterContainer.js и напишите код.

Ознакомьтесь с TeslaSpeedCounterContainer.js

10.5 Контейнер TeslaTempCounter

Контейнер TeslaTempCounter почти идентичен TeslaSpeedCounter, за исключением того, что передаются создатели состояния и действий.

Создайте src / container / TeslaTempCounterContainer.js и напишите код.

Ознакомьтесь с TeslaTempCounterContainer.js

10.6 TeslaClimateContainer

Создайте src / container / TeslaClimateContainer.js и напишите код.

Ознакомьтесь с TeslaClimateContainer

10.7 TeslaWheelsContainer

Создайте src / container / TeslaWheelsContainer.js и напишите код.

Ознакомьтесь с TeslaWheelsContainer.js

Мы создали компоненты контейнера, соответствующие компонентам презентации, сгенерированным в части 1 через connect () из react-redux.

11. Провайдер

Давайте объединим все, что мы сделали, и заставим наши приложения работать. До сих пор мы определили объекты действий и создали создателей действий, которые создают объекты действий. И когда происходит действие, мы создали редукторы, которые фактически обрабатывают и возвращают новое состояние. Затем мы создали компонент-контейнер, который соединяет каждый из компонентов презентации с хранилищем Redux.

Теперь каждому компоненту контейнера нужен способ доступа к хранилищу, что и делает Provider. Компонент Provider обертывает все приложение и позволяет подкомпонентам связываться с магазином через connect ().

Компонент верхнего уровня нашего приложения, App.js, выглядит так:

Ознакомьтесь с App.js

12. Как все они работают вместе

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

Теперь запустите npm start, и он будет нормально скомпилирован, и приложение будет запущено.

Но есть еще кое-что, что нужно сделать.

  • Сначала скопируйте все содержимое /containers/TeslaBattery.css, которое вы создали в части 1, и добавьте их в App.css.

Ознакомьтесь с App.css

  • Затем откройте /components/TeslaCounter/TeslaCounter.js и измените обработчик событий onClick следующим образом: это связано с тем, что часть 2 больше не обрабатывает события в TeslaBattery. js.
onClick={(e) => props.increment(e, props.initValues.title)}
-->
onClick={(e) => {
  e.preventDefault();
  props.increment(props.currentValue)}}
onClick={(e) => props.decrement(e, props.initValues.title)}
-->
onClick={(e) => {
  e.preventDefault();
  props.decrement(props.currentValue)}}
  • Далее, давайте не будем многократно использовать реквизиты, используя деструктуризацию объектов ES6.
const TeslaCounter = (props) => (
  <p className="tesla-counter__title">{props.initValues.title}</p>
  ...
-->
const TeslaCounter = ({ initValues, currentValue, increment, decrement }) => (
  <p className="tesla-counter__title">{initValues.title}</p>
  ...

Посетите TeslaCounter.js

Наконец, наша версия приложения Tesla Battery Range Calculator для Redux готова!

13. Еще кое-что: Redux Dev Tools.

Redux Dev Tool значительно упрощает просмотр отслеживания состояния Redux и позволяет использовать полезные функции, такие как отладка путешествия во времени.

Мы установим его в Chrome здесь.

Откройте файл App.js и измените часть createStore следующим образом:

const store = createStore(appReducer);
-->
const store = createStore(appReducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
  • Проверить в браузере

Прежде чем перейти к следующей части:

Дополнительные ресурсы: