🙌 Больше никаких бурений!

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

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

Что такое Redux?

Redux позиционирует себя как контейнер с предсказуемым состоянием для приложений JavaScript.

Не знаю, как вы, но для меня это ничего не значит. Я знаю, что это за состояние. Я знаю, что такое контейнер. Зачем мне нужна библиотека для хранения моего состояния? 🤷‍♂️ Понятия не имею. Эй, все говорят, что Redux - это круто, и я не хочу быть неудачником.

Так что это на самом деле означает? Что ж, когда мы думаем о том, как мы обычно управляем состоянием в React, вы, вероятно, делаете одно из следующих действий:

  1. «Детализация компонентов», при которой вы передаете объекты вниз вниз от родительского компонента к какому-то удаленному дочернему компоненту. Например, если вы хотите показать аватар пользователя, вы можете передать объект user вниз от вашего App к вашему Page, к вашему Navbar, к вашему UserMenu, к вашему Avatar. Или что-то похуже.
  2. «Free-for-all», где вы устали от этой пропасти, проходящей через ад, и каждый компонент управляет своим собственным состоянием, возможно, через какое-то промежуточное хранилище данных, которое вы создали. Здесь начинается смешение бизнес-логики и логики представления, и через некоторое время вы хотите буквально поджечь всю свою кодовую базу из-за созданного вами 💩.
  3. «Детский реквизит», довольно разумный промежуточный вариант, когда вы используете свойство 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. 1️⃣ Есть один единственный источник истины:
    Все данные о состоянии приложения поступают только из одного места: из вашего магазина.
  2. 🔒 Состояние только для чтения:
    Мы только читаем состояние и никогда не меняем его. Действия могут привести к новому состоянию.
  3. 😇 Изменения вносятся с помощью чистых функций:
    мы никогда не изменяем состояние напрямую, но редукторы могут возвращать новое состояние.

📕 Редукторы 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 + работа с неизменяемыми паттернами

Давайте следовать рекомендациям сокращения шаблонов. Мы собираемся позаботиться о нескольких вещах:

  1. Мы собираемся добавить константы для имен наших действий.
  2. Мы собираемся реализовать действия «Завершить» и «Удалить».
  3. Мы собираемся реализовать createReducer (как рекомендовано в документации), чтобы мы могли использовать отдельные функции вместо оператора switch.
  4. Мы собираемся очистить 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}&nbsp;</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. Мы можем видеть каждое действие, которое мы отправили, и даже прыгать во времени между этими точками:

Думаю, это хорошее начало!

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

Следующие шаги

Код для этой демонстрации

Если вы хотите поиграть с файлами, упомянутыми в этом сообщении:
👉 https://github.com/roydejong/react-redux-todo-sample

Спасибо за чтение!

👇 Сообщите мне ниже, если у вас есть какие-либо вопросы, комментарии или советы.