Когда пишешь на React уже седьмой год, начинаешь замечать, сколько рутинного кода приходится писать даже для простых стейт-менеджеров. Тот же Zustand — удобнее Redux, но селекторы, мемоизация и ручное управление зависимостями все равно остаются. Coaction 3.0 пытается решить это радикально: просто берешь данные из стора, а библиотека сама разбирается с оптимизациями.
Как Coaction убивает селекторы
Вот типичный zustand-код:
const useUser = create((set) => ({
user: null,
fetchUser: async (id) => set({ user: await api.getUser(id) })
}))
function Avatar() {
const user = useUser(store => store.user) // вот этот селектор
return <img src={user.avatar} />
}
В Coaction это выглядит так:
const store = coaction({
user: null,
async fetchUser(id) {
this.user = await api.getUser(id)
}
})
function Avatar() {
const { user } = store.use() // никаких селекторов
return <img src={user.avatar} />
}
Под капотом — прокси, которые отслеживают, какие поля реально используются в компоненте. При изменении user.address компонент Avatar не перерендерится, потому что он обращается только к user.avatar. Это похоже на то, как работают сигналы в SolidJS, но без необходимости явно оборачивать значения в createSignal.
Вычисляемые свойства без useMemo
Еще одна боль — производные состояния. В том же Zustand приходится либо заводить отдельные селекторы с мемоизацией, либо делать это на уровне компонента. Coaction решает это через геттеры:
const store = coaction({
todos: [],
get completedTodos() {
return this.todos.filter(todo => todo.completed)
}
})
// Где-то в компоненте:
const { completedTodos } = store.use()
Геттер completedTodos кешируется автоматически и пересчитывается только при изменении todos. Причем сама логика “грязных проверок” спрятана внутри библиотеки. На практике это означает, что можно не думать о зависимостях для useMemo — достаточно написать обычный геттер.
Web Workers без boilerplate
Самое неочевидное — Coaction умеет прокидывать действия в Web Workers. Допустим, у вас есть тяжелая сортировка:
const store = coaction({
data: [],
async sortData() {
this.data = await heavySort(this.data) // Блокирует UI
}
})
Достаточно добавить декоратор, чтобы метод запускался в воркере:
const store = coaction({
data: [],
@worker
async sortData() {
this.data = await heavySort(this.data) // Теперь не блокирует UI
}
})
Под капотом Coaction использует Comlink для работы с воркерами, но из кода это полностью убрано. Правда, есть ограничение — аргументы и возвращаемые значения должны быть сериализуемы.
Где Coaction не справляется
После недели тестов на реальном проекте я нашел несколько подводных камней:
- SSR: Прокси-логика плохо дружит с серверным рендерингом, приходится добавлять костыльные проверки на
typeof window. - Типизация: Геттеры с автоматической мемоизацией ломают вывод типов в сложных сценариях. Для TS-пуристов это может быть проблемой.
- Миграция: Если переезжаешь с Redux/Zustand, придется переписывать не только селекторы, но и часть логики под геттеры.
Кому стоит попробовать
Coaction выглядит как хороший вариант для новых проектов, где хочется минимального boilerplate без потери производительности. Особенно если в приложении есть:
- Много производных состояний (геттеры решают эту проблему элегантно)
- Тяжелые вычисления (воркеры из коробки)
- Частые перерисовки (автоматический tracking зависимостей)
Но если у вас уже настроен Zustand с codegen-селекторами или используется RTK Query, миграция может не окупиться. Лучше всего скачать пример, покрутить на песочнице и решить, стоит ли переписывать под это продакшен.
Источник: https://github.com/coactionjs/coaction