Skip to content

Commit f2514ba

Browse files
feat(signals): add patchState function and remove $update method (#4037)
1 parent eeadd3f commit f2514ba

File tree

6 files changed

+196
-188
lines changed

6 files changed

+196
-188
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { patchState, signalState } from '../src';
2+
3+
describe('patchState', () => {
4+
const initialState = {
5+
user: {
6+
firstName: 'John',
7+
lastName: 'Smith',
8+
},
9+
foo: 'bar',
10+
numbers: [1, 2, 3],
11+
ngrx: 'signals',
12+
};
13+
14+
it('patches state via partial state object', () => {
15+
const state = signalState(initialState);
16+
17+
patchState(state, {
18+
user: { firstName: 'Johannes', lastName: 'Schmidt' },
19+
foo: 'baz',
20+
});
21+
22+
expect(state()).toEqual({
23+
...initialState,
24+
user: { firstName: 'Johannes', lastName: 'Schmidt' },
25+
foo: 'baz',
26+
});
27+
});
28+
29+
it('patches state via updater function', () => {
30+
const state = signalState(initialState);
31+
32+
patchState(state, (state) => ({
33+
numbers: [...state.numbers, 4],
34+
ngrx: 'rocks',
35+
}));
36+
37+
expect(state()).toEqual({
38+
...initialState,
39+
numbers: [1, 2, 3, 4],
40+
ngrx: 'rocks',
41+
});
42+
});
43+
44+
it('patches state via sequence of partial state objects and updater functions', () => {
45+
const state = signalState(initialState);
46+
47+
patchState(
48+
state,
49+
{ user: { firstName: 'Johannes', lastName: 'Schmidt' } },
50+
(state) => ({ numbers: [...state.numbers, 4], foo: 'baz' }),
51+
(state) => ({ user: { ...state.user, firstName: 'Jovan' } }),
52+
{ foo: 'foo' }
53+
);
54+
55+
expect(state()).toEqual({
56+
...initialState,
57+
user: { firstName: 'Jovan', lastName: 'Schmidt' },
58+
foo: 'foo',
59+
numbers: [1, 2, 3, 4],
60+
});
61+
});
62+
63+
it('patches state immutably', () => {
64+
const state = signalState(initialState);
65+
66+
patchState(state, {
67+
foo: 'bar',
68+
numbers: [3, 2, 1],
69+
ngrx: 'rocks',
70+
});
71+
72+
expect(state.user()).toBe(initialState.user);
73+
expect(state.foo()).toBe(initialState.foo);
74+
expect(state.numbers()).not.toBe(initialState.numbers);
75+
expect(state.ngrx()).not.toBe(initialState.ngrx);
76+
});
77+
});
Lines changed: 76 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { effect, isSignal } from '@angular/core';
2-
import { signalState } from '../src';
2+
import { patchState, signalState } from '../src';
33
import { testEffects } from './helpers';
44

55
describe('signalState', () => {
@@ -13,167 +13,100 @@ describe('signalState', () => {
1313
ngrx: 'signals',
1414
};
1515

16-
describe('$update', () => {
17-
it('updates state via partial state object', () => {
18-
const state = signalState(initialState);
16+
it('creates signals for nested state slices', () => {
17+
const state = signalState(initialState);
1918

20-
state.$update({
21-
user: { firstName: 'Johannes', lastName: 'Schmidt' },
22-
foo: 'baz',
23-
});
19+
expect(state()).toBe(initialState);
20+
expect(isSignal(state)).toBe(true);
2421

25-
expect(state()).toEqual({
26-
...initialState,
27-
user: { firstName: 'Johannes', lastName: 'Schmidt' },
28-
foo: 'baz',
29-
});
30-
});
22+
expect(state.user()).toBe(initialState.user);
23+
expect(isSignal(state.user)).toBe(true);
3124

32-
it('updates state via updater function', () => {
33-
const state = signalState(initialState);
25+
expect(state.user.firstName()).toBe(initialState.user.firstName);
26+
expect(isSignal(state.user.firstName)).toBe(true);
3427

35-
state.$update((state) => ({
36-
numbers: [...state.numbers, 4],
37-
ngrx: 'rocks',
38-
}));
28+
expect(state.foo()).toBe(initialState.foo);
29+
expect(isSignal(state.foo)).toBe(true);
3930

40-
expect(state()).toEqual({
41-
...initialState,
42-
numbers: [1, 2, 3, 4],
43-
ngrx: 'rocks',
44-
});
45-
});
31+
expect(state.numbers()).toBe(initialState.numbers);
32+
expect(isSignal(state.numbers)).toBe(true);
33+
34+
expect(state.ngrx()).toBe(initialState.ngrx);
35+
expect(isSignal(state.ngrx)).toBe(true);
36+
});
37+
38+
it('does not modify props that are not state slices', () => {
39+
const state = signalState(initialState);
40+
(state as any).x = 1;
41+
(state.user as any).x = 2;
42+
(state.user.firstName as any).x = 3;
43+
44+
expect((state as any).x).toBe(1);
45+
expect((state.user as any).x).toBe(2);
46+
expect((state.user.firstName as any).x).toBe(3);
4647

47-
it('updates state via sequence of partial state objects and updater functions', () => {
48+
expect((state as any).y).toBe(undefined);
49+
expect((state.user as any).y).toBe(undefined);
50+
expect((state.user.firstName as any).y).toBe(undefined);
51+
});
52+
53+
it(
54+
'emits new values only for affected signals',
55+
testEffects((tick) => {
4856
const state = signalState(initialState);
57+
let numbersEmitted = 0;
58+
let userEmitted = 0;
59+
let firstNameEmitted = 0;
4960

50-
state.$update(
51-
{ user: { firstName: 'Johannes', lastName: 'Schmidt' } },
52-
(state) => ({ numbers: [...state.numbers, 4], foo: 'baz' }),
53-
(state) => ({ user: { ...state.user, firstName: 'Jovan' } }),
54-
{ foo: 'foo' }
55-
);
56-
57-
expect(state()).toEqual({
58-
...initialState,
59-
user: { firstName: 'Jovan', lastName: 'Schmidt' },
60-
foo: 'foo',
61-
numbers: [1, 2, 3, 4],
61+
effect(() => {
62+
state.numbers();
63+
numbersEmitted++;
6264
});
63-
});
6465

65-
it('updates state immutably', () => {
66-
const state = signalState(initialState);
66+
effect(() => {
67+
state.user();
68+
userEmitted++;
69+
});
6770

68-
state.$update({
69-
foo: 'bar',
70-
numbers: [3, 2, 1],
71-
ngrx: 'rocks',
71+
effect(() => {
72+
state.user.firstName();
73+
firstNameEmitted++;
7274
});
7375

74-
expect(state.user()).toBe(initialState.user);
75-
expect(state.foo()).toBe(initialState.foo);
76-
expect(state.numbers()).not.toBe(initialState.numbers);
77-
expect(state.ngrx()).not.toBe(initialState.ngrx);
78-
});
79-
});
76+
expect(numbersEmitted).toBe(0);
77+
expect(userEmitted).toBe(0);
78+
expect(firstNameEmitted).toBe(0);
8079

81-
describe('nested signals', () => {
82-
it('creates signals for nested state slices', () => {
83-
const state = signalState(initialState);
80+
tick();
8481

85-
expect(state()).toBe(initialState);
86-
expect(isSignal(state)).toBe(true);
82+
expect(numbersEmitted).toBe(1);
83+
expect(userEmitted).toBe(1);
84+
expect(firstNameEmitted).toBe(1);
8785

88-
expect(state.user()).toBe(initialState.user);
89-
expect(isSignal(state.user)).toBe(true);
86+
patchState(state, { numbers: [1, 2, 3] });
87+
tick();
9088

91-
expect(state.user.firstName()).toBe(initialState.user.firstName);
92-
expect(isSignal(state.user.firstName)).toBe(true);
89+
expect(numbersEmitted).toBe(2);
90+
expect(userEmitted).toBe(1);
91+
expect(firstNameEmitted).toBe(1);
9392

94-
expect(state.foo()).toBe(initialState.foo);
95-
expect(isSignal(state.foo)).toBe(true);
93+
patchState(state, (state) => ({
94+
user: { ...state.user, lastName: 'Schmidt' },
95+
}));
96+
tick();
9697

97-
expect(state.numbers()).toBe(initialState.numbers);
98-
expect(isSignal(state.numbers)).toBe(true);
98+
expect(numbersEmitted).toBe(2);
99+
expect(userEmitted).toBe(2);
100+
expect(firstNameEmitted).toBe(1);
99101

100-
expect(state.ngrx()).toBe(initialState.ngrx);
101-
expect(isSignal(state.ngrx)).toBe(true);
102-
});
102+
patchState(state, (state) => ({
103+
user: { ...state.user, firstName: 'Johannes' },
104+
}));
105+
tick();
103106

104-
it('does not modify props that are not state slices', () => {
105-
const state = signalState(initialState);
106-
(state as any).x = 1;
107-
(state.user as any).x = 2;
108-
(state.user.firstName as any).x = 3;
109-
110-
expect((state as any).x).toBe(1);
111-
expect((state.user as any).x).toBe(2);
112-
expect((state.user.firstName as any).x).toBe(3);
113-
114-
expect((state as any).y).toBe(undefined);
115-
expect((state.user as any).y).toBe(undefined);
116-
expect((state.user.firstName as any).y).toBe(undefined);
117-
});
118-
119-
it(
120-
'emits new values only for affected signals',
121-
testEffects((tick) => {
122-
const state = signalState(initialState);
123-
let numbersEmitted = 0;
124-
let userEmitted = 0;
125-
let firstNameEmitted = 0;
126-
127-
effect(() => {
128-
state.numbers();
129-
numbersEmitted++;
130-
});
131-
132-
effect(() => {
133-
state.user();
134-
userEmitted++;
135-
});
136-
137-
effect(() => {
138-
state.user.firstName();
139-
firstNameEmitted++;
140-
});
141-
142-
expect(numbersEmitted).toBe(0);
143-
expect(userEmitted).toBe(0);
144-
expect(firstNameEmitted).toBe(0);
145-
146-
tick();
147-
148-
expect(numbersEmitted).toBe(1);
149-
expect(userEmitted).toBe(1);
150-
expect(firstNameEmitted).toBe(1);
151-
152-
state.$update({ numbers: [1, 2, 3] });
153-
tick();
154-
155-
expect(numbersEmitted).toBe(2);
156-
expect(userEmitted).toBe(1);
157-
expect(firstNameEmitted).toBe(1);
158-
159-
state.$update((state) => ({
160-
user: { ...state.user, lastName: 'Schmidt' },
161-
}));
162-
tick();
163-
164-
expect(numbersEmitted).toBe(2);
165-
expect(userEmitted).toBe(2);
166-
expect(firstNameEmitted).toBe(1);
167-
168-
state.$update((state) => ({
169-
user: { ...state.user, firstName: 'Johannes' },
170-
}));
171-
tick();
172-
173-
expect(numbersEmitted).toBe(2);
174-
expect(userEmitted).toBe(3);
175-
expect(firstNameEmitted).toBe(2);
176-
})
177-
);
178-
});
107+
expect(numbersEmitted).toBe(2);
108+
expect(userEmitted).toBe(3);
109+
expect(firstNameEmitted).toBe(2);
110+
})
111+
);
179112
});

modules/signals/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1+
export { PartialStateUpdater, patchState } from './patch-state';
12
export { selectSignal } from './select-signal';
23
export { signalState } from './signal-state';
3-
export { SignalStateUpdater } from './signal-state-models';

modules/signals/src/patch-state.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { SIGNAL_STATE_META_KEY, SignalStateMeta } from './signal-state';
2+
3+
export type PartialStateUpdater<State extends Record<string, unknown>> =
4+
| Partial<State>
5+
| ((state: State) => Partial<State>);
6+
7+
export function patchState<State extends Record<string, unknown>>(
8+
signalState: SignalStateMeta<State>,
9+
...updaters: PartialStateUpdater<State>[]
10+
): void {
11+
signalState[SIGNAL_STATE_META_KEY].update((currentState) =>
12+
updaters.reduce(
13+
(nextState: State, updater) => ({
14+
...nextState,
15+
...(typeof updater === 'function' ? updater(nextState) : updater),
16+
}),
17+
currentState
18+
)
19+
);
20+
}

modules/signals/src/signal-state-models.ts

Lines changed: 0 additions & 21 deletions
This file was deleted.

0 commit comments

Comments
 (0)