🙌 Больше никаких бурений!
👋 Эй! Сегодня я изучаю Redux. Я обнаружил, что лучший способ для меня учиться - это просто записывать это по мере того, как я это делаю. Это полезно для меня и, надеюсь, для вас тоже.
Благодарим за прочтение. Оставьте комментарий, если у вас есть предложения или исправления.
Что такое Redux?
Redux позиционирует себя как контейнер с предсказуемым состоянием для приложений JavaScript.
Не знаю, как вы, но для меня это ничего не значит. Я знаю, что это за состояние. Я знаю, что такое контейнер. Зачем мне нужна библиотека для хранения моего состояния? 🤷♂️ Понятия не имею. Эй, все говорят, что Redux - это круто, и я не хочу быть неудачником.
Так что это на самом деле означает? Что ж, когда мы думаем о том, как мы обычно управляем состоянием в React, вы, вероятно, делаете одно из следующих действий:
- «Детализация компонентов», при которой вы передаете объекты вниз вниз от родительского компонента к какому-то удаленному дочернему компоненту. Например, если вы хотите показать аватар пользователя, вы можете передать объект
user
вниз от вашегоApp
к вашемуPage
, к вашемуNavbar
, к вашемуUserMenu
, к вашемуAvatar
. Или что-то похуже. - «Free-for-all», где вы устали от этой пропасти, проходящей через ад, и каждый компонент управляет своим собственным состоянием, возможно, через какое-то промежуточное хранилище данных, которое вы создали. Здесь начинается смешение бизнес-логики и логики представления, и через некоторое время вы хотите буквально поджечь всю свою кодовую базу из-за созданного вами 💩.
- «Детский реквизит», довольно разумный промежуточный вариант, когда вы используете свойство
children
для передачи JSX и списков компонентов по всему приложению, от высокого к низкому, чтобы избежать чрезмерной детализации свойств.
Ни одно из этих решений не станет отличным, когда ваше приложение станет больше, чем «маленькое».
Прежде чем вы погрузитесь в
Поскольку я лично узнал о Redux и составил этот пост, следует упомянуть, что Redux требует некоторого обучения.
Помимо изучения того, как работает Redux и его концепции, вам, возможно, придется поработать по колено в шаблонах неизменяемого состояния, и вам, вероятно, придется добавить несколько дополнительных библиотек в микс, чтобы все это работало.
Я считаю, что это хорошее вложение в обучение, но не надо перерабатывать инженерные решения. :) Особенно для небольших проектов вы должны задаться вопросом, может ли Redux и его прямые и косвенные зависимости быть немного излишними.
Как Redux делает жизнь лучше?
💉 Впрыск, бурение больше не требуется
Redux оборачивается вокруг ваших компонентов и позволяет вам вводить свойства прямо в них из хранилища - вашего приложения, единого центрального репозитория данных, в котором хранится все состояние вашего приложения.
Если мы возьмем этот UserAvatar
в качестве примера, давайте посмотрим, как этот компонент будет выглядеть с Redux:
const mapStateToProps = state => ({ user: state.user }); const UserAvatar = connect(mapStateToProps)(({ user }) => ( <img src={user.avatar} alt={user.name}> ));
Волшебство происходит, когда мы используем connect
из Redux - это позволяет нам извлекать состояние из хранилища и извлекать нужные нам свойства без детализации.
😇 Чистые компоненты
Каждый раз, когда вы используете connect
, он автоматически делает эти компоненты чистыми: они повторно визуализируются только тогда, когда их состояние изменяется в хранилище Redux.
Это фактически означает, что у вас не будет ненужных повторных рендеров. Обычно вам нужно реализовывать shouldComponentUpdate
самостоятельно, чтобы эффективно управлять этим, но теперь вы получаете это «бесплатно» с Redux.
Довольно удобно иметь независимые, многоразовые и предсказуемые компоненты, которые не полагаются ни на что, кроме ввода состояния.
✅ Плюс еще несколько преимуществ
Наличие центрального хранилища данных, в котором размещается весь наш штат, имеет несколько преимуществ:
- Независимые компоненты чрезвычайно предсказуемы и легко тестируются.
- Централизовать состояние - это удобно: вы можете сохранить его, вы можете восстановить, вы можете управлять им и т. Д. - все в одном центральном месте.
- Это также упрощает совместное использование кода и рендеринг на стороне сервера: все, что вам нужно сделать, это поделиться состоянием, а клиент и сервер должны отображаться одинаково.
- Это значительно упрощает тестирование и отладку. Вы даже можете путешествовать во времени! Чтобы понять, насколько мощна эта концепция, ознакомьтесь с инструментами разработки Redux.
Redux: концепции, основы и проблемы
Давайте подумаем о составлении приложения со списком дел. Наше состояние будет состоять из списка задач, некоторые из которых могут быть выполнены. Пользователь может захотеть добавить, удалить и дополнить элементы, которые изменят состояние.
Как заставить это работать, что такое действия и редукторы и как они взаимодействуют с нашим магазином и состояние держится?
📕 Действия 101
Итак, как и следовало ожидать, действие может быть чем-то вроде «добавить задачу». В Redux каждое действие просто представлено как простой объект JavaScript:
{ type: "ADD_TODO", whatever: "add your own payload" }
Это свойство type
требуется для каждого действия Redux. Эти значения принято записывать в стиле UPPER_CASE
. Остальная часть объекта принадлежит вам, чтобы заполнить любую полезную нагрузку, которую вы хотите.
👉 Итак, как мы используем действия и как их запускать? Чтобы упростить задачу и обеспечить единообразное поведение всех элементов, вы обычно записываете каждое действие как отдельную функцию (известную как «создатель действия»), например:
function addTodo(text) { return { type: "ADD_TODO", text }; }
Фактической отправки там не происходит! Вместо этого из кода вашего компонента, в котором происходит фактический вызов, вы должны сделать что-то вроде этого:
store.dispatch(addTodo("My first todo!"));
Итак: мы отправляем действия, вызывая dispatch()
в нашем хранилище и передавая ему объект действия, который обычно исходит от создателя действия.
Теперь мы знаем, как отправлять действия, но на самом деле мы вообще не реализовали их логику. Здесь на помощь приходят r educers, так что давайте углубимся немного глубже.
📕 Антракт: три принципа
Теперь у нас есть некоторое представление о том, как данные проходят через приложение Redux, важно упомянуть три основных принципа, которых оно придерживается:
- 1️⃣ Есть один единственный источник истины:
Все данные о состоянии приложения поступают только из одного места: из вашего магазина. - 🔒 Состояние только для чтения:
Мы только читаем состояние и никогда не меняем его. Действия могут привести к новому состоянию. - 😇 Изменения вносятся с помощью чистых функций:
мы никогда не изменяем состояние напрямую, но редукторы могут возвращать новое состояние.
📕 Редукторы 101
По сути, редуктор - это просто функция, которая обрабатывает действие: она принимает два параметра: текущее состояние и действие. объект. Он может либо генерировать новое состояние, либо сохранять текущее состояние.
В нашем примере вот как может выглядеть редуктор, реализующий действие ADD_TODO
:
function todoApp(state, action) { switch (action.type) { case "ADD_TODO": return Object.assign({}, state, { todos: [ ...state.todos, { text: action.text, completed: false } ] }); default: return state; } }
Это простой пример. Если мы получаем новое задание, мы создаем новое состояние, добавляя в него это задание. Мы делаем это путем клонирования предыдущего состояния на новый объект вместо изменения существующего состояния.
Обратите внимание, как мы используем здесь Object.assign()
: передавая пустой объект {}
в качестве первого параметра, мы инициализируем новый объект и объединяем с ним предыдущее состояние с некоторыми изменениями. Это гарантирует, что мы не изменим предыдущий объект состояния, который является неизменным.
Вы также заметите, что если мы не распознаем тип действия, мы просто возвращаем текущее состояние и оставляем его как есть.
🤵 Краткое содержание: Хранилище хранит и контролирует состояние, редуктор может создавать новое состояние на основе объектов, описывающих действия.
✋ Подождите: гигантский оператор переключения с подробным кодом?
Если вы возьмете эти примеры и создадите их, у вас останется гигантский и со временем растущий оператор switch в вашем редукторе , который содержит каждую операцию, изменяющую государство. Легко понять, как это может выйти из-под контроля.
Более того, каждая отдельная операция, создающая новое состояние, требует написания довольно подробного кода, что не идеально:
// The code you'd want to write, ideally: state.todos.push(todo); state.todos[123].checked = true; // The "pure" code you end up writing in Redux reducers: return Object.assign({}, state, { todos: [...state.todos, todo] }); return { ...state, todos: { ...state.todos, 123: { ...state.todos[123], checked: true } } }; // ...many times over, in a giant switch statement.
Итак, как нам с этим справиться? В документации Redux есть хорошая страница под названием Уменьшение Boilerplate. Это страница, которая действительно усердно работает, чтобы убедить вас в том, что их выбор дизайна имеет смысл, а также предлагает некоторые решения.
Основные выводы для меня:
- Используйте константы для таких вещей, как типы действий (меньше текста; меньше ошибок).
- Создатели действий имеют смысл ради последовательности и надежности, но вы можете раскрыть свой творческий потенциал в том, как вы их пишете или генерируете.
- То же самое и с гигантским оператором switch: довольно легко написать небольшой вспомогательный код, который разбивает его на независимые функции.
- Вы можете разделить и объединить редукторы, чтобы упорядочить код.
- Промежуточное ПО, такое как redux-thunk, очень важно для выполнения асинхронных операций в редукторах (таких как обращение к API).
- Промежуточное ПО, такое как redux-act, упрощает написание создателей действий и редукторов и объединяет их вместе, что также помогает уменьшить количество шаблонов.
- Такие библиотеки, как Immer, НАМНОГО упрощают работу с неизменяемым состоянием - подробнее об этом позже.
Я упомяну некоторые из этих методов и библиотек позже, когда мы начнем собирать вещи вместе и улучшать их шаг за шагом. Приступим к коду!
🥪 Собираем все вместе
🌯 Магазин, Провайдер и упаковка вашего приложения
Класс Provider
охватывает ваше приложение, что позволяет базовым компонентам получать доступ к вашему хранилищу через connect
. Вот как это выглядит:
// src/index.js import React from "react"; import ReactDOM from "react-dom"; import { Provider } from "react-redux"; import App from "./app.js"; import store from "./store.js"; ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') );
🗄 Создание магазина и добавление редукторов
В приведенном выше примере я ссылался на store.js
. Самый простой пример этого файла будет выглядеть примерно так:
// src/store.js import { createStore } from "redux"; import { todoReducer } from "./reducer.js"; const store = createStore(todoReducer); export default store;
Мы используем createStore
от Redux для создания пустого хранилища с редуктором.
Этот редуктор в конечном итоге обработает все наши диспетчерские действия. Мы называем его «корневым редуктором» - мы всегда передаем только один редуктор непосредственно в хранилище.
В реальном мире с более крупными приложениями вы должны разделить редукторы, а затем объединить их вместе в один корневой редуктор:
rootReducer = combineReducers({ todo: todoReducer, account: accountReducer });
👉 Здесь важно отметить: когда вы разделяете редукторы, вы также разделяете свое состояние на те же пространства имен. В приведенном выше примере состояние будет разделено на ключи todo
и account
, а связанный редуктор будет получать / создавать только эту конкретную часть состояния.
➖ Добавление редуктора
Давайте добавим наш редуктор, а также «начальное состояние»:
// src/reducer.js const initialState = { todos: [] }; function todoReducer(state = initialState, action) { switch (action.type) { case "ADD_TODO": return Object.assign({}, state, { todos: [ ...state.todos, { text: action.text, completed: false } ] }); default: return state; } } export default todoReducer;
Это то же самое, что и в предыдущем примере, с этим уродливым оператором переключения. Но мы можем добиться большего! И нам также нужно добавить «удалить» и «переключить». Так:
✨ Уменьшение Boilerplate + работа с неизменяемыми паттернами
Давайте следовать рекомендациям сокращения шаблонов. Мы собираемся позаботиться о нескольких вещах:
- Мы собираемся добавить константы для имен наших действий.
- Мы собираемся реализовать действия «Завершить» и «Удалить».
- Мы собираемся реализовать
createReducer
(как рекомендовано в документации), чтобы мы могли использовать отдельные функции вместо оператора switch. - Мы собираемся очистить
todoReducer
, чтобы следить за этим.
Давайте создадим новый файл для хранения всех констант действий, которые мы хотим реализовать:
// src/actionTypes.js export const ActionTypes = { AddTodo: "ADD_TODO", CompleteTodo: "COMPLETE_TODO", DeleteTodo: "DELETE_TODO" };
Нам также необходимо реализовать «завершить» и «удалить», что создает новую проблему: как мы подходим к этому с неизменным состоянием?
Мы можем найти несколько полезных советов в документации в разделе Неизменяемые шаблоны обновления, но наиболее полезным намеком из всех, кажется, является библиотека под названием Immer, которая звучит великолепно. : с помощью Immer мы можем писать нормальный код и забыть о большинстве обычных проблем с неизменяемым состоянием. Ура! 🥂🎉
Итак, добавив все это, вот как выглядит полностью реализованный редуктор:
// src/reducer.js import produce from "immer"; import {ActionTypes} from "./actionTypes"; const initialState = { todos: {} }; // createReducer as suggested in "reducing boilerplate" function createReducer(handlers) { return function reducer(state = initialState, action) { if (handlers.hasOwnProperty(action.type)) { // let's integrate immer at this level for ease! let test = produce(state, draft => { const handler = handlers[action.type]; return handler(draft, action) }); console.log(initialState, test); return test; } else { return state } } } // In the real world, your server would probably assign the ID // For the sake of this example, we'll auto-increment a counter let idMaker = 0; export const todoReducer = createReducer({ [ActionTypes.AddTodo]: (state, action) => { const text = action.text.trim(); const nextId = idMaker++; state.todos[nextId] = { id: nextId, text: text, checked: false }; }, [ActionTypes.CompleteTodo]: (state, action) => { state.todos[action.id].checked = true; }, [ActionTypes.DeleteTodo]: (state, action) => { delete state.todos[action.id]; } });
Итак, мы внесли несколько полезных улучшений в файл редуктора:
- Все действия (добавить, завершить, удалить) реализованы.
- Благодаря Immer код легко писать, читать и поддерживать. 👌
- Больше никаких уродливых операторов switch, а аккуратно разделенные функции.
💻 Генераторы пользовательского интерфейса и основных действий
Хорошо, разобравшись с этим, давайте, наконец, заставим Redux работать!
Мы собираемся настроить действительно простой пользовательский интерфейс для нашего todo-приложения и связать действия, которые объединят все это воедино. Вот что я придумал:
Замечательно, насколько прост код. Для простоты я объединил все это в один исходный файл. В реальном мире вы, вероятно, захотите разделить это:
// src/app.js import React from 'react'; import {connect} from 'react-redux'; import {ActionTypes} from './actionTypes.js'; // Our action generators: const addTodo = (text) => ({ type: ActionTypes.AddTodo, text }); const deleteTodo = (id) => ({ type: ActionTypes.DeleteTodo, id }); const completeTodo = (id) => ({ type: ActionTypes.CompleteTodo, id }); // Our Todo-item presentation component: class Todo extends React.Component { render() { return ( <div> <input type={"checkbox"} id={this.props.id} checked={this.props.checked} onClick={this.props.onComplete}/> <label htmlFor={this.props.id}> <strong>{this.props.text} </strong> </label> <button onClick={this.props.onDelete}>Del</button> <hr/> </div> ); } } // The main container component: class App extends React.Component { handleKeyPress(e) { if (e.key === "Enter") { const text = e.target.value; if (text) { this.props.dispatch(addTodo(text)); e.target.value = ""; } } } handleTodoDelete(todo) { this.props.dispatch(deleteTodo(todo.id)); } handleTodoCheck(todo) { if (!todo.checked) { this.props.dispatch(completeTodo(todo.id)); } } render() { return ( <div> <h1>✔ TodoApp</h1> <hr/> <input placeholder={"Add new todo"} onKeyPress={(e) => this.handleKeyPress(e)} style={{width: "300px", height: "30px"}} autoFocus={true}/> <hr/> {Object.values(this.props.todos).map((todo, i) => ( <Todo key={i} {...todo} onDelete={() => this.handleTodoDelete(todo)} onComplete={() => this.handleTodoCheck(todo)} /> ))} </div> ); }; } const mapStateToProps = (state) => ({ todos: state.todos }); export default connect(mapStateToProps)(App);
Итак, давайте посмотрим на некоторые вещи, происходящие в этом приложении:
- В нашем основном экспорте для класса
App
мы используем функциюconnect
, чтобы обернуть компонент магией Redux. - Функция
mapStateToProps
фильтрует соответствующие реквизиты, которые мы хотим извлечь из хранилища, и передаем компонентуApp
. В нашем случае мы просто достаем список задач. Если этот список изменится, он будет повторно отрисован. - Вы никогда не обращаетесь к хранилищу напрямую, вместо этого
connect
дает нам доступ кprops.dispatch
для передачи действий редуктору.
И вот мы: супер простое приложение для задач на основе React и Redux! 🙌
🤩 Добавление DevTools
Давайте возьмем Redux DevTools и включим его, добавив крючок в вызов createStore
.
const store = createStore( todoReducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() );
Если вам интересно, мы передаем нечто, называемое enhancer
, в инициализатор хранилища, который может добавить [..] сторонние возможности, такие как промежуточное ПО, путешествия во времени, постоянство и т. Д..
В нашем случае это помогает инструментам разработчика подключиться к Redux. Мы можем видеть каждое действие, которое мы отправили, и даже прыгать во времени между этими точками:
Думаю, это хорошее начало!
Теперь у нас есть основное приложение, инструменты для его легкой отладки и стек, который избавляет нас от ада операторов переключения и сложных шаблонов неизменяемого состояния. Я думаю, что это очень прочная основа для дальнейшего развития.
Следующие шаги
- Если вы хотите глубже погрузиться в Redux, ознакомьтесь с официальной документацией. Иногда они бывают немного тяжелыми, но очень полными и содержат полезные примеры.
- Если вы переходите в реальный мир, вы, вероятно, захотите сосредоточиться на интеграции редукторов с вашим API. Обратите внимание на библиотеку redux-thunk (операции асинхронного хранилища). Вот хорошая статья Чарльза Стовера.
Код для этой демонстрации
Если вы хотите поиграть с файлами, упомянутыми в этом сообщении:
👉 https://github.com/roydejong/react-redux-todo-sample
Спасибо за чтение!
👇 Сообщите мне ниже, если у вас есть какие-либо вопросы, комментарии или советы.