SSR & testing
Because a query is plain effector under the hood, fork() + allSettled() work as usual — no special test utilities.
Testing a query
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 / …Bindings are scope-aware: React via <Provider value={scope}>, Vue via the EffectorScopePlugin({ scope }).
Transferring the cache (dehydrate / hydrate)
serialize(scope) captures the store state, but the query cache (dedupe / staleAfter) lives outside the scope, so it isn't included. dehydrate snapshots it; hydrate restores it on the client — so cached params hit instead of refetching:
// server — alongside serialize(scope)
const cache = inMemoryCache();
const todos = createQuery({ effect: fetchTodosFx, cache: { adapter: cache } });
// … run queries under the scope …
const payload = { values: serialize(scope), cache: dehydrate(cache) };
// client
hydrate(cache, payload.cache); // warm the cache (storedAt preserved → staleAfter ages correctly)
const scope = fork({ values: payload.values }); // $data restored — no loading flashOnly adapters that can enumerate entries (e.g. inMemoryCache) are dehydratable; web-storage adapters already persist themselves.
Persisting on the client
Two complementary ways to keep data across reloads in the browser:
Cache layer — use
localStorageCache/sessionStorageCacheas the adapter; the query cache survives reloads (andversionlets you invalidate old data).Store layer — persist
$datadirectly witheffector-storage:tsimport { persist } from 'effector-storage/local'; persist({ store: todosQuery.$data as StoreWritable<Todo[] | null>, key: 'todos:data' });(
$datais read-only in the public type but writable at runtime — cast forpersist.)
Full runnable flow: examples/ssr.ts.
Notes
- Sourced config (
Storeforconcurrency/retry.times/cache.staleAfter/enabled) is fork-correct — each scope sees its own value. - Cache adapters hold state outside the effector scope; for isolated SSR build queries per request (as usual), or pass a fresh adapter.
- In-flight
AbortControllers are tracked per query instance; avoid sharing one query instance across concurrent SSR requests if you also callcancel.