Store Features
Sync store state using Angular's TransferState API for SSR hydration
Kind: global function
Use Case: When using Server-Side Rendering (SSR), this custom store feature enables state to be serialized on the server and automatically restored on the client during hydration. This prevents unnecessary API calls and ensures a seamless transition from server-rendered to client-side rendered content.
Import the withServerStateTransfer custom store feature from @ngrx-traits/signals.
import { withServerStateTransfer } from '@ngrx-traits/signals';
To avoid unnecessary backend calls after state restoration, ensure you add withServerStateTransfer after state declarations but before any initialization calls that fetch data from the backend. This allows you to check if data is already loaded before making API calls.
// ✅ Correct order
signalStore(
withEntities({ entity }),
withCallStatus({ prop: 'products', initialValue: 'loading' }),
// Add withServerStateTransfer BEFORE loading calls
withServerStateTransfer({ key: 'my-state' }),
// Loading call comes LAST
withEntitiesLoadingCall({
fetchEntities: () => inject(Service).getProducts()
})
)
// ❌ Incorrect order - will make unnecessary backend call
signalStore(
withEntities({ entity }),
withCallStatus({ prop: 'products', initialValue: 'loading' }),
// Loading call runs first
withEntitiesLoadingCall({
fetchEntities: () => inject(Service).getProducts()
}),
// State transfer restores too late
withServerStateTransfer({ key: 'my-state' })
)
const store = signalStore(
withEntities({ entity, collection }),
withCallStatus({ prop: collection, initialValue: 'loading' }),
withServerStateTransfer({
key: 'my-state',
}),
);
Use filterState when you only want to transfer a subset of your state:
const store = signalStore(
withEntities({ entity, collection }),
withCallStatus({ prop: collection, initialValue: 'loading' }),
withServerStateTransfer({
key: 'products-state',
// Only transfer entity data, not loading states
filterState: ({ orderItemsEntityMap, orderItemsIds }) => ({
orderItemsEntityMap,
orderItemsIds,
}),
}),
);
Use valueMapper when you need custom serialization/deserialization logic:
const store = signalStore(
withState({
userProfile: {
userName: '',
email: '',
preferences: { theme: 'light', notifications: true },
tempData: null,
}
}),
withServerStateTransfer({
key: 'user-profile',
// Only transfer userName and email
valueMapper: (store) => ({
stateToTransferValue: () => ({
userName: store.userProfile().userName,
email: store.userProfile().email,
}),
transferValueToState: (savedData) => {
patchState(store, {
userProfile: {
...store.userProfile(),
userName: savedData.userName,
email: savedData.email,
}
});
},
}),
}),
);
Execute custom logic after state restoration from server:
const store = signalStore(
withEntities({ entity, collection }),
withCallStatus({ prop: collection }),
withServerStateTransfer({
key: 'products-state',
filterState: ({ productsEntityMap, productsIds }) => ({
productsEntityMap,
productsIds,
}),
onRestore: (store) => {
// Custom logic after restoration, e.g., set loaded status
patchState(store, { productsCallStatus: 'loaded' });
},
}),
);
Real-world example combining entities, filtering, pagination, sorting, and SSR state transfer:
const productsEntityConfig = entityConfig({
entity: type<Product>(),
collection: 'products',
});
export const ProductsSSRStore = signalStore(
{ providedIn: 'root' },
// 1. State declarations first
withEntities(productsEntityConfig),
withCallStatus({ ...productsEntityConfig, initialValue: 'loading' }),
// 2. Feature configurations
withEntitiesLocalPagination({
...productsEntityConfig,
pageSize: 5,
}),
withEntitiesLocalFilter({
...productsEntityConfig,
defaultFilter: { search: '' },
filterFn: (entity, filter) =>
!filter?.search ||
entity?.name.toLowerCase().includes(filter?.search.toLowerCase()),
}),
withEntitiesLocalSort({
...productsEntityConfig,
defaultSort: { field: 'name', direction: 'asc' },
}),
withEntitiesSingleSelection(productsEntityConfig),
// 3. Sync to URL query params
withEntitiesSyncToRouteQueryParams(productsEntityConfig),
// 4. Transfer state from server to client (BEFORE loading call)
withServerStateTransfer({
key: 'product-list-ssr',
}),
// 5. Loading call LAST - when state comes from server
// overrides status to loaded so fetchEntities wont be trigger
withEntitiesLoadingCall({
...productsEntityConfig,
fetchEntities: () => {
return inject(ProductService)
.getProducts()
.pipe(map((d) => d.resultList));
},
}),
);
This order ensures:
withEntitiesLoadingCall sees data is already loaded and skips the API callOn the Server:
On the Client:
patchState (or custom mapper)onRestore callback is executed if providedThis custom store feature receives an object to allow specific configurations:
| Property | Description | Value |
|---|---|---|
| key | Unique key to store state in TransferState | string |
| filterState | Filter state before transferring (mutually exclusive with valueMapper) | (state) => Partial<State> |
| valueMapper | Custom transformation between store and transfer value (mutually exclusive with filterState) | TransferValueMapper<T, Store> |
| onRestore | Callback executed after state restoration on client | (store) => void |
The valueMapper is a factory function that receives the store and returns an object with two methods:
valueMapper: (store: Store) => {
stateToTransferValue: () => T | undefined | null;
transferValueToState: (value: T) => void;
}
| Property | Description | Type |
|---|---|---|
| stateToTransferValue | Transform store state to transfer value (server) | () => T | undefined | null |
| transferValueToState | Transform transfer value to state (client) | (value: T) => void |
Note: valueMapper and filterState are mutually exclusive - use one or the other, not both.
No extra state generated
No extra computed generated
No extra methods generated