Remix runs loaders and actions on the server for every navigation, then hydrates the same component tree in the browser. BlaC’s job is to make sure the server-rendered HTML and the client’s initial state match, preventing hydration mismatches.
Remix loaders return data that the server uses to render HTML. After the page reaches the browser, React hydrates the existing HTML by running the component tree again. If a bloc’s initial state on the client differs from what the server rendered, React reports a hydration mismatch.
The fix is to seed each bloc’s initial state from the loader data via init(args) so both renders start from the same snapshot.
A Remix loader is a server function — do not call registry functions there. Instead, return the data as JSON, receive it in the component, and forward it to useBloc as args:
Cubit<S> is a StateContainer<S> with emit / patch exposed as
public mutation surface. Today it adds nothing structurally beyond
StateContainer — both are inherited from the underlying
StructuralContainer<S>. Kept as a real class (not a type alias) because
downstream code does instance instanceof Cubit checks (see A2 audit).
The class body is intentionally empty: a no-op emit override would
still go through applyState, and patch is inherited from
StructuralContainer (path-diffed, microtask-flushed). If a caller
needs the old "skip if no real change" patch semantics, they can wrap
patch themselves or call emit after a manual equality check.
Cubit } from'@blac/core';
// Type stubs — replace with your actual loader return type
declarefunction
functionjson<T>(data:T): {
data:T;
}
json<
function(typeparameter)Tinjson<T>(data:T): {
data:T;
}
T>(
data: T
data:
function(typeparameter)Tinjson<T>(data:T): {
data:T;
}
T): {
data: T
data:
function(typeparameter)Tinjson<T>(data:T): {
data:T;
}
T };
declarefunction
functionfetchProduct(id:string):Promise<{
id:string;
title:string;
price:number;
}>
fetchProduct(
id: string
id:string,
):
interface Promise<T>
Represents the completion of an asynchronous operation
Promise<{
id: string
id:string;
title: string
title:string;
price: number
price:number }>;
type
type LoaderArgs = {
params: {
id:string;
};
}
LoaderArgs= {
params: {
id: string;
}
params: {
id: string
id:string } };
exportasyncfunction
functionloader({ params }:LoaderArgs):Promise<{
data: {
product: {
id:string;
title:string;
price:number;
};
};
}>
loader({
params: {
id: string;
}
params }:
type LoaderArgs = {
params: {
id:string;
};
}
LoaderArgs) {
const
const product: {
id:string;
title:string;
price:number;
}
product = await
functionfetchProduct(id:string):Promise<{
id:string;
title:string;
price:number;
}>
fetchProduct(
params: {
id: string;
}
params.
id: string
id);
return
functionjson<{
product: {
id:string;
title:string;
price:number;
};
}>(data: {
product: {
id:string;
title:string;
price:number;
};
}): {
data: {
product: {
id:string;
title:string;
price:number;
};
};
}
json({
product: {
id: string;
title: string;
price: number;
}
product });
}
// app/routes/product.$id.tsx — component (runs on server and client)
<buttononClick={cubit.addToCart}>Add to cart</button>
</div>
);
}
init(args) runs synchronously before the first state snapshot, so the bloc’s initial state on the server and the client are identical. React hydrates without a mismatch.
Actions are Remix’s server-side mutation path. After an action, Remix re-runs the loader and re-renders. Blocs handle optimistic UI and local state changes; the loader refetch propagates server state back:
If you call registry functions inside Remix resource routes or utility modules that run in the Node process, wrap them in withRequestRegistry so state does not bleed between concurrent requests. See SSR & per-request isolation for the full pattern.
Install plugins in root.tsx so they run once in the browser. Guard with typeof window !== 'undefined' to prevent plugin code from running in the server-side bundle:
The IndexedDB persistence plugin checks typeof indexedDB !== 'undefined' inside isAvailable() and silently disables itself when unavailable, but the guard above makes the intent explicit and avoids unnecessary code in the server bundle.