SSR и тесты
Поскольку под капотом query — обычный effector, fork() + allSettled() работают как обычно — без специальных тестовых утилит.
Тестирование запроса
import { fork, allSettled } from 'effector';
const scope = fork();
await allSettled(query.start, { scope, params: 1 });
expect(scope.getState(query.$data)).toEqual(/* ... */);SSR
const scope = fork();
await allSettled(query.start, { scope, params: req.params });
const html = renderToString(/* app */, scope);
const serialized = serialize(scope); // effector serialize — $data / $status / …Биндинги учитывают scope: React через <Provider value={scope}>, Vue через EffectorScopePlugin({ scope }).
Изоляция кэша на запрос ($queryCache)
По умолчанию адаптер кэша у query модульный — общий для всех scope. Для мультитенантного SSR задайте $queryCache на форк: каждый query этого scope читает/пишет изолированный адаптер, и параллельные запросы не видят данных друг друга:
import { $queryCache, inMemoryCache, dehydrate, hydrate } from 'effector-refetch';
// сервер — один адаптер на HTTP-запрос
const cache = inMemoryCache();
const scope = fork({ values: [[$queryCache, cache]] });
await allSettled(todosQuery.start, { scope });
const payload = { values: serialize(scope), cache: dehydrate(cache) };
// клиент
const clientCache = inMemoryCache();
hydrate(clientCache, payload.cache); // storedAt сохраняется → staleAfter стареет корректно
const clientScope = fork({ values: payload.values }); // $data восстановлен — без мигания загрузки
await allSettled($queryCache, { scope: clientScope, params: clientCache }); // сторы вызываемы
// закэшированные ключи теперь дают хит вместо перезапросаEffector babel/SWC-плагин не нужен ни для одного из слоёв. Публичные сториз ($data / $status / $error / $params / …) несут явные стабильные sid (er/<name>/$data), поэтому serialize(scope) подхватывает их, хотя библиотека поставляется собранной (плагины бандлеров не обрабатывают node_modules); внутренние сториз помечены serialize: 'ignore'. Записи кэша неймспейсятся так же: name ?? sid эффекта ?? счётчик создания. Давайте query стабильные name, если порядок инициализации модулей на сервере и клиенте может отличаться (и sid, и неймспейсы кэша следуют за именем). $queryCache автоматически исключён из serialize(scope).
Дегидрируются только адаптеры, умеющие перечислять записи (например inMemoryCache); web-storage адаптеры персистят себя сами. Без $queryCache всё работает как раньше — используется адаптер самого query (нормально для одно-клиентского приложения). Барьеры остаются глобальными по дизайну.
Персист на клиенте
Два взаимодополняющих способа пережить перезагрузку в браузере:
Слой кэша — используйте
localStorageCache/sessionStorageCacheкак адаптер; кэш запроса переживает перезагрузку (аversionинвалидирует старые данные).Слой стора — персистите
$dataнапрямую черезeffector-storage:tsimport { persist } from 'effector-storage/local'; persist({ store: todosQuery.$data as StoreWritable<Todo[] | null>, key: 'todos:data' });(
$dataв публичном типе read-only, но писабельный в рантайме — кастуйте дляpersist.)
Полный рабочий поток: examples/ssr.ts.
Заметки
- Sourced-конфиг (
Storeдляconcurrency/retry.times/cache.staleAfter/enabled) fork-корректен — каждый scope видит своё значение. - Изоляция кэша для SSR —
$queryCacheна форк (выше). Без него адаптеры кэша держат модульное состояние, общее для всех scope. - In-flight
AbortController-ы отслеживаются на инстанс query; не шарьте один инстанс между конкурентными SSR-запросами, если ещё и вызываетеcancel.