Coaction 3.0: Zustand без селекторов и с самооптимизирующимися геттерами

#react#state-management#coaction#zustand

Когда пишешь на 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 не справляется

После недели тестов на реальном проекте я нашел несколько подводных камней:

  1. SSR: Прокси-логика плохо дружит с серверным рендерингом, приходится добавлять костыльные проверки на typeof window.
  2. Типизация: Геттеры с автоматической мемоизацией ломают вывод типов в сложных сценариях. Для TS-пуристов это может быть проблемой.
  3. Миграция: Если переезжаешь с Redux/Zustand, придется переписывать не только селекторы, но и часть логики под геттеры.

Кому стоит попробовать

Coaction выглядит как хороший вариант для новых проектов, где хочется минимального boilerplate без потери производительности. Особенно если в приложении есть:

Но если у вас уже настроен Zustand с codegen-селекторами или используется RTK Query, миграция может не окупиться. Лучше всего скачать пример, покрутить на песочнице и решить, стоит ли переписывать под это продакшен.


Источник: https://github.com/coactionjs/coaction