Skip to content

Commit 1f203b6

Browse files
feat: freeze complete state and not just delta in patchState
1 parent e134030 commit 1f203b6

File tree

4 files changed

+73
-12
lines changed

4 files changed

+73
-12
lines changed

modules/signals/spec/deep-freeze.spec.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { patchState } from '../src/state-source';
1+
import { getState, patchState } from '../src/state-source';
22
import { signalState } from '../src/signal-state';
33
import { signalStore } from '../src/signal-store';
44
import { TestBed } from '@angular/core/testing';
@@ -52,6 +52,64 @@ describe('deepFreeze', () => {
5252
"Cannot assign to read only property 'firstName' of object"
5353
);
5454
});
55+
describe('mutable changes outside of patchState', () => {
56+
it('throws on reassigned a property of the exposed state', () => {
57+
const state = stateFactory();
58+
expect(() => {
59+
state.user().firstName = 'mutable change 1';
60+
}).toThrowError(
61+
"Cannot assign to read only property 'firstName' of object"
62+
);
63+
});
64+
65+
it('throws when exposed state via getState is mutated', () => {
66+
const state = stateFactory();
67+
const s = getState(state);
68+
69+
expect(() => (s.ngrx = 'mutable change 2')).toThrowError(
70+
"Cannot assign to read only property 'ngrx' of object"
71+
);
72+
});
73+
74+
it('throws when mutable change happens for', () => {
75+
const state = stateFactory();
76+
const s = { user: { firstName: 'M', lastName: 'S' } };
77+
patchState(state, s);
78+
79+
expect(() => {
80+
s.user.firstName = 'mutable change 3';
81+
}).toThrowError(
82+
"Cannot assign to read only property 'firstName' of object"
83+
);
84+
});
85+
});
5586
});
5687
}
88+
89+
describe('special tests', () => {
90+
for (const { name, mutationFn } of [
91+
{
92+
name: 'location',
93+
mutationFn: (state: { location: { city: string } }) =>
94+
(state.location.city = 'Paris'),
95+
},
96+
{
97+
name: 'user',
98+
mutationFn: (state: { user: { firstName: string } }) =>
99+
(state.user.firstName = 'Jane'),
100+
},
101+
]) {
102+
it(`throws on concatenated state (${name})`, () => {
103+
const UserStore = signalStore(
104+
{ providedIn: 'root' },
105+
withState(initialState),
106+
withState({ location: { city: 'London' } })
107+
);
108+
const store = TestBed.inject(UserStore);
109+
const state = getState(store);
110+
111+
expect(() => mutationFn(state)).toThrowError();
112+
});
113+
}
114+
});
57115
});

modules/signals/src/signal-state.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import { signal } from '@angular/core';
22
import { STATE_SOURCE, WritableStateSource } from './state-source';
33
import { DeepSignal, toDeepSignal } from './deep-signal';
4+
import { freezeInDevMode } from './deep-freeze';
45

56
export type SignalState<State extends object> = DeepSignal<State> &
67
WritableStateSource<State>;
78

89
export function signalState<State extends object>(
910
initialState: State
1011
): SignalState<State> {
11-
const stateSource = signal(initialState as State);
12+
const stateSource = signal(freezeInDevMode(initialState as State));
1213
const signalState = toDeepSignal(stateSource.asReadonly());
1314
Object.defineProperty(signalState, STATE_SOURCE, {
1415
value: stateSource,

modules/signals/src/state-source.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,11 @@ export function patchState<State extends object>(
3838
): void {
3939
stateSource[STATE_SOURCE].update((currentState) =>
4040
updaters.reduce(
41-
(nextState: State, updater) => ({
42-
...nextState,
43-
...(typeof updater === 'function'
44-
? updater(freezeInDevMode(nextState))
45-
: updater),
46-
}),
41+
(nextState: State, updater) =>
42+
freezeInDevMode({
43+
...nextState,
44+
...(typeof updater === 'function' ? updater(nextState) : updater),
45+
}),
4746
currentState
4847
)
4948
);

modules/signals/src/with-state.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
SignalStoreFeature,
1010
SignalStoreFeatureResult,
1111
} from './signal-store-models';
12+
import { freezeInDevMode } from './deep-freeze';
1213

1314
export function withState<State extends object>(
1415
stateFactory: () => State
@@ -35,10 +36,12 @@ export function withState<State extends object>(
3536

3637
assertUniqueStoreMembers(store, stateKeys);
3738

38-
store[STATE_SOURCE].update((currentState) => ({
39-
...currentState,
40-
...state,
41-
}));
39+
store[STATE_SOURCE].update((currentState) =>
40+
freezeInDevMode({
41+
...currentState,
42+
...state,
43+
})
44+
);
4245

4346
const stateSignals = stateKeys.reduce((acc, key) => {
4447
const sliceSignal = computed(

0 commit comments

Comments
 (0)