Server-Side Rendering β
SSR works with a ViewModelStore and normal client hydration. The rule that matters: the first server HTML and the first client render must match (same props/payload, same provider tree).
Below is a Next.js (Pages Router) checklist. A working layout lives in examples/ssr-nextjs. For the App Router, keep the same idea: load data on the server, pass serializable props into a client subtree that uses ViewModelsProvider and your hooks/HOCs.
Client components only
withViewModel, useCreateViewModel, and useViewModel use React hooks. They belong in client components, not in Server Components or "use server" modules.
1. next.config β
reactStrictMode: falseβ in dev, React Strict Mode double-mounts components. This library tiesattach/detachto mount and layout effects, so the extra cycle can surface bugs (wrong VM instances or counts). Turn Strict Mode off while debugging SSR if you see that.
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
reactStrictMode: false,
};
export default nextConfig;2. MobX on the server: enableStaticRendering β
On the server, observer should not run reactions like in the browser. Run once before rendering (e.g. import at the top of client _app):
import { configure } from 'mobx';
import { enableStaticRendering } from 'mobx-react-lite';
configure({ enforceActions: 'always' });
enableStaticRendering(typeof window === 'undefined');Example: examples/ssr-nextjs/src/bootstrap/client.ts.
App Router vs Pages Router
The sample app uses the Pages router and imports this bootstrap from a client _app. With the App router, put configure / enableStaticRendering in a small module marked with 'use client' and import it from your client root layout (or another client entry), so the same code runs during SSR and in the browser.
3. Root store + ViewModelsProvider β
Put a ViewModelStoreBase (or custom store) on your app root. If every VM needs rootStore, override createViewModel β see Integration with RootStore.
Wrap the tree with your context and ViewModelsProvider:
import { ViewModelsProvider } from 'mobx-view-model-react';
export function RootStoreProvider({ store, children }: { store: RootStore; children: React.ReactNode }) {
return (
<RootStoreContext.Provider value={store}>
<ViewModelsProvider value={store.viewModels}>{children}</ViewModelsProvider>
</RootStoreContext.Provider>
);
}Example: examples/ssr-nextjs/src/stores/root-store/, examples/ssr-nextjs/src/shared/lib/vm-store.ts.
4. _app, one RootStore, and page data β
getServerSideProps cannot live on _app. Typical split:
Snapshot β JSON-safe fields you pass from the server (e.g. appInfo). Optional fields are fine: static pages like /404 may not send a snapshot; your domain stores can fall back to defaults.
withRootStoreProps β wraps each pageβs getServerSideProps and adds rootStoreSnapshot (built on the server, e.g. getRootStoreSnapshot() in the example).
withRootStore (around _app) β holds one root store for the lifetime of that load and merges the server snapshot with any client-only fields your root store needs. In the example repo, that includes router from useRouter() β not a framework requirement, just one way to expose the Pages Router to the store.
Example files: pages/_app.tsx, with-root-store.tsx, with-root-store-props.ts, snapshot.ts, app-info-store/snapshot.ts (under examples/ssr-nextjs/src/).
5. Page: getServerSideProps + props into client UI β
export const getServerSideProps = withRootStoreProps(async () => ({
props: {
initialPayload: await loadPagePayload(),
},
}));The page module can stay without 'use client'; pass initialPayload into a client component that uses withViewModel / hooks.
6. Client screen: withViewModel, id, fallback, observer β
'use client'where you use the libraryβs hooks/HOCs.- Pass the same
payload(or props) on server and client. - Use a stable
idper route if several pages share one VM class β avoids collisions in the store. - If
mount()is async, setfallbackso the first server and client paint match. - Deep children:
useViewModel+observer.
Why the first paint can match: during the server HTML pass, React does not run useLayoutEffect / useEffect. useCreateViewModel (and withViewModel, which uses it) calls attach() during render as void store.attach(...) β the promise is not awaited. When the VMβs mount() finishes synchronously, the store completes attach in the same turn, isMounted becomes true, and the main view can render immediately. If mount() returns a Promise, the render pass continues without waiting; the VM stays in mountingViews until it settles, isAbleToRenderView is false in the meantime, and you should use fallback so server and client output agree. Custom ViewModelStore implementations should keep the same semantics as ViewModelStoreBase for attach(viewModel).
Minimal pattern (any SSR stack) β
Same store + same payload on server and client:
import { ViewModelBase, ViewModelStoreBase } from 'mobx-view-model';
import { ViewModelsProvider, withViewModel } from 'mobx-view-model-react';
class PageVM extends ViewModelBase<{ count: number }> {}
const Page = withViewModel(
PageVM,
({ model }) => <div>{`count ${model.payload.count}`}</div>,
);
export function renderPage(count: number) {
const vmStore = new ViewModelStoreBase();
return (
<ViewModelsProvider value={vmStore}>
<Page payload={{ count }} />
</ViewModelsProvider>
);
}Hydration on the client (same wiring β use the same Page component and the same payload values as on the server so the tree matches):
import { ViewModelStoreBase } from 'mobx-view-model';
import { ViewModelsProvider } from 'mobx-view-model-react';
import { hydrateRoot } from 'react-dom/client';
// import { Page } from './page';
const vmStore = new ViewModelStoreBase();
hydrateRoot(
document.getElementById('app')!,
<ViewModelsProvider value={vmStore}>
<Page payload={{ count: 1 }} />
</ViewModelsProvider>,
);Async mount() β
Use fallback for the initial render on server and client:
import { sleep } from "yummies/async";
class PageVM extends ViewModelBase {
async mount() {
await sleep(100);
super.mount();
}
}Pass a fallback component in withViewModel config (or set viewModelsConfig.fallbackComponent) so both server and client render that UI until mount() completes.
Same data everywhere
Reuse the same payload and the same ViewModelsProvider / store wiring on server and client. Do not depend on mount() side effects for the first paint.
