Repository: angular
Issue: Signal for TransferState data
Labels: feature,area: core,area: server,core: reactivity,cross-cutting: signals

### Which @angular/* package(s) are relevant/related to the feature request?

core

### Description

When creating an app that uses Signals and TransferState, it was found that we had to do a fair amount of boilerplate for this to work with persisting the signal state to TransferState and then loading it again on the browser: 

- Create a state key with `makeStateKey` that requires a new constant with duplicated type and string key
- Inject TransferState
- Create a signal property
- Set the signal on the server once data is loaded
- Set the TransferState data keys for each signal
- Set the signal on the browser from TransferState data

It would be handy if there is a signal that wraps TransferState and can remove some of this boilerplate to just:

- Create a `transferStateSignal` property, giving it the key and initial value
- Set the signal on the server once data is loaded
- (Signal is automatically loaded on the browser from TransferState data)

No state key, TransferState injection, or setting/loading TransferState is needed.

### Proposed solution

Here is a quick example of a prototype of what this could look like:

```ts
import { inject, makeStateKey, signal, TransferState, WritableSignal } from "@angular/core";

export function transferStateSignal<T>(key: string, initialValue: T): WritableSignal<T> {
    const stateKey = makeStateKey<T>(key);
    const transferState = inject(TransferState);

    const value = transferState.hasKey(stateKey)
        ? transferState.get(stateKey, initialValue)
        : initialValue;

    const signalValue = signal(value);

    const signalProxy = new Proxy(signalValue, {
        // override `set` method of signalValue to set on TransferState
        get: (target, prop, receiver) => {
            if (prop === 'set') {
                return (newValue: T) => {
                    transferState.set(stateKey, newValue);
                    target.set(newValue);
                };
            }
            return Reflect.get(target, prop, receiver);
        }
    });

    return signalProxy;
}
```

Example usage:
```ts
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
import { ChangeDetectionStrategy, Component, computed, Inject, PLATFORM_ID } from '@angular/core';
import { transferStateSignal } from '../transferStateSignal';

@Component({
    selector: 'app-transfer-state-signal',
    standalone: true,
    imports: [],
    template: `
    <h1>TransferStateSignal Example</h1>
    <p>String: {{dataString()}}</p>
    <p>Number: {{dataNumber()}}</p>
    <p>Boolean: {{dataBoolean()}}</p>
    <p>Object.key: {{dataObject().key}}</p>
    <p>Computed: {{computedNumber()}}</p>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TransferStateSignalComponent {
    readonly dataString = transferStateSignal<string>('dataString', 'Hello, World!');
    readonly dataNumber = transferStateSignal<number>('dataNumber', 42);
    readonly dataBoolean = transferStateSignal<boolean>('dataBoolean', true);
    readonly dataObject = transferStateSignal<{ key: string }>('dataObject', { key: 'value' });

    readonly computedNumber = computed<number>(() => this.dataNumber() * 100);

    constructor(
        @Inject(PLATFORM_ID) private readonly platformId: Object
    ) { }

    ngOnInit(): void {
        if (isPlatformServer(this.platformId)) {
            // pretend we load data on the server here, then set in signals for SSR,
            // and no need to set on TransferState as it's done in transferStateSignal
            this.dataString.set('Hello, Server!');
            this.dataNumber.set(24);
            this.dataBoolean.set(false);
            this.dataObject.set({ key: 'server' });
        } else if (isPlatformBrowser(this.platformId)) {
            setInterval(() => {
                this.dataNumber.set(this.dataNumber() + 1);
            }, 1000);
        }
    }
}
```

Compare this to what is required to do this without `transferStateSignal`:

```ts
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
import { ChangeDetectionStrategy, Component, computed, Inject, makeStateKey, PLATFORM_ID, signal, TransferState } from '@angular/core';

// duplicated effort of having to make a state key for each and ensure data types match
const dataStringKey = makeStateKey<string>('dataStringCurrent');
const dataNumberKey = makeStateKey<number>('dataNumberCurrent');
const dataBooleanKey = makeStateKey<boolean>('dataBooleanCurrent');
const dataObjectKey = makeStateKey<{ key: string }>('dataObjectCurrent');

@Component({
    selector: 'app-current',
    standalone: true,
    imports: [],
    template: `
    <h1>Current Example</h1>
    <p>String: {{dataString()}}</p>
    <p>Number: {{dataNumber()}}</p>
    <p>Boolean: {{dataBoolean()}}</p>
    <p>Object.key: {{dataObject().key}}</p>
    <p>Computed: {{computedNumber()}}</p>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CurrentComponent {
    readonly dataString = signal<string>('Hello, World!');
    readonly dataNumber = signal<number>(42);
    readonly dataBoolean = signal<boolean>(true);
    readonly dataObject = signal<{ key: string }>({ key: 'value' });

    readonly computedNumber = computed<number>(() => this.dataNumber() * 100);

    constructor(
        @Inject(PLATFORM_ID) private readonly platformId: Object,
        private readonly transferState: TransferState, // extra import
    ) { }

    ngOnInit(): void {
        if (isPlatformServer(this.platformId)) {
            // pretend we load data on the server here, then set in signals for SSR
            this.dataString.set('Hello, Server!');
            this.dataNumber.set(24);
            this.dataBoolean.set(false);
            this.dataObject.set({ key: 'server' });

            // duplicated effort of setting on TransferState
            this.transferState.set(dataStringKey, this.dataString());
            this.transferState.set(dataNumberKey, this.dataNumber());
            this.transferState.set(dataBooleanKey, this.dataBoolean());
            this.transferState.set(dataObjectKey, this.dataObject());
        } else if (isPlatformBrowser(this.platformId)) {
            // duplicated effort of getting from TransferState
            this.dataString.set(this.transferState.get(dataStringKey, this.dataString()));
            this.dataNumber.set(this.transferState.get(dataNumberKey, this.dataNumber()));
            this.dataBoolean.set(this.transferState.get(dataBooleanKey, this.dataBoolean()));
            this.dataObject.set(this.transferState.get(dataObjectKey, this.dataObject()));

            setInterval(() => {
                this.dataNumber.set(this.dataNumber() + 1);
            }, 1000);
        }
    }
}
```

### Alternatives considered

Will consider creating this as a third-party OSS library if the Angular team is not interested in adding something like this to the framework.