Add linkedSignalState
Which @ngrx/* package(s) are relevant/related to the feature request?
signals
Information
Currently signal state is a strong utility to handle state even outside signal store. The missing part is allowing that state to be a linked signal.
I propose a new linkedSignalState which will allow deriving the state based on another signal.
This will allow resetting the state on signal change - or any other state transformation based on some other signal change.
Describe any alternatives/workarounds you're currently using
Currently either don't use signal state or use an effect to update the state.
I would be willing to submit a PR to fix this issue
- [X] Yes
- [ ] No
An example:
const a = signal(2);
// The state is based on another signal (linked)
const state = linkedSignalState(() => ({value: a() + 1}));
// or
const state = linkedSignalState({
source: a,
computation: (source) => ({value: source + 1})
});
patchState(state, {value: 5}); // Patch state like any other state
console.log(state.value()); // Deep signal
Thank you, @Harpush. Rest assured, we are fully aware of the need for this and are actively discussing various strategies to address it. There are also multiple scenarios where linkedSignal could come into play, and we’re taking those into consideration.
We’re not rushing this—our goal is to ensure we get it right the first time. 😊
An alternative to keep the store in sync based on an component signal is to use an rxMethod
export interface MyState {
prop1: Prop1 | null;
prop2: Prop2 | null;
}
export const MySignalStore = signalStore(
{ providedIn: 'root' },
withState<MyState>({
prop1: null,
prop2: null
}),
withMethods(store => ({
syncProp1: rxMethod<Prop1 | null>(
pipe(tap(prop1 => patchState(store, { prop1 })))
)
}))
);
then in the component you can pass the signal itself to the store method to keep in in sync based every time the prop1 component signal changes.
prop1 = computed(() => {
/// return prop1 value
});
_ = this.#mySignalStore.syncProp1(this.prop1);
Hello, withLinkedState is now available - but still no low level linkedSignalState. Is it something you want to support?
Oh, didn't see that this issue is still open. @Harpush, I don't really see the benefit of providing a linkedSignalState. We can wait for the rest of the team says and then proceed or close this issue.
@markostanimirovic @timdeschryver
@rainerhahnekamp I can give an example when it can be beneficial. Let's assume we have a grid state with a details grid state too. Whenever the parent grid selected row changes the details grid needs to get reset entirely. This is a minimal example with signals services:
interface GridState {
filters: any[];
sorts: any[];
selectedRowId: string | undefined;
}
@Injectable({ providedIn: 'root' })
export class MainGridStoreService {
private state = signal<GridState>({
filters: [],
sorts: [],
selectedRowId: undefined,
});
filters = computed(() => this.state().filters);
sorts = computed(() => this.state().sorts);
selectedRowId = computed(() => this.state().selectedRowId);
}
@Injectable({ providedIn: 'root' })
export class DetailsGridStoreService {
private mainGrid = inject(MainGridStoreService);
private state = linkedSignal({
source: this.mainGrid.selectedRowId,
computation: (): GridState => ({
filters: [],
sorts: [],
selectedRowId: undefined,
}),
});
filters = computed(() => this.state().filters);
sorts = computed(() => this.state().sorts);
selectedRowId = computed(() => this.state().selectedRowId);
}
With ngrx signalState and a hypothetical linkedSignalState:
@Injectable({ providedIn: 'root' })
export class MainGridStoreService {
private state = signalState<GridState>({
filters: [],
sorts: [],
selectedRowId: undefined,
});
filters = this.state.filters;
sorts = this.state.sorts;
selectedRowId = this.state.selectedRowId;
}
@Injectable({ providedIn: 'root' })
export class DetailsGridStoreService {
private mainGrid = inject(MainGridStoreService);
private state = linkedSignalState({
source: this.mainGrid.selectedRowId,
computation: (): GridState => ({
filters: [],
sorts: [],
selectedRowId: undefined,
}),
});
filters = this.state.filters;
sorts = this.state.sorts;
selectedRowId = this.state.selectedRowId;
}
Another option is extending signalState to accept a writeable signal. So:
signalState({a: 2});
signalState(signal({a: 2}));
signalState(linkedSignal(() => ({a: 2})));
Which allows having linked state but also doesn't need to copy the linkedSignal declaration.
Yes, I was also thinking about the option of providing a "user-defined" signal for the signalState. That would also allow other signal types than linkedSignal. We just have to make sure we aren't running into that again: #4863