In Angular SSR, how do you use TransferState and makeStateKey to carry data that did not come through HttpClient from the server to the browser?
answer
- typed key, root-provided store
- set on server, read in browser
- JSON only, lossy for classes
- visible in page source
basics
~20 sCreate a typed key with makeStateKey, inject TransferState, set the value during the server render, and in the browser read it with get or hasKey before recomputing. Values travel as JSON inside the page, so keep them small, plain and non-secret.
solid answer
~40 s`TransferState` from `@angular/core` is a root-provided key-value store that Angular serializes into the HTML at the end of the server render and restores in the browser. You create keys with `makeStateKey<T>('name')`, which is just a typed string. The usual pattern: check `hasKey(KEY)`; if the value is there, use it (you are hydrating in the browser); otherwise compute it and `set(KEY, value)` so the server's result is embedded. `onSerialize(KEY, fn)` computes a value lazily at serialization time. Serialization is `JSON.stringify`, so `Date` becomes a string, `Map` and class instances lose their shape, and everything is readable in the page source. Keys share one app-wide namespace, so prefix them. `HttpClient` responses already use this store through the transfer cache, so `TransferState` is for data from other sources.
go deeper
Know that TransferState moves values from the server render into the browser and that keys come from makeStateKey.
Explain the hasKey, get and set pattern, JSON serialization limits, and why HttpClient data does not need it.
Keep the payload small and public-safe, namespace keys, and make sure the value is set before the render is serialized.
Decide which server-computed data is worth shipping inside HTML versus refetching, weighing page weight and exposure.
## What TransferState is `TransferState` is a small **key-value store** in `@angular/core` that crosses the server-to-browser boundary in an Angular SSR app. It is `providedIn: 'root'`, so every injection in one application instance sees the same store. On the server, Angular serializes the store into the HTML before sending it. In the browser, the store is filled from that HTML the first time something injects it. The built-in **HTTP transfer cache** is itself a user of `TransferState`: it writes `HttpClient` responses there under hashed keys. You reach for `TransferState` directly when the data did **not** come from `HttpClient`: - a third-party SDK with its own network layer; - a value computed on the server, such as a slow text transformation; - data read from a server-only source during the render. ## The API | Member | Purpose | |---|---| | `makeStateKey<T>(name)` | Creates a `StateKey<T>`, a typed string key | | `set(key, value)` | Stores a value | | `get(key, defaultValue)` | Returns the value, or the default if the key is absent | | `hasKey(key)` | Tells whether the key exists | | `remove(key)` | Deletes an entry | | `onSerialize(key, fn)` | Calls `fn` at serialization time and stores its result | | `isEmpty` | True when nothing is stored | ## The usual pattern 1. Declare a key once, with a namespaced name: `makeStateKey<Settings>('site.settings')`. 2. In the service, inject `TransferState` and check `hasKey`. 3. If the key is present, use the stored value. That branch runs in the browser after a server render. 4. If it is absent, load the value, then `set` it, so the server's result goes into the page. 5. Optionally `remove` the key after reading it, so a later call in the browser loads fresh data instead of reusing old server data. ```ts import { Injectable, TransferState, inject, makeStateKey } from '@angular/core'; interface Settings { edition: string; breakingBanner: boolean; } const SETTINGS = makeStateKey<Settings>('site.settings'); @Injectable({ providedIn: 'root' }) export class SiteSettings { private state = inject(TransferState); async load(fetchFromSdk: () => Promise<Settings>): Promise<Settings> { if (this.state.hasKey(SETTINGS)) { const cached = this.state.get<Settings | null>(SETTINGS, null); this.state.remove(SETTINGS); if (cached) return cached; } const fresh = await fetchFromSdk(); this.state.set(SETTINGS, fresh); return fresh; } } ``` In the browser the `set` in the last branch is harmless: nothing serializes the store there. ## Serialization rules and their consequences The store is written with `JSON.stringify` into a `<script type="application/json">` tag whose id is the app id plus `-state` (`ng-state` by default). Angular escapes `<` and `/` so the payload cannot close the tag early. Consequences: - **Only JSON survives.** A `Date` comes back as a string, a `Map` or `Set` as `{}`, a class instance as a plain object without methods, and `undefined` properties disappear. - **Everything is public.** Any visitor can read the script tag, so never put tokens, internal ids or another user's data there. - **Size is page weight.** The JSON is inside the HTML document, so a large payload slows the first byte-to-paint path for every visitor. - **Timing matters.** Serialization happens when the server render is finished and the app is stable. A value set after that moment is lost, so asynchronous work must be tracked by Angular for the render to wait for it. ## TransferState versus the HTTP transfer cache | Aspect | HTTP transfer cache | Your own `TransferState` code | |---|---|---| | Data source | `HttpClient` requests only | Anything you can serialize to JSON | | Keys | Hashed automatically from the request | Named by you with `makeStateKey()` | | Configuration | `withHttpTransferCacheOptions()` | Your service logic | | Cut-off | Stops when the app is stable | Stays readable until you `remove` it | Both write into the same store and the same script tag, so their payloads add up in the page. ## Common mistakes - Using the same key string in two features, so one silently overwrites the other. - Storing rich objects and then calling methods on what comes back in the browser. - Wrapping `HttpClient` calls in manual `TransferState` code, duplicating what the transfer cache already does. - Assuming the value exists in a client-only render. Without a server render the key is absent, so the fallback path must always work. ## History Before v18 these APIs were also exported from `@angular/platform-browser`, and a `ServerTransferStateModule` existed; both were removed in v18, and `TransferState`, `makeStateKey` and `StateKey` come from `@angular/core`.
- Why does a Date stored in TransferState come back as a string in the browser?The store is serialized with `JSON.stringify` and restored with `JSON.parse`, and JSON has no date type, so the `Date` is written as an ISO string and read back as that string. Store the ISO string or a timestamp deliberately and convert it when reading.
- When would you use onSerialize instead of set?When the value is only final at the end of the render, such as an accumulated list or a store snapshot. `onSerialize(key, fn)` runs `fn` just before the state is written into the HTML, so you capture the final value without calling `set` after every change. An exception in the callback is logged as a warning instead of failing the render.
saying these in an interview costs you the question
- TransferState keeps class instances intact, methods included.
- Data in TransferState is hidden from users because it is not rendered.
- Every HttpClient call needs manual TransferState code to avoid refetching.
- makeStateKey creates a unique symbol, so key names cannot collide.
- The key is always present in the browser, even without a server render.