Привет, друзья!
Предлагаю вашему вниманию результаты небольшого исследования, посвященного сравнению Redux
и Vuex
. Это вторая часть статьи, вот ссылка на первую.
Введение
Redux
и Vuex
— это библиотеки для управления состоянием приложений, написанных на React
и Vue
, соответственно. Каждая из них по-своему реализует архитектуру для создания пользовательских интерфейсов, известную под названием Flux
.
Обратите внимание: Flux-архитектура предназначена для работы с глобальным или распределенным (global, shared) состоянием, т.е. состоянием, которое используется двумя и более автономными компонентами приложения. Автономными являются компоненты, между которыми не существует отношений или связи "предок-потомок" или "родитель-ребенок", т.е. это компоненты из разных поддеревьев дерева компонентов. Состояние, которое используется одним компонентом или передается от родительского компонента дочерним и обратно (в пределах одного поддерева), является локальным (local), оно должно храниться и управляться соответствующим компонентом. Разумеется, это не относится к корневому (root) компоненту.
Сказанное можно проиллюстрировать следующим образом (диаграмма посвящена коллокации, или совместному размещению состояний, но сути дела это не меняет):
Архитектура Flux
(в Redux
) предполагает следующее:
- наличие единственного источника истины или места для хранения состояния — хранилища (store);
- состояние изменяется только с помощью чистых функций — операций (actions);
- операции изменяют состояние не напрямую, а через редуктор (reducer), который модифицирует состояние на основе типа (type) и опциональной (необязательной) полезной нагрузки (payload) операции;
- операции отправляются в редуктор из слоя представления (view), пользовательского интерфейса, с помощью диспетчера (dispatcher);
- выборка определенной части состояния или вычисление производных данных осуществляется с помощью селекторов (selectors). Вы можете думать о селекторах как об инструкциях
SELECT
из языкаSQL
- асинхронные операции, такие как HTTP- или AJAX-запросы, выполняются с помощью преобразователей (thunks).
Если вас интересует только код, то вот ссылка на репозиторий.
Демо React-приложения можно посмотреть здесь, а Vue-приложения — здесь.
Вы готовы? Тогда вперед!
Vuex
Vuex
— это паттерн / библиотека для управления состоянием, вдохновленная Flux
, Redux
и Elm
.
Как и в Redux
, единственным источником истины или местом для хранения состояния в Vuex
является хранилище (store). Состояние также не изменяется напрямую. Вместо этого слой представления (view) — пользовательский интерфейс — запускает операции (actions), которые могут запускать (dispatch) другие операции или фиксировать (commit) изменения с помощью мутаций (mutations). Мутации также могут запускаться из слоя представления напрямую. Основное отличие между мутациями и операциями состоит в том, что мутации должны быть синхронными, а операции, по сути, предназначены для реализации асинхронной логики с последующим запуском мутаций. Однако зачастую мутации и операции дублируются и слой представления взаимодействует только с операциями. Для извлечения части состояния в Vuex
вместо селекторов (selectors) используются геттеры (getters).
Это выглядит так:
API
Хранилище
Для создания хранилища используется метод createStore()
, которому передается объект со следующими настройками (из тех, которые мы будем использовать):
-
state: object | function
— значение начального состояния -
mutations: { [string]: function }
— объект с функциями для синхронной модификации состояния. Каждая функция в качестве первого аргумента получает текущее состояние (state
), а в качестве второго аргумента — опциональную полезную нагрузку (payload
) -
actions: { [string]: function }
— объект, содержащий функции, предназначенные для выполнения асинхронных операций и запуска мутаций для изменения состояния. Каждая операция в качестве второго аргумента, как и мутации, получает опциональную полезную нагрузку (payload
), а в качестве первого аргумента — объект контекста (context
) со следующими свойствами:-
state
— состояние -
commit
— метод для запуска мутации -
dispatch
— метод для запуска другой операции -
getters
— свойство для получения геттеров
-
-
getters: { [string]: function }
— геттеры для извлечения части состояния или вычисления производных данных. Каждый геттер получает состояние (state
) и геттеры (getters
)
import { createStore } from 'vuex'
const store = createStore({
state() {
return {
count: 0
}
}
mutations: {
['SET_COUNT'](state, payload) {
state.count = payload.count
},
['INCREMENT'](state){
state.count++
},
['DECREMENT'](state){
state.count--
},
['INCREMENT_BY_AMOUNT'](state, payload) {
state.count += payload.amount
}
},
actions: {
increment({ commit }) {
commit('INCREMENT')
},
decrement({ commit }) {
commit('DECREMENT')
},
incrementByAmount({ commit }, payload) {
commit('INCREMENT_BY_AMOUNT', payload)
},
setCount({ commit }, payload) {
commit('SET_COUNT', payload)
},
setCountAsync({ dispatch }) {
const timerId = setTimeout(() => {
dispatch(setCount(10))
clearTimeout(timerId)
}, 1000)
}
}
})
Экземпляр хранилища
Доступ к экземпляру хранилища в компоненте можно получить с помощью this.$store
.
Свойства экземпляра хранилища:
-
state
— состояние; -
getters
— геттеры.
Методы экземпляра хранилища:
-
commit
— для запуска мутации; -
dispatch
— для запуска операции; -
replaceState
— для замены состояния; используется для гидратации состояния; -
watch (function, callback)
— для наблюдения за значением, возвращаемымfunction
, и запускомcallback
при изменении этого значения; -
subscribe (handler, options)
— для подписки на изменения хранилища. Обработчик (handler
) получает мутацию и модифицированное этой мутацией состояние и, помимо прочего, возвращает метод для отписки —const unsubsciribe = store.subscribe((mutation, state) => {})
. По умолчанию каждый последующий обработчик добавляется в конец цепочки обработчиков. Для добавления обработчика в начало цепочки используется настройкаprepend
со значениемtrue
—store.subscribe(handler, { prepend: true })
Существуют несколько дополнительных методов для особых случаев.
Утилиты для привязки (binding)
-
mapState
— для создания вычисляемых свойств компонента, возвращающих часть состояния; -
mapGetters
— для создания вычисляемых свойств компонента, возвращающих значения геттеров; -
mapActions
— для создания методов компонента, отправляющих операции; -
mapMutations
— для создания методов, запускающих мутации.
И это все, что вам нужно знать о Vuex
, ну, почти, не считая продвинутых тем, вроде модулей или пространств имен. "Все? — спросите вы. — Но рассмотрению Redux Toolkit
была посвящена целая статья!" Вот именно, друзья, вот именно.
Реализация хранилища
Учитывая, что мы спроектировали наше хранилище в первой части статьи, можно сразу приступать к его реализации с помощью Vuex
. Поразмыслив, я пришел к выводу, что индикатор загрузки (статус) и сообщение (об успехе или провале операций) должны принадлежать всему приложению, а не части состояния для задач, как это было в хранилище Redux
. Также немного отличается логика вызова операции для выполнения искусственной задержки перед очисткой сообщения:
import { createStore } from 'vuex'
// Утилита для выполнения HTTP-запросов
import axios from 'axios'
// Адрес сервера
const SERVER_URL = 'http://localhost:5000/todos'
// Асинхронная операция - получение задач от сервера
const getTodos = async () => {
try {
const { data: todos } = await axios(SERVER_URL)
// возвращаем задачи и сообщение об успехе
return {
todos,
message: { type: 'success', text: 'Todos has been loaded' }
}
} catch (err) {
console.error(err.toJSON())
// возвращаем сообщение об ошибке
return {
message: {
type: 'error',
text: 'Something went wrong. Try again later'
}
}
}
}
// Асинхронная операция - сохранение задач в БД
const postTodos = async (newTodos) => {
try {
// получаем данные - существующие задачи
const { data: existingTodos } = await axios(SERVER_URL)
// перебираем существующие задачи
for (const todo of existingTodos) {
// формируем `URL` текущей задачи
const todoUrl = `${SERVER_URL}/${todo.id}`
// пытаемся найти общую задачу
const commonTodo = newTodos.find((_todo) => _todo.id === todo.id)
// если получилось
if (commonTodo) {
// определяем наличие изменений
if (
!Object.entries(commonTodo).every(
([key, value]) => value === todo[key]
)
) {
// если изменения есть, обновляем задачу на сервере,
// в противном случае, ничего не делаем
await axios.put(todoUrl, commonTodo)
}
} else {
// если общая задача отсутствует, удаляем задачу на сервере
await axios.delete(todoUrl)
}
}
// перебираем новые задачи и сравниваем их с существующими
for (const todo of newTodos) {
// если новой задачи нет среди существующих
if (!existingTodos.find((_todo) => _todo.id === todo.id)) {
// сохраняем ее в БД
await axios.post(SERVER_URL, todo)
}
}
// сообщение об успехе операции
return {
type: 'success',
text: 'Todos has been saved'
}
} catch (err) {
console.error(err.toJSON())
// сообщение о провале операции
return {
type: 'error',
text: 'Something went wrong. Try again later'
}
}
}
// Создаем и экспортируем хранилище
export default createStore({
// Начальное состояние
state: {
// для задач
todos: [],
// для статуса приложения
status: 'idle',
// для сообщения
message: {},
// для фильтра
filter: 'all'
},
// Мутации - синхронная модификация состояния
mutations: {
// добавление задач
['SET_TODOS'](state, { todos, message }) {
if (todos) {
state.todos = todos
}
state.message = message
state.status = 'idle'
},
// установка статуса
['SET_STATUS'](state, status) {
state.status = status
},
// добавление новой задачи
['ADD_TODO'](state, todo) {
state.todos.push(todo)
},
// удаление задачи
['REMOVE_TODO'](state, todo) {
state.todos.splice(state.todos.indexOf(todo), 1)
},
// обновление задачи
['UPDATE_TODO'](state, { id, changes }) {
const index = state.todos.findIndex((todo) => todo.id === id)
state.todos.splice(index, 1, { ...state.todos[index], ...changes })
},
// завершение всех активных задач
['COMPLETE_TODOS'](state) {
state.todos = state.todos.map((todo) =>
todo.done ? todo : { ...todo, done: !todo.done }
)
},
// удаление завершенных задач
['CLEAR_COMPLETED'](state) {
state.todos = state.todos.filter((todo) => !todo.done)
},
// сохранение задач в БД
['SAVE_TODOS'](state, message) {
state.message = message
state.status = 'idle'
},
// установка фильтра
['SET_FILTER'](state, filter) {
state.filter = filter
},
// очистка сообщения
['CLEAR_MESSAGE'](state) {
state.message = {}
}
},
// Операции - асинхронная логика и вызов мутаций
actions: {
// получение задач от сервера
fetchTodos({ commit, dispatch, state }) {
state.status = 'loading'
getTodos()
.then((todos) => {
commit('SET_TODOS', todos)
})
.then(() => {
dispatch('giveMeSomeTime')
})
},
// установка статуса
setStatus({ commit }, status) {
commit('SET_STATUS', status)
},
// добавление новой задачи
addTodo({ commit }, todo) {
commit('ADD_TODO', todo)
},
// обновление задачи
updateTodo({ commit }, payload) {
commit('UPDATE_TODO', payload)
},
// удаление задачи
removeTodo({ commit }, id) {
commit('REMOVE_TODO', id)
},
// завершение всех активных задач
completeTodos({ commit }) {
commit('COMPLETE_TODOS')
},
// удаление завершенных задач
clearCompleted({ commit }) {
commit('CLEAR_COMPLETED')
},
// сохранение задач в БД
saveTodos({ commit, state, dispatch }) {
state.status = 'loading'
postTodos(state.todos)
.then((message) => {
commit('SAVE_TODOS', message)
})
.then(() => {
dispatch('giveMeSomeTime')
})
},
// установка фильтра
setFilter({ commit }, filter) {
commit('SET_FILTER', filter)
},
// выполнение задержки перед очисткой сообщения
giveMeSomeTime({ commit }) {
const timerId = setTimeout(() => {
commit('CLEAR_MESSAGE')
clearTimeout(timerId)
}, 1500)
}
},
// Геттеры - получение части состояния
getters: {
// получение отфильтрованных задач
filteredTodos: ({ todos, filter }) => {
if (filter === 'all') return todos
return filter === 'active'
? todos.filter((todo) => !todo.done)
: todos.filter((todo) => todo.done)
},
// получение статистики
todoStats: ({ todos }) => {
const total = todos.length
const completed = todos.filter((todo) => todo.done).length
const active = total - completed
const percent = total === 0 ? 0 : Math.round((active / total) * 100)
return {
total,
completed,
active,
percent
}
}
}
})
В приведенном коде реализован такой же функционал, как и в случае с Redux
, но самого кода в 2 раза меньше, да и архитектура более понятная и простая. Так что по данному критерию я присваиваю победу Vuex
. Но что насчет производительности? Возможно, сложная архитектура Redux
— залог высокой скорости выполнения операций чтения / записи данных? Скоро мы это выясним, но сначала проведем покомпонентное сравнение наших приложений.
Компоненты приложений
Дабы не утомлять тех читателей, которым данный раздел неинтересен, я помещу код компонентов под кат.
{
"todos": [
{
"id": "1",
"text": "Eat",
"done": true,
"edit": false
},
{
"id": "2",
"text": "Code",
"done": true,
"edit": false
},
{
"id": "3",
"text": "Sleep",
"done": false,
"edit": false
},
{
"id": "4",
"text": "Repeat",
"done": false,
"edit": false
}
]
}
React (index.jsx)
import React, { StrictMode } from 'react'
import { render } from 'react-dom'
// Провайдер для передачи состояния в дочерние компоненты
import { Provider } from 'react-redux'
// Хранилище и операции для получения задач от сервера и выполнения задержки перед очисткой хранилища
import { store, fetchTodos, giveMeSomeTime } from './store'
// Основной компонент приложения
import App from './App'
// Отправляем в редуктор операцию для получения задач от сервера
// и следом за ней операцию для очистки сообщения с задержкой в 2 секунды
store.dispatch(fetchTodos()).then(() => store.dispatch(giveMeSomeTime()))
render(
<StrictMode>
{/* Передаем хранилище в качестве пропа `store` */}
<Provider store={store}>
<App />
</Provider>
</StrictMode>,
document.getElementById('root')
)
Vue (main.js)
import { createApp } from 'vue'
// Основной компонент приложения
import App from './App.vue'
// Хранилище
import store from './store'
// Получаем задачи от сервера
store.dispatch('fetchTodos')
createApp(App).use(store).mount('#app')
React (App.jsx)
// Хук для получения селекторов
import { useSelector } from 'react-redux'
// Компоненты приложения
import New from './components/New'
import Filters from './components/Filters'
import List from './components/List'
import Controls from './components/Controls'
import Stats from './components/Stats'
import Loader from './components/Loader'
// Селектор для выборки всех задач
import { selectTotal } from './store'
export default function App() {
const dispatch = useDispatch()
// Получаем общее количество задач
const total = useSelector(selectTotal)
// Получаем индикатор загрузки
const status = useSelector(({ todos }) => todos.status)
// Получаем сообщение
const message = useSelector(({ todos }) => todos.message)
/**
* Логика рендеринга:
* - компонент для добавления новой задачи (New) рендерится всегда
* - если есть сообщение, оно рендерится
* - если приложение находится в состоянии загрузки (status === 'loading'), рендерится только индикатор
* - если в массиве есть хотя бы одна задача (total > 0),
* рендерятся все остальные компоненты, в противном случае,
* рендерится только кнопка для сохранения задач из компонента `Controls`
*/
return (
<div
className='container d-flex flex-column text-center mt-2 mb-2'
style={{ maxWidth: '600px' }}
>
<h1 className='mb-4'>Modern Redux Todo App</h1>
<New />
{message.text ? (
<div
className={`alert ${
message.type === 'success' ? 'alert-success' : 'alert-danger'
} position-fixed top-50 start-50 translate-middle`}
role='alert'
style={{ zIndex: 1 }}
>
{message.text}
</div>
) : null}
{status === 'loading' ? (
<Loader />
) : total ? (
<>
<Stats />
<div className='row'>
<Filters />
<List />
<Controls />
</div>
</>
) : (
<div className='d-flex justify-content-end'>
<Controls />
</div>
)}
</div>
)
}
Vue (App.vue)
<template>
<div class="container d-flex flex-column text-center mt-2 mb-2">
<h1 class="mb-4">Modern Vuex Todo App</h1>
<New />
<div
v-if="message.text"
class="alert position-fixed top-50 start-50 translate-middle"
:class="messageClass"
role="alert"
>
{{ message.text }}
</div>
<Loader v-if="status === 'loading'" />
<template v-else-if="length">
<Stats />
<div class="row">
<Filters />
<List />
<Controls />
</div>
</template>
<div class="d-flex justify-content-end" v-else>
<Controls />
</div>
</div>
</template>
<script>
// Компоненты приложения
import New from './components/New'
import Stats from './components/Stats'
import Filters from './components/Filters'
import List from './components/List'
import Controls from './components/Controls'
import Loader from './components/Loader'
export default {
name: 'App',
components: { New, Stats, Filters, List, Controls, Loader },
computed: {
// Вычисляем сообщение
message() {
return this.$store.state.message
},
// Вычисляем CSS-класс для сообщения
messageClass() {
return this.message.type === 'success' ? 'alert-success' : 'alert-danger'
},
// Получаем статус приложения
status() {
return this.$store.state.status
},
// Получаем длину массива с задачами
length() {
return this.$store.state.todos.length
}
}
}
</script>
<style>
.container {
max-width: 600px;
}
.alert {
z-index: 1;
}
</style>
Список
React (index.jsx)
// Хук для получения селекторов
import { useSelector } from 'react-redux'
// Селектор для выборки отфильтрованных задач
import { selectFilteredTodos } from '../../store'
// Обычный элемент списка
import Regular from './Regular'
// Элемент списка для редактирования задачи
import Edit from './Edit'
export default function List() {
// Получаем отфильтрованные задачи
const filteredTodos = useSelector(selectFilteredTodos)
/**
* Логика рендеринга:
* - рендерим только отфильтрованные задачи
* - и в зависимости от индикатора редактирования задачи,
* рендерим тот или иной элемент списка
*/
return (
<div className=' col-6'>
<h3>Todos</h3>
<ul className='list-group'>
{filteredTodos.map((todo) =>
todo.edit ? (
<Edit key={todo.id} todo={todo} />
) : (
<Regular key={todo.id} todo={todo} />
)
)}
</ul>
</div>
)
}
Vue (index.vue)
<template>
<div class="col-6">
<h3>Todos</h3>
<ul v-show="todos.length">
<li
v-for="todo in todos"
:key="todo.id"
class="list-group-item d-flex align-items-center"
>
<Regular :todo="todo" v-if="!todo.edit" />
<Edit :todo="todo" v-else />
</li>
</ul>
</div>
</template>
<script>
// Утилита для привязки геттеров к компоненту
import { mapGetters } from 'vuex'
// Компонент для обычной задачи
import Regular from './Regular'
// Компонент для редактируемой задачи
import Edit from './Edit'
export default {
name: 'List',
components: { Regular, Edit },
computed: {
// Привязываем геттер к компоненту
...mapGetters({ todos: 'filteredTodos' })
}
}
</script>
Компонент для обычной задачи
React (Regular.jsx)
// Хук для получения диспетчера
import { useDispatch } from 'react-redux'
// Операции для обновления и удаления задачи
import { updateTodo, removeTodo } from '../../store'
export default function Regular({ todo }) {
// Получаем диспетчер
const dispatch = useDispatch()
// Извлекаем все свойства задачи
const { id, text, done, edit } = todo
return (
<li className='list-group-item d-flex align-items-center'>
<input
type='checkbox'
checked={done}
onChange={() => dispatch(updateTodo({ id, changes: { done: !done } }))}
className='form-check-input'
/>
<p
className={`flex-grow-1 m-0 ${
done ? 'text-muted text-decoration-line-through' : ''
}`}
>
{text}
</p>
<button
onClick={() => dispatch(updateTodo({ id, changes: { edit: !edit } }))}
className='btn btn-outline-info'
disabled={done}
>
<i className='bi bi-pencil'></i>
</button>
<button
onClick={() => dispatch(removeTodo(id))}
className='btn btn-outline-danger'
>
<i className='bi bi-trash'></i>
</button>
</li>
)
}
Vue (Regular.vue)
<template>
<input
type="checkbox"
:checked="todo.done"
@change="completeTodo"
class="form-check-input"
/>
<p class="flex-grow-1 m-0" :class="todoClass">
{{ todo.text }}
</p>
<button @click="editTodo" class="btn btn-outline-info" :disabled="todo.done">
<i class="bi bi-pencil"></i>
</button>
<button @click="removeTodo(todo)" class="btn btn-outline-danger">
<i class="bi bi-trash"></i>
</button>
</template>
<script>
// Утилита для привязки операций к методам компонента
import { mapActions } from 'vuex'
export default {
name: 'Regular',
props: ['todo'],
computed: {
// Вычисляем дополнительный CSS-класс для задачи
todoClass() {
return this.todo.done ? 'text-muted text-decoration-line-through' : ''
}
},
methods: {
// Привязываем операции к методам компонента
...mapActions(['updateTodo', 'removeTodo']),
// Метод для изменения индикатора завершенности задачи
completeTodo() {
this.updateTodo({ id: this.todo.id, changes: { done: !this.todo.done } })
},
// Метод для изменения индикатора редактирования задачи
editTodo() {
this.updateTodo({ id: this.todo.id, changes: { edit: !this.todo.edit } })
}
}
}
</script>
Компонент для редактируемой задачи
React (Edit.jsx)
import { useState, useEffect } from 'react'
// Хук для получения диспетчера
import { useDispatch } from 'react-redux'
// Операции для обновления и удаления задачи
import { updateTodo, removeTodo } from '../../store'
export default function Edit({ todo }) {
// Получаем диспетчер
const dispatch = useDispatch()
// Извлекаем все свойства задачи, кроме индикатора завершенности
const { id, text, edit } = todo
// Локальное состояние для текста редактируемой задачи
const [newText, setNewText] = useState(text)
// Функция для изменения текста
const onChangeText = ({ target: { value } }) => {
const trimmed = value.replace(/\s{2,}/g, ' ').trim()
setNewText(trimmed)
}
// Функция для завершения редактирования
const onFinishEdit = () => {
// Если текст отсутствует, вероятно, пользователь хочет удалить задачу
if (!newText) {
return dispatch(removeTodo(id))
}
// Отправляем операцию для обновления текста задачи
dispatch(updateTodo({ id, changes: { text: newText, edit: !edit } }))
}
// Функция для отмены редактирования
const onCancelEdit = () => {
// Отправляем операцию для изменения индикатора редактирования
dispatch(updateTodo({ id, changes: { edit: !edit } }))
}
// Функция для обработки нажатия клавиш клавиатуры:
// если нажата клавиша "Enter", завершаем редактирование,
// если нажата клавиша "Escape", отменяем редактирование
const onKeyDown = ({ key }) => {
switch (key) {
case 'Enter':
onFinishEdit()
break
case 'Escape':
onCancelEdit()
break
default:
break
}
}
// Регистрируем обработчик нажатия клавиш
// и удаляем его при размонтировании компонента
useEffect(() => {
window.addEventListener('keydown', onKeyDown)
return () => {
window.removeEventListener('keydown', onKeyDown)
}
})
return (
<li className='list-group-item d-flex align-items-center'>
<input
type='text'
value={newText}
onChange={onChangeText}
className='form-control flex-grow-1'
/>
<button onClick={onFinishEdit} className='btn btn-outline-success'>
<i className='bi bi-check'></i>
</button>
<button onClick={onCancelEdit} className='btn btn-outline-warning'>
<i className='bi bi-x-square'></i>
</button>
</li>
)
}
Vue (Edit.vue)
<template>
<input type="text" v-model="newText" class="form-control flex-grow-1" />
<button @click="finishEdit" class="btn btn-outline-success">
<i class="bi bi-check"></i>
</button>
<button @click="cancelEdit" class="btn btn-outline-warning">
<i class="bi bi-x-square"></i>
</button>
</template>
<script>
// Утилита для привязки операций к методам компонента
import { mapActions } from 'vuex'
export default {
name: 'Edit',
props: ['todo'],
data() {
// Локальное состояние для текста редактируемой задачи
return {
newText: this.todo.text
}
},
// Регистрируем обработчик нажатия клавиш клавиатуры при монтировании компонента
created() {
window.addEventListener('keydown', this.onKeyDown)
},
// Удаляем обработчик нажатия клавиш клавиатуры при размонтировании компонента
unmounted() {
window.removeEventListener('keydown', this.onKeyDown)
},
methods: {
// Привязываем операции к методам компонента
...mapActions(['updateTodo', 'removeTodo']),
// Обработчик нажатия клавиш клавиатуры
onKeyDown({ key }) {
switch (key) {
// Если нажата клавиша `Enter`, вызываем метод для завершения редактирования
case 'Enter':
this.finishEdit()
break
// Если нажата клавиша `Escape`, вызываем метод для отмены редактирования
case 'Escape':
this.cancelEdit()
break
default:
break
}
},
// Метод для завершения редактирования
finishEdit() {
const text = this.newText.replace(/\s{2,}/g, ' ').trim()
if (!text) return this.removeTodo(this.todo)
this.updateTodo({
id: this.todo.id,
changes: { text, edit: !this.todo.edit }
})
},
// Метод для отмены редактирования
cancelEdit() {
this.updateTodo({ id: this.todo.id, changes: { edit: !this.todo.edit } })
}
}
}
</script>
React (Controls.jsx)
// Хуки для получения диспетчера и селекторов
import { useDispatch, useSelector } from 'react-redux'
// Операции для завершения всех активных задач,
// удаления завершенных задач,
// сохранения задач в БД,
// очистки сообщения
// и селектор для выборки всех задач
import {
completeAllTodos,
clearCompletedTodos,
saveTodos,
giveMeSomeTime,
selectAll
} from '../store'
export default function Controls() {
// Получаем диспетчер
const dispatch = useDispatch()
// Получаем все задачи
const todos = useSelector(selectAll)
/**
* Логика рендеринга:
* - если в массиве есть хотя бы одна задача,
* рендерятся кнопки для завершения всех активных задач, удаления завершенных задач и сохранения задач в БД,
* в противном случае, рендерится только кнопка для сохранения задач
*/
return (
<div className='col-3 d-flex flex-column'>
<h3>Controls</h3>
{todos.length ? (
<>
<button
onClick={() => dispatch(completeAllTodos())}
className='btn btn-info mb-2'
>
Complete
</button>
<button
onClick={() => dispatch(clearCompletedTodos())}
className='btn btn-danger mb-2'
>
Clear
</button>
</>
) : null}
<button
onClick={() =>
// Отправляем операцию для сохранения задач
// и следом за ней операцию для очистки сообщения с задержкой в 2 секунды
dispatch(saveTodos(todos)).then(() => dispatch(giveMeSomeTime()))
}
className='btn btn-success'
>
Save
</button>
</div>
)
}
Vue (Controls.vue)
<template>
<div class="col-3 d-flex flex-column">
<h3>Controls</h3>
<template v-if="length">
<button @click="completeTodos" class="btn btn-info mb-2">Complete</button>
<button @click="clearCompleted" class="btn btn-danger mb-2">Clear</button>
</template>
<button @click="saveTodos" class="btn btn-success">Save</button>
</div>
</template>
<script>
// Утилита для привязки операций к методам компонента
import { mapActions } from 'vuex'
export default {
name: 'Controls',
computed: {
// Вычисляем длину массива с задачами
length() {
return this.$store.state.todos.length
}
},
methods: {
// Привязываем операции к методам компонента
...mapActions(['completeTodos', 'clearCompleted', 'saveTodos'])
}
}
</script>
React (Filters.jsx)
// Хуки для получения диспетчера и селекторов
import { useDispatch, useSelector } from 'react-redux'
// Операция для установки значения фильтра
import { setFilter } from '../store'
export default function Filters() {
// Получаем диспетчер
const dispatch = useDispatch()
// Получаем текущее значение фильтра
const { status } = useSelector((state) => state.filter)
return (
<div className='col-3'>
<h3>Filters</h3>
{['all', 'active', 'completed'].map((filter) => (
<div key={filter} className='form-check' style={{ textAlign: 'left' }}>
<input
id={filter}
type='radio'
checked={filter === status}
onChange={() => dispatch(setFilter(filter))}
className='form-check-input'
/>
<label htmlFor={filter} className='form-check-label'>
{filter.toUpperCase()}
</label>
</div>
))}
</div>
)
}
Vue (Filters.vue)
<template>
<div class="col-3">
<h3>Filters</h3>
<div
v-for="filter in ['all', 'active', 'completed']"
:key="filter"
class="form-check text-left"
>
<input
:id="filter"
type="radio"
:checked="filter === currentFilter"
@click="setFilter(filter)"
class="form-check-input"
/>
<label :for="filter" class="form-check-label">
{{ filter.toUpperCase() }}
</label>
</div>
</div>
</template>
<script>
// Утилита для привязки операций к методам компонента
import { mapActions } from 'vuex'
export default {
name: 'Filters',
computed: {
// Получаем текущее значение фильтра
currentFilter() {
return this.$store.state.filter
}
},
methods: {
// Привязываем операцию к методу компонента
...mapActions(['setFilter'])
}
}
</script>
<style scoped>
.text-left {
text-align: left !important;
}
</style>
React (Loader.jsx)
// Индикатор загрузки
export default function Loader() {
return (
<div className='spinner-border text-primary m-auto' role='status'>
<span className='visually-hidden'>Loading...</span>
</div>
)
}
Vue (Loader.vue)
<template>
<div class="spinner-border text-primary m-auto" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</template>
<script>
export default {
name: 'Loader'
}
</script>
React (New.jsx)
import { useState } from 'react'
// Хук для получения диспетчера
import { useDispatch } from 'react-redux'
// Утилита для генерации уникальных идентификаторов
import { nanoid } from '@reduxjs/toolkit'
// Операция для добавления новой задачи
import { addTodo } from '../store'
export default function New() {
// Получаем диспетчер
const dispatch = useDispatch()
// Локальное состояние для текста новой задачи
const [text, setText] = useState('')
// Функция для изменения текста задачи
const changeText = ({ target: { value } }) => {
// Заменяем два и более пробела на один и удаляем пробелы в начале и конце строки
const trimmed = value.replace(/\s{2,}/g, ' ').trim()
setText(trimmed)
}
// Функция для добавления задачи
const onAddTodo = (e) => {
e.preventDefault()
if (!text) return
const newTodo = {
id: nanoid(5),
text,
done: false,
edit: false
}
// Отправляем операцию для добавления новой задачи
dispatch(addTodo(newTodo))
setText('')
}
return (
<form onSubmit={onAddTodo} className='d-flex mb-4'>
<input
type='text'
placeholder='What needs to be done?'
value={text}
onChange={changeText}
className='form-control flex-grow-1'
/>
<button className='btn btn-outline-success'>
<i className='bi bi-plus-square'></i>
</button>
</form>
)
}
Vue (New.vue)
<template>
<form @submit.prevent="addTodoMethod" class="d-flex mb-4">
<input
type="text"
placeholder="What needs to be done?"
class="form-control flex-grow-1"
v-model="text"
/>
<button class="btn btn-outline-success">
<i class="bi bi-plus-square"></i>
</button>
</form>
</template>
<script>
// Утилита для привязки операций к методам компонента
import { mapActions } from 'vuex'
// Утилита для генерации уникальных идентификаторов
import { nanoid } from 'nanoid'
export default {
name: 'New',
data() {
// Локальное состояние для текста новой задачи
return {
text: ''
}
},
methods: {
// Привязываем операцию к методу компонента
...mapActions(['addTodo']),
// Метод для добавления новой задачи
addTodoMethod() {
const trimmed = this.text.replace(/\s{2,}/g, ' ').trim()
if (!trimmed) return
const todo = {
id: nanoid(5),
text: trimmed,
done: false,
edit: false
}
this.addTodo(todo)
this.text = ''
}
}
}
</script>
React (Stats.jsx)
// Хук для получения селекторов
import { useSelector } from 'react-redux'
// Селектор для выборки статистики
import { selectTodoStats } from '../store'
export default function Stats() {
// Получаем статистику (объект)
const stats = useSelector(selectTodoStats)
/**
* Логика рендеринга:
* - перебираем ключи и формируем с их помощью колонки-заголовки (выполняем "капитализацию" ключей)
* - перебираем значения и формируем с их помощью обычные колонки
*/
return (
<div className='row'>
<h3>Statistics</h3>
<table className='table text-center'>
<thead>
<tr>
{Object.keys(stats).map(([first, ...rest], index) => (
<th scope='col' key={index}>
{`${first.toUpperCase()}${rest.join('').toLowerCase()}`}
</th>
))}
</tr>
<tr>
{Object.values(stats).map((value, index) => (
<td key={index}>{value}</td>
))}
</tr>
</thead>
</table>
</div>
)
}
Vue (Stats.vue)
<template>
<div class="row">
<h3>Statistics</h3>
<table class="table text-center">
<thead>
<tr>
<th scope="col" v-for="(_, name, index) in todoStats" :key="index">
{{ `${name[0].toUpperCase()}${name.slice(1).toLowerCase()}` }}
</th>
</tr>
<tr>
<td v-for="(val, _, index) in todoStats" :key="index">{{ val }}</td>
</tr>
</thead>
</table>
</div>
</template>
<script>
// Утилита для привязки геттеров к компоненту
import { mapGetters } from 'vuex'
export default {
name: 'Stats',
computed: {
// Привязываем геттер к компоненту
...mapGetters(['todoStats'])
}
}
</script>
Своими впечатлениями о работе с Vue
я поделюсь как-нибудь в другой раз, но, в целом, могу сказать, что впечатления крайне положительные (есть много прикольных плюшек).
Измерение производительности
Я опишу измерение производительности на примере Redux-приложения, но точно такой же функционал реализован во Vuex-приложении.
Я не буду рассказывать обо всех методах, которые я испробовал. Наиболее показательные и стабильные результаты удалось получить с помощью записи 2500 задач и их последующего обновления.
Отключаем получение задач от сервера:
// store.dispatch(fetchTodos()).then(() => store.dispatch(giveMeSomeTime()))
Вносим изменения в App.jsx
:
// Хук для получения диспетчера
import { useDispatch } from 'react-redux'
// Операции для добавления новой задачи, обновления задачи и селектор для получения всех задач
import { addTodo, updateTodo, selectAll } from './store'
// Утилита для генерации идентификаторов
import { nanoid } from '@reduxjs/toolkit'
Получаем все задачи (они потребуются нам для обновления):
const todos = useSelector(selectAll)
Создаем функцию для записи 2500 задач:
const createManyTodos = () => {
// массив для времени выполнения операций
const times = []
// 25 итераций
for (let i = 0; i < 25; i++) {
// время начала выполнения операции
const start = Date.now()
// 100 задач
for (let i = 0; i < 100; i++) {
const id = nanoid()
const todo = {
id,
text: `Todo ${id}`,
done: false,
edit: false
}
// отправляем операцию для добавления новой задачи
dispatch(addTodo(todo))
}
// разница между началом и окончанием выполнения операции
const diff = Date.now() - start
// помещаем разницу в массив
times.push(diff)
}
// вычисляем среднее время выполнения операций
const time = times.reduce((a, c) => (a += c), 0) / 25
// и выводим его в консоль
console.log(time)
}
Создаем функцию для обновления всех задач:
const updateAllTodos = () => {
// время начала выполнения операции
const start = Date.now()
// перебираем задачи
for (let i = 0; i < todos.length; i++) {
// отправляем операцию для обновления задачи - изменения значения индикатора ее завершенности
dispatch(updateTodo({ id: todos[i].id, changes: { done: true } }))
}
// вычисляем разницу между началом и окончанием выполнения операции
const diff = Date.now() - start
// и выводим ее в консоль
console.log(diff)
}
Добавляем в разметку соответствующие кнопки:
<div className='d-flex gap-2 mb-2 align-items-center'>
<button className='btn btn-primary' onClick={createManyTodos}>
Create
</button>
<p className='mb-0'>many todos</p>
</div>
<div className='d-flex gap-2 mb-2 align-items-center'>
<button className='btn btn-primary' onClick={updateAllTodos}>
Update
</button>
<p className='mb-0'>all todos</p>
</div>
У вас может возникнуть вопрос о том, какое влияние оказывает рендеринг задач на выполнение операций. Ответ: для Vuex
почти никакое. Но в случае с Redux
, на первый взгляд, может показаться, что он ожидает завершения отрисовки всех задач (даже когда такая отрисовка отключена), что делает его ОЧЕНЬ медленным. На самом деле, как мы увидим позже, все с точностью да наоборот: не рендеринг замедляет работу Redux
, а Redux
блокирует рендеринг.
Я обновил код в репозитории и песочницах, так что можете оценить производительность приложений самостоятельно.
Мои результаты выглядят следующим образом (они очень сильно меня удивили):
- в режиме для разработки
- запись задач
-
Redux
—400-450
мс -
Vuex
—4-5
мс - обновление задач
-
Redux
— чудовищные18000-20000
мс -
Vuex
— в районе1500
мс
- в производственном режиме (с помощью
serve
)- запись задач
-
Redux
— в среднем300
мс -
Vuex
— около4
мс - обновление задач
-
Redux
— по-прежнему чудовищные11000-13000
мс -
Vuex
— как ни странно, около2000
мс (-500
мс по сравнению с режимом для разработки)
Таким образом, при выполнении операций записи скорость Vuex
выше скорости Redux
в 70-90
раз, а при выполнении операций обновления — в 5-6
раз.
Попытавшись определить причину такой медлительности Redux
, я пришел к выводу, что все дело в манипуляциях с данными, которые совершает Redux
с целью их нормализации с помощью адаптера сущностей. Причем, все настолько плохо, что попытка записи 10 000 задач "вешает" браузер (возможно, ваша машина сумеет с этим справиться, моя категорически отказывалась со мной сотрудничать). Для сравнения Vuex
прекрасно с этим справляется (осторожно: отрисовка такого количества задач может оказаться непосильной задачей для браузера). На мой взгляд, игра, определенно, не стоит свеч. Нормализованные структуры, действительно, бывают удобными в некоторых ситуациях, но такое влияние на производительность является неприемлемым.
Но неужели Vuex
— это лучшее, что может предложить Flux-архитектура с точки зрения скорости выполнения операций и удобства использования для управления состоянием приложения?
Мне, как человеку, который для разработки приложений использует в основном React
, стало за него обидно. Поэтому я решил переписать хранилище с помощью хуков useContext
и useReducer
, а также парочки трюков:
import { createContext, useContext, useReducer, useMemo } from 'react'
import axios from 'axios'
const SERVER_URL = 'http://localhost:5000/todos'
// константы
const SET_TODOS = 'SET_TODOS'
const SET_STATUS = 'SET_STATUS'
const ADD_TODO = 'ADD_TODO'
const UPDATE_TODO = 'UPDATE_TODO'
const REMOVE_TODO = 'REMOVE_TODO'
const COMPLETE_TODOS = 'COMPLETE_TODOS'
const CLEAR_COMPLETED = 'CLEAR_COMPLETED'
const SET_FILTER = 'SET_FILTER'
const SET_MESSAGE = 'SET_MESSAGE'
// редуктор
const reducer = (state, { type, payload }) => {
switch (type) {
case SET_TODOS:
return {
...state,
todos: payload
}
case SET_STATUS:
return {
...state,
status: payload
}
case ADD_TODO:
return { ...state, todos: state.todos.concat(payload) }
case UPDATE_TODO:
return {
...state,
todos: state.todos.map((todo) =>
todo.id === payload.id ? { ...todo, ...payload.changes } : todo
)
}
case REMOVE_TODO:
return {
...state,
todos: state.todos.filter((todo) => todo.id !== payload)
}
case COMPLETE_TODOS:
return { ...state, todos: state.todos.map((todo) => todo.done === true) }
case CLEAR_COMPLETED:
return {
...state,
todos: state.todos.filter((todo) => todo.done === true)
}
case SET_FILTER:
return {
...state,
filter: payload
}
case SET_MESSAGE:
return {
...state,
message: payload
}
default:
return state
}
}
// задержка
const giveMeSomeTime = async () =>
await new Promise((resolve) => {
const timerId = setTimeout(() => {
resolve()
clearTimeout(timerId)
}, 2000)
})
// создатель операций
// принимает диспетчер
const createActions = (dispatch) => ({
setTodos: (todos) => ({
type: SET_TODOS,
payload: todos
}),
setStatus: (status) => ({
type: SET_STATUS,
payload: status
}),
addTodo: (todo) => ({
type: ADD_TODO,
payload: todo
}),
updateTodo: (payload) => ({
type: UPDATE_TODO,
payload
}),
removeTodo: (todoId) => ({
type: REMOVE_TODO,
payload: todoId
}),
completeTodos: () => ({
type: COMPLETE_TODOS
}),
clearCompleted: () => ({
type: COMPLETE_TODOS
}),
setFilter: (filter) => ({
type: SET_FILTER,
payload: filter
}),
setMessage: (message) => ({
type: SET_MESSAGE,
payload: message
}),
async fetchTodos() {
dispatch(this.setStatus('loading'))
try {
const { data: todos } = await axios(SERVER_URL)
dispatch(this.setTodos(todos))
dispatch(
this.setMessage({ type: 'success', text: 'Todos has been loaded' })
)
} catch (err) {
console.error(err.toJSON())
dispatch(
this.setMessage({
type: 'error',
text: 'Something went wrong. Try again later'
})
)
} finally {
dispatch(this.setStatus('idle'))
await giveMeSomeTime()
dispatch(this.setMessage({}))
}
},
async saveTodos(newTodos) {
dispatch(this.setStatus('loading'))
try {
const { data: existingTodos } = await axios(SERVER_URL)
for (const todo of existingTodos) {
const todoUrl = `${SERVER_URL}/${todo.id}`
const commonTodo = newTodos.find((_todo) => _todo.id === todo.id)
if (commonTodo) {
if (
!Object.entries(commonTodo).every(
([key, value]) => value === todo[key]
)
) {
await axios.put(todoUrl, commonTodo)
}
} else {
await axios.delete(todoUrl)
}
}
for (const todo of newTodos) {
if (!existingTodos.find((_todo) => _todo.id === todo.id)) {
await axios.post(SERVER_URL, todo)
}
}
dispatch(
this.setMessage({ type: 'success', text: 'Todos has been saved' })
)
} catch (err) {
console.error(err.toJSON())
dispatch(
this.setMessage({
type: 'error',
text: 'Something went wrong. Try again later'
})
)
} finally {
dispatch(this.setStatus('idle'))
await giveMeSomeTime()
dispatch(this.setMessage({}))
}
}
})
// создатель селекторов
// принимает состояние
const createSelectors = (state) => ({
selectFilteredTodos() {
const { todos, filter } = state
if (filter === 'all') return todos
return filter === 'active'
? todos.filter((todo) => !todo.done)
: todos.filter((todo) => todo.done)
},
selectTodoStats() {
const { todos } = state
const { length } = todos
const completed = todos.filter((todo) => todo.done).length
const active = length - completed
const percent = length === 0 ? 0 : Math.round((active / length) * 100)
return {
total: length,
completed,
active,
percent
}
}
})
// начальное состояние
const initialState = {
todos: [],
status: 'idle',
message: {},
filter: 'all'
}
// контекcт
const Context = createContext()
// провайдер
export const Provider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState)
// операции меняться не будут, поэтому их можно мемоизировать
// "свежесть" изменяемого состояния обеспечивается диспетчером
const actions = useMemo(() => createActions(dispatch), [])
// селекторы должны повторно вычисляться при каждом изменении состояния
const selectors = createSelectors(state)
return (
<Context.Provider value={{ state, dispatch, actions, selectors }}>
{children}
</Context.Provider>
)
}
// хук для использования контекста
export const useAppContext = () => useContext(Context)
Я не буду приводить код компонентов, он похож на код, в котором используется Redux
, только намного короче. Мы просто извлекаем состояние, диспетчер, операции и селекторы в нужных местах с помощью хука useAppContext
.
Демо этого приложения можно посмотреть здесь. Оно также лежит в репозитории.
Для измерения производительности я применил ту же технику и получил такие результаты:
- в режиме для разработки
- запись — около
1
мс - обновление — около
10
мс
- запись — около
- в производственном режиме
- запись —
1
мс - обновление —
5-7
мс
- запись —
Таким образом, по сравнению с Vuex
мы получаем прирост производительности: для записи — в 4-5
раз, для обновления (внимание!) — в 100-150
раз.
Данный вариант хранилища является настолько производительным, что время записи 10000
задач также составляет в среднем 1
мс, не считая времени рендеринга. Что касается последнего, то при сравнении Vue
и React
, первый справляется с отрисовкой большого количества DOM-элементов немного лучше, но я бы не сказал, что разница является существенной.
Заключение
Мы с вами на практическом примере сравнили Redux
и Vuex
— два наиболее популярных решения для управления состоянием приложений, реализованных с помощью соответствующих фреймворков, и убедились, что Redux
— далеко не лучший выбор как с точки зрения простоты предоставляемого им интерфейса, так и с точки зрения производительности.
К счастью, современный React
предоставляет в наше распоряжение другой инструмент, который если не лучше, то уж точно не хуже Vuex
. Интерфейс этого инструмента можно сильно упростить, но это тема для отдельной статьи. Для себя я сделал такой вывод: нам больше не нужен Redux
для управления состоянием React-приложений.
Надеюсь, вы не зря потратили время и нашли для себя что-то интересное. Благодарю за внимание и хорошего дня!
Комментарии (12)
Spunreal
11.08.2021 10:10К счастью, современный
React
предоставляет в наше распоряжение другой инструмент, который если не лучше, то уж точно не хужеVuex
Как-то незакончено получается. Не хватает названия или ссылки на инструмент.
aio350 Автор
11.08.2021 16:13я имел ввиду useContext + useReducer) + если обернуть редуктор в produce() из immer (и вместо state работать с draft), то можно мутировать состояние напрямую
evgeniyPP
11.08.2021 12:44Не очень понятна практическая польза сравнения Redux и Vuex. Это как сравнивать, кто сильнее: акула или медведь) У них всё равно разный ареал обитания и заменить один на другой не получится.
untilx
11.08.2021 16:00Очень странное заявление. А как тогда у автора получилось заменить одно на другое?
Graff1406
11.08.2021 16:06Правильно подметил. Но все таки это говорит о том что vuex как инструмент практичнее.
DarthVictor
11.08.2021 14:31Попытавшись определить причину такой медлительности
Redux
, я пришел к выводу, что все дело в манипуляциях с данными, которые совершаетRedux
с целью их нормализации с помощью адаптера сущностей.Заменил в изначальном Redux варианте циклические вызовы добавления на множественное добавление https://codesandbox.io/s/redux-todo-forked-resdk и вышло 7ms на каждое из 25 созданий в среднем и 80мс на общее обновление. В дев режиме со всеми дефолтными мидлварами тулкита и прочим.
Так что дело точно не в EntityAdapter. Причем я специально не использовал из адаптера функции addMany и updateMany.
Vlad_Murashchenko
11.08.2021 15:51+3Я просмотрел статью очень вскользь. На мой взгляд в статье очень много неточностей, нюансов и не правильные выводы.
АрхитектураFlux
(вRedux
) предполагает следующее:
состояние изменяется только с помощью чистых функций — операций (actions);
actions - это не чистые функции, а обычные объекты. И функции которые создают события (action creators) так же не обязательно должны быть чистыми. Чистыми функциями обязаны быть только reducers и selectors. И в этом очень много смысла. Я могу расписывать часами то, насколько сильно чистые функции упрощают жизнь, если понимать их преимущества.
Наиболее показательные и стабильные результаты удалось получить с помощью записи 2500 задач и их последующего обновления.
И делается это с помощью вызова dispatch 2500 раз. Это абсолютно не соответсвует идеям redux и здравому смыслу. Когда нужно добавить или отредактировать 2500 элементов, это должно делаться с помощью одной тракзакции. Об этом написано в strongly recommended правилах в redux style-guide:
https://redux.js.org/style-guide/style-guide#avoid-dispatching-many-actions-sequentially
Попытавшись определить причину такой медлительностиRedux
, я пришел к выводу, что все дело в манипуляциях с данными, которые совершаетRedux
с целью их нормализации с помощью адаптера сущностей.
Я почти на 100% уверен, что проблема с производетельностью является производной нарушения правила выше и особенностями работы immer который redux-toolkit использует под коробкой. Дело в том, что immer работает очень медленно, если вызывать его в цикле как это делается в примерах статьи.
Из документации immer:for (let x of y) produce(base, d => d.push(x))
is exponentially slower thanproduce(base, d => { for (let x of y) d.push(x)})
В случае примеров статьи produce вызывается внутри dispatch который вызывается в цикле. Если бы изменение было тракзакционное, как должно быть, то этих проблем бы не возникло.
На практике проблемы производительности обычно вызывает не скорость обновления данных, а лишние перерендеры и согласования виртуального DOM. И как раз этот нюанс упускает эта идея сuseAppContext
. При изменении состояния все компоненты которые используютuseAppContext
внутри себя будут обновлены, это особенность работы контекста.useSelector
из react-redux работает таким образом, что если данные на которые ссылается селектор не изменились, компонент не обновлялся и команда react-redux приложила не мало усилий, чтобы сделать поведениеuseSelector
именно таким и исправить баги "Stale Props" и "Zombie Children".Alexandroppolus
11.08.2021 16:44команда react-redux приложила не мало усилий, чтобы ... исправить баги "Stale Props" и "Zombie Children".
насколько я знаю (где-то читал), те усилия в основном свелись к навешиванию try...catch вокруг вызова селектора.
Vlad_Murashchenko
11.08.2021 17:17Да, так они исправили баг и скорее всего вы читал это вот здесь в доке. Однако не только. Также есть Subscription который используется внутри useSelector чтобы родительские компоненты обновилесь раньше дочерних. Еще там есть сравнивание состояний с предыдущим и
forceUpdate
при их изменении для пропуска ненужных рендеров.
В общем, после чтения этого кода видно, что есть куча нюансов которые команда redux уже решили за нас. И я не вижу смысла повторять это все для того, чтобы перейти на useReducer + useContext. К тому же в этом подходе не закрыты еще 2 вопроса - асинхронность и интеграция с devtools. Они конечно разрешими, вот только зачем их решать самому если в итоге получится тот же redux?
Другое дело если бы был подход, который максимально сохраняет преимущества redux и добавляет много новых.Alexandroppolus
11.08.2021 17:28+2И я не вижу смысла повторять это все для того, чтобы перейти на useReducer + useContext.
Да это понятно. О том, что контекст нихрена не стейт-манагер, уже сказано миллион раз. Просто благодаря хукам в Реакте стало легко работать с контекстом, и сотни велосипедистов-стейтманагерщиков ломанулись в эту пропасть :)
Focushift
16.08.2021 01:30Таким образом, по сравнению с
Vuex
мы получаем прирост производительности: для записи — в4-5
раз, для обновления (внимание!) — в100-150
раз.в следующий раз, для корректного сравнения, еще добавьте систему реактивности, а то получается сравнение скорости библиотеки против голого нативного кода.
Alexandroppolus
Просто для чистоты эксперимента: если 2500 действий обернуть в batch(() => { ... }), будет быстрее? Оно ведь понятно, что из-за иммутабельности получается O(n^2), но объемы не те, чтобы только асимптотика так разваливала.
Edit и Regular обернуть в React.memo.
А так, да, автор Редукса тоже в нем разочаровался и признал, что Редукс - это медленно и невкусно. В фейсбуке, разумеется, его не используют.