Hummbitv1.x

Матрица публичного API

hummbit

  • getState(): Readonly<RootState>
  • setState(input: RootState | NextStateUpdater<RootState>): Promise<void>
  • mergeState(input: Patch<RootState> | Updater<RootState>): Promise<void>
  • configureGlobalStore(config: GlobalStoreConfig): void
  • selector<T>(selectorFn: (state: Readonly<RootState>) => T): T
  • initStore(config, options?): InitializedStore<...>

hummbit/react

  • getState(): Readonly<RootState>
  • setState(input: RootState | NextStateUpdater): Promise<void>
  • mergeState(input: Patch | MergeUpdater): Promise<void>
  • useSelector<T>(selectorFn, equalityFn?): T
  • configureGlobalStore(config): void
  • initStore(config, options?): InitializedReactStore<...>

Справочник API


hummbit.getState

Одноразовое чтение иммутабельного snapshot из глобального singleton store.

Базовое чтение

import { getState } from "hummbit";

const snapshot = getState();
console.log(snapshot);

Чтение вложенного поля

import { getState } from "hummbit";

const userName = getState().user?.name ?? "anonymous";

Чтение в обработчике события

import { getState } from "hummbit";

function onCheckoutClick() {
  const cart = getState().cart;
  submitCheckout(cart);
}

Чтение после awaited обновления

import { setState, getState } from "hummbit";

await setState((prev) => ({ ...prev, ready: true }));
console.log(getState().ready); // true

Анти-паттерн и корректный подход

import { getState } from "hummbit";

// Анти-паттерн: считать, что значение всегда актуально
const cached = getState().counter;

// Правильно: читать getState() каждый раз при необходимости
const fresh = getState().counter;

hummbit.setState

Полная замена корневого state глобального store.

Прямая замена объектом

import { setState } from "hummbit";

await setState({ user: { id: "u1" }, ready: true });

Замена через updater

import { setState } from "hummbit";

await setState((prev) => ({ ...prev, ready: !prev.ready }));

Асинхронный updater

import { setState } from "hummbit";

await setState(async (prev) => {
  const profile = await loadProfile();
  return { ...prev, profile };
});

Порядок вызовов

import { setState } from "hummbit";

const p1 = setState((s) => ({ ...s, step: 1 }));
const p2 = setState((s) => ({ ...s, step: 2 }));
await Promise.all([p1, p2]); // применяется в порядке вызова

Анти-паттерн и корректный подход

import { setState } from "hummbit";

// Анти-паттерн: потерять остальные поля корня
await setState({ user: { id: "u2" } } as any);

// Правильно: сохранить root через spread
await setState((prev) => ({ ...prev, user: { ...prev.user, id: "u2" } }));

hummbit.mergeState

Поверхностный patch на уровне корня.

Root patch

import { mergeState } from "hummbit";

await mergeState({ loading: true });

Patch через updater

import { mergeState } from "hummbit";

await mergeState((prev) => ({ counter: prev.counter + 1 }));

Асинхронный patch

import { mergeState } from "hummbit";

await mergeState(async () => {
  const token = await refreshToken();
  return { token };
});

Статусы запроса

import { mergeState } from "hummbit";

await mergeState({ requestStatus: "pending" });
await mergeState({ requestStatus: "done" });

Анти-паттерн и корректный deep update

import { mergeState } from "hummbit";

// Анти-паттерн: перезаписать весь user
await mergeState({ user: { age: 31 } as any });

// Правильно:
await mergeState((prev) => ({ user: { ...prev.user, age: 31 } }));

hummbit.configureGlobalStore

Конфигурация глобального singleton для DevTools и middleware.

Выключить DevTools

import { configureGlobalStore } from "hummbit";

configureGlobalStore({ devtools: false });

Включить с кастомным именем

import { configureGlobalStore } from "hummbit";

configureGlobalStore({ devtools: { enabled: true, name: "app-global" } });

Добавить middleware

import { configureGlobalStore, type Middleware } from "hummbit";

type Root = { count: number };
const logger: Middleware<Root> = (ctx) => ({
  afterUpdate: () => console.log("count", ctx.getState().count),
});
configureGlobalStore({ middleware: [logger] });

Инициализация на старте

import { configureGlobalStore } from "hummbit";

export function bootstrap() {
  configureGlobalStore({ devtools: true });
}

Анти-паттерн и корректный подход

import { configureGlobalStore } from "hummbit";

// Анти-паттерн: поздняя пере-конфигурация
setTimeout(() => configureGlobalStore({ devtools: false }), 5000);

// Правильно: один вызов при bootstrap
configureGlobalStore({ devtools: false });

hummbit.selector

Одноразовый селектор для глобального state (не подписка).

Базовое чтение

import { selector } from "hummbit";

const isAuth = selector((s) => Boolean(s.session?.token));

Вычисляемое значение

import { selector } from "hummbit";

const total = selector((s) => s.items.reduce((acc, i) => acc + i.price, 0));

Optional chaining

import { selector } from "hummbit";

const city = selector((s) => s.user?.address?.city ?? "unknown");

Guard перед сайд-эффектом

import { selector } from "hummbit";

if (selector((s) => s.flags?.canUpload)) {
  startUpload();
}

Анти-паттерн и корректный подход

import { selector } from "hummbit";

// Анти-паттерн: воспринимать selector как реактивную подписку
const value = selector((s) => s.counter);

// Правильно: читать снова после обновлений или useSelector в React
const next = selector((s) => s.counter);

hummbit.initStore

Создаёт изолированный типизированный store с actions, selectors, select.

Минимальный store

import { initStore } from "hummbit";

const store = initStore({
  initialState: { count: 0 },
  actions: ({ actionCreator, setState }) => ({
    inc: actionCreator("inc", () =>
      setState((s) => ({ ...s, count: s.count + 1 })),
    ),
  }),
  selectors: { count: (s) => s.count },
});

Использование select(...)

const count = store.select(store.selectors.count);

freeze: false

import { initStore } from "hummbit";

const store = initStore({
  initialState: { debug: true },
  freeze: false,
  actions: () => ({}),
  selectors: { debug: (s) => s.debug },
});

Опции DevTools (второй аргумент initStore)

import { initStore } from "hummbit";

const store = initStore(
  {
    initialState: { q: 0 },
    devtools: { name: "query-store" },
    actions: () => ({}),
    selectors: { q: (s) => s.q },
  },
  { devtools: { hideSetState: true } },
);

Что важно понимать:

  • config.devtools (первый аргумент) отвечает за включение/выключение интеграции DevTools.
  • options.devtools (второй аргумент) отвечает за тонкую настройку логирования/имени.
  • Это разные уровни настройки и они работают вместе.

Полный список параметров второго аргумента:

type InitStoreOptions = {
  devtools?: {
    name?: string;
    hideSetState?: boolean;
    hideMergeState?: boolean;
  };
};

Расшифровка:

  • options.devtools.name — имя инстанса store в Redux DevTools.
    Приоритет выше, чем у config.devtools.name.
  • options.devtools.hideSetState — скрывает события setState в DevTools-логе (на поведение store не влияет).
  • options.devtools.hideMergeState — скрывает события mergeState в DevTools-логе (на поведение store не влияет).

Анти-паттерн и корректный подход

// Анти-паттерн: держать всю типизацию только в глобальном RootState
// Правильно: предпочитать локальные типы через initStore

hummbit/react.getState

Семантика как у hummbit.getState, но типы идут через публичный augmentable RootState.

Базовое чтение

import { getState } from "hummbit/react";

const st = getState();

One-off проверка для UI-команды

import { getState } from "hummbit/react";

function onOpenModal() {
  if (getState().permissions?.canOpen) openModal();
}

Read в аналитике

import { getState } from "hummbit/react";

track("page_open", { locale: getState().locale });

Await и затем read

import { setState, getState } from "hummbit/react";

await setState((s) => ({ ...s, hydrated: true }));
console.log(getState().hydrated);

Анти-паттерн и корректный подход

// Анти-паттерн: использовать getState() как механизм постоянного рендера
// Правильно: для реактивного UI использовать useSelector(...)

hummbit/react.setState

Полная замена root state из React entrypoint.

Переключение флага

import { setState } from "hummbit/react";

await setState((s) => ({ ...s, open: !s.open }));

Async интеграция с fetch

import { setState } from "hummbit/react";

await setState(async (s) => ({ ...s, profile: await fetchProfile() }));

Loading lifecycle

import { setState } from "hummbit/react";

await setState((s) => ({ ...s, loading: true }));
await setState((s) => ({ ...s, loading: false }));

Event callback update

import { setState } from "hummbit/react";

const onDismiss = () => setState((s) => ({ ...s, toast: null }));

Анти-паттерн и корректный подход

// Анти-паттерн: мутировать старый объект и возвращать его же
// Правильно: всегда возвращать новый root-объект

hummbit/react.mergeState

Поверхностный merge на уровне корня из React entrypoint.

Флаг загрузки

import { mergeState } from "hummbit/react";

await mergeState({ loading: true });

Updater patch

import { mergeState } from "hummbit/react";

await mergeState((s) => ({ counter: s.counter + 1 }));

Async patch

import { mergeState } from "hummbit/react";

await mergeState(async () => ({ token: await refreshToken() }));

Step update

import { mergeState } from "hummbit/react";

await mergeState({ step: 2 });

Анти-паттерн и корректный подход

// Анти-паттерн: ожидать deep merge
// Правильно: обновлять вложенные поля вручную через spread

hummbit/react.useSelector

Подписывает React-компонент на slice глобального singleton store.

Базовый вариант

import { useSelector } from "hummbit/react";

function Counter() {
  const value = useSelector((s) => s.counter);
  return <span>{value}</span>;
}

Кастомный equality

import { useSelector } from "hummbit/react";

const id = useSelector(
  (s) => s.user.id,
  (a, b) => a === b,
);

Derived selector

import { useSelector } from "hummbit/react";

const total = useSelector((s) => s.items.reduce((acc, i) => acc + i.qty, 0));

Разделение селекторов для perf

const name = useSelector((s) => s.user.name);
const age = useSelector((s) => s.user.age);

Анти-паттерн и корректный подход

// Анти-паттерн: всегда новый объект без equality
const userView = useSelector((s) => ({ id: s.user.id, name: s.user.name }));

// Правильно: добавить equality или выбирать примитивы отдельно

hummbit/react.configureGlobalStore

Та же функция конфигурации глобального store, экспортируется и в React entrypoint.

Выключить DevTools

import { configureGlobalStore } from "hummbit/react";

configureGlobalStore({ devtools: false });

Кастомное имя DevTools

configureGlobalStore({ devtools: { enabled: true, name: "react-global" } });

Middleware

import { configureGlobalStore, type Middleware } from "hummbit/react";

const mw: Middleware<any> = () => ({
  afterUpdate: () => console.log("updated"),
});
configureGlobalStore({ middleware: [mw] });

Bootstrap

export const setupStore = () => {
  configureGlobalStore({ devtools: true });
};

Анти-паттерн и корректный подход

// Анти-паттерн: пере-конфигурировать в компонентах на каждом рендере
// Правильно: вызывать один раз в bootstrap

hummbit/react.initStore

Создаёт instance-store и добавляет store.useSelector.

Базовый React store

import { initStore } from "hummbit/react";

export const store = initStore({
  initialState: { count: 0 },
  actions: ({ actionCreator, setState }) => ({
    inc: actionCreator("inc", () =>
      setState((s) => ({ ...s, count: s.count + 1 })),
    ),
  }),
  selectors: { count: (s) => s.count },
});

Потребление через store.useSelector

function Counter() {
  const count = store.useSelector(store.selectors.count);
  return <button onClick={() => store.actions.inc()}>{count}</button>;
}

Кастомный equality

const profile = store.useSelector(
  (s) => s.profile,
  (a, b) => a.id === b.id && a.version === b.version,
);

Async action flow

const asyncStore = initStore({
  initialState: { loading: false, data: null as null | string },
  actions: ({ actionCreator, setState }) => ({
    load: actionCreator("load", async () => {
      await setState((s) => ({ ...s, loading: true }));
      const data = await Promise.resolve("ok");
      await setState((s) => ({ ...s, loading: false, data }));
    }),
  }),
  selectors: { data: (s) => s.data, loading: (s) => s.loading },
});

Анти-паттерн и корректный подход

// Анти-паттерн: держать shared domain logic только в локальном component state
// Правильно: выносить shared updates/actions в initStore
Автор: Alexey TolkachevLinkedIn