Это руководство является второй частью построения калькулятора заряда батареи 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' } }
- Посмотрите index.js
Поскольку нам нужны значения по умолчанию на основе создателя действия, мы определяем это постоянное значение в 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
}
}
- Посмотрите counterDefaultVal.js
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. или Просмотр.
- Посмотрите teslaRangeApp.js в том виде, в каком мы его уже писали.
- Ознакомьтесь с teslaRangeApp.spec.js
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 здесь.
- Расширение Chrome установить
- Добавить для Redux store
Откройте файл App.js и измените часть createStore следующим образом:
const store = createStore(appReducer);
-->
const store = createStore(appReducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
- Проверить в браузере
Прежде чем перейти к следующей части:
- Ознакомьтесь с окончательным кодом проекта
- Посмотрите живую демонстрацию