Я уже довольно давно использую Vue и React.

Будучи разработчиками JavaScript, мы получаем возможность работать с несколькими фреймворками и различными системами управления состоянием. Поскольку я внештатный веб-разработчик, я использую как React, так и Vue в зависимости от требований проекта. В этой истории я сравню работу двух очень популярных систем управления состоянием, Vuex и Redux. Хотя оба они вдохновлены архитектурой Flux, они используют разные способы достижения результата.

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

Итак, вот статья о сравнении того, как Vuex и Redux делают одни и те же вещи разными способами.

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

HTML

На данный момент разметка обоих приложений абсолютно одинакова, позже мы будем использовать директивы в функциях Vue и Render в React для их изменения.

Начиная

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

Vuex

Vuex тесно связан с VueJS, поэтому для начала работы с Vuex меньше стандартного кода.

Redux

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

Магазин и состояние

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

Как это делает Vuex

Состояние Vuex изменчиво, поэтому мы можем напрямую создавать переменные состояния и присваивать им значения.

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

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

Использование этих состояний в нашем приложении

Теперь, когда мы смогли создать состояние с одним жестко запрограммированным элементом TODO, давайте посмотрим, как мы можем использовать это в нашем приложении.

Как это делает Vuex

Vue имеет вспомогательную функцию mapState() для сопоставления состояний из нашего хранилища Vuex нашим компонентам. Затем к этим сопоставленным переменным можно будет обращаться напрямую, как к обычным переменным состояния, хотя мы не можем напрямую изменять эти переменные.

Если нам нужно выполнить некоторую операцию с нашими переменными состояния и получить вычисленное значение для использования в различных компонентах, мы можем использовать getters Vuex, используя это в нашем шаблоне:

<li v-for="(item, index) in todoList" 
  :key="item.id" 
  :class="{ completed: item.completed}"
>

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

Redux имеет mapStateToProps() метод, который передается компоненту более высокого порядка connect, предоставленному react-redux библиотекой. Эти состояния теперь доступны как свойства в нашем компоненте.

//component
import { connect } from 'react-redux';
const mapStateToProps = (state) => {
  return { todos: state };
}
export default connect(mapStateToProps)(App);

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

renderList() {
return this.props.todos.map(item => {
  return (
    <li key={item.id}
      className={"todo " + (item.completed ? "completed" : "")}
      onClick={() => this.props.toggleCompletion(item.id)}>
    </li>
  )
})
}

Изменение состояния

Переменная состояния не должна изменяться напрямую. Мы используем специальные методы для их изменения / обновления, чтобы их можно было правильно отслеживать.

Как это делает Vuex

Единственный способ изменить состояние в хранилище Vuex - это совершить мутацию. Мутации Vuex очень похожи на события; каждая мутация имеет строковый тип и обработчик. Функция обработчика - это то место, где мы выполняем фактические изменения состояния, и она получит состояние в качестве первого аргумента.


//store.js
mutations: {
    addItem(state, payload) {
      state.todos.push({id:GLOBAL_ID++, title: payload, completed:   false});
    },
    togglecompletion(state, id) {
      state.todos.forEach( item => {
        if(item.id === id) 
          item.completed = !item.completed;
      })
    },
    removeItem(state, index) {
      state.todos.splice(index, 1);
    }
  }

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

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

В Redux методы изменения состояния также написаны в редукторах.

//reducer/index.js
const todos = (state = initialState, action) => {
  switch (action.type) {
    case "ADD_ITEM":
      return [
        ...state,
        {
          id: GLOBAL_ID++,
          title: action.title,
          completed: false
        }
      ];
    case "TOGGLE_COMPLETION":
      console.log('action', action);
      return state.map(todo =>
        todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
      );
    case "REMOVE_ITEM":
      return state.filter(todo => todo.id !== action.id);
    default:
      return state;
  }
};

Редукторы поддерживают как состояние, так и методы их модификации. Эти методы вызываются действиями диспетчеризации. Эти действия также принимают полезную нагрузку для отправки данных из нашего приложения в наше хранилище Redux. (Помните, что в Redux состояния неизменны)

//actions/index.js
let nextTodoId = 0;
export const addItem = title => {
  return {
    type: "ADD_ITEM",
    id: nextTodoId++,
    title
  };
};
export const toggleCompletion = id => {
  return {
    type: "TOGGLE_COMPLETION",
    id
  };
};
export const removeItem = id => {
  return {
    type: "REMOVE_ITEM",
    id
  }
};

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

Изменение состояния из наших компонентов

Как это делает Vuex

Vuex предоставляет вспомогательный метод mapMutations() для доступа к нашим мутациям в компонентах.

methods: {
 ...mapMutations([
  'addItem', 
  'togglecompletion',
  'removeItem',
 ])
}

После сопоставления к этим методам можно будет получить доступ, как к обычным методам компонентов, используя эти мутации в нашем компоненте:

<button class="destroy" @click.stop="removeTodo(index)"></button>
removeTodo: function(index) {
  this.removeItem(index);
}

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

Подобно mapStateToProps() Redux предоставляет нам другого помощника, называемого mapDispatchToProps(), который передается нашему HOC.

const mapDispatcherstoProps = dispatch =>  {
  return {
    toggleCompletion: (id) => dispatch(toggleCompletion(id)),
    removeItem: (id) => dispatch(removeItem(id)),
    addItem: (title)=> dispatch(addItem(title)),
    addItemFromWeb: ()=> dispatch(addItemFromWeb())   
  }
}
export default connect(mapStateToProps, mapDispatcherstoProps)(App);

Этот метод получает dispatch в качестве аргумента, этот метод отправки используется для фиксации наших действий. Эти действия теперь сопоставляются с локальными методами, доступными через props, как таковые:

<button className="destroy" 
    onClick={() => this.props.removeItem(item.id)}
/>

Теперь наше приложение TO DO DO LIST полностью функционально, мы можем добавлять элементы, проверять завершенные элементы и удалять элементы.

Выполнение асинхронных вызовов

Расширяя наше приложение TO DO LIST, допустим, мы хотим загрузить список для пользователя, хранящийся на сервере, мы не можем делать вызовы на сервер напрямую из наших мутаций Vuex или действий Redux, поскольку они являются синхронными методами. Для этого нужны особые способы.

Как это делает Vuex

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

//store.js
actions: {
    addItemFromWeb(context) {
      axios.get('https://jsonplaceholder.typicode.com/todos/1')
      .then((response) => {
        console.log(response);
        context.commit('addItem', response.data.title)
      })
      .catch((error) => console.log(error));
    }
  }

В приведенном выше примере мы используем axios library для выполнения HTTP-вызова.

Чтобы использовать эти действия в наших компонентах, Vuex предоставляет нам mapActions() вспомогательный метод.

<button @click="addItemFromWeb"> Async Add </button>
methods: {
    ...mapActions([
      'addItemFromWeb'
    ])
}

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

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

Для этого мы используем промежуточное программное обеспечение под названием redux-thunk. Это промежуточное программное обеспечение очень просто: оно проверяет, является ли действие функцией. Если это так, то эта функция вызывается с dispatch.. Если нет, редукторы вызываются напрямую.

import { applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
const store = createStore(
   reducer,
   applyMiddleware(thunk),
);

Теперь в actions.js мы создаем нашу асинхронную функцию:

export const addItemFromWeb = () => {
    return dispatch => {
        axios.get('https://jsonplaceholder.typicode.com/todos/1')
        .then((response) =>{
            console.log(response);
            dispatch(addItem(response.data.title));
        })
        .catch((error) => {
            console.log(error);
        })
    }
}

Управление приложениями и масштабирование

По мере роста нашего приложения у нас будет больше состояний для управления, у нас не может быть единственного store.js файла для Vuex или единственного reducer.js файла для Redux, нам нужно разбить наше приложение на модули.

Как это делает Vuex

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

const moduleA = {
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: { ... },
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> `moduleA`'s state
store.state.b // -> `moduleB`'s state

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

Каждый module можно записать в отдельные файлы, которые можно импортировать в store.js

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

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

import { combineReducers } from 'redux' 
import todos from './todos' 
import counter from './counter'
  
let reducers = combineReducers({
todo: todos,
ctr: counter
})
const store = createStore(reducer);
ReactDOM.render(<Provider store={store}><App /></Provider>, document.getElementById('root'));

Библиотека Redux предоставляет нам функцию под названием combineReducers для объединения всех наших редукторов в один редуктор. (Обратите внимание, что мы не можем напрямую получить доступ к состояниям, присутствующим в одном редукторе, в другом) данные, необходимые для редуктора, должны передаваться компонентом через действия.

Заключение

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

Я надеюсь, что вы нашли это полезным. Если так, не забудьте оставить много хлопков! 👏

Если вы хотите сравнить Vue и React, я рекомендую вам прочитать эту статью Сунил Сандху.

Живая демонстрация

Vue + Vuex

React + Redux

Исходный код демонстрационного приложения

Vuex TODO Github

Redux TODO Github

В настоящее время я также могу работать внештатным веб-разработчиком. Есть какие-нибудь проекты для меня? Обязательно напишите мне на адрес [email protected] 😃