Skip to content

Commit 3981df9

Browse files
docs: add v20 migration guide (#4859)
Closes #4842
1 parent e11c23f commit 3981df9

File tree

2 files changed

+429
-2
lines changed
  • projects
    • ngrx.io/content/guide/migration
    • www/src/app/pages/guide/migration

2 files changed

+429
-2
lines changed

projects/ngrx.io/content/guide/migration/v20.md

Lines changed: 209 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ NgRx supports using the Angular CLI `ng update` command to update your dependenc
77
To update your packages to the latest released version, run the command below.
88

99
```sh
10-
ng update @ngrx/store@20
10+
# NgRx Store related packages
11+
ng update @ngrx/[email protected]
12+
13+
# NgRx Signals package
14+
ng update @ngrx/[email protected]
1115
```
1216

1317
## Dependencies
@@ -20,3 +24,207 @@ Version 20 has the minimum version requirements:
2024
- RxJS version ^6.5.x || ^7.5.0
2125

2226
## Breaking changes
27+
28+
### Signals
29+
30+
The internal `STATE_SOURCE` is no longer represented as a single `WritableSignal` holding the entire state object. Instead, each top-level state property becomes its own `WritableSignal` or remains as-is if a `WritableSignal` is provided as a state property.
31+
32+
BEFORE:
33+
34+
1. The initial state object reference is preserved:
35+
36+
```ts
37+
const initialState = { ngrx: 'rocks' };
38+
39+
// signalState:
40+
const state = signalState(initialState);
41+
state() === initialState; // true
42+
43+
// withState:
44+
const Store = signalStore(withState(initialState));
45+
const store = new Store();
46+
getState(store) === initialState; // true
47+
```
48+
49+
2. Top-level `WritableSignal`s are wrapped with `Signal`s:
50+
51+
```ts
52+
// signalState:
53+
const state = signalState({ ngrx: signal('rocks') });
54+
state.ngrx // type: Signal<WritableSignal<string>>
55+
56+
// withState:
57+
const Store = signalStore(withState({ ngrx: signal('rocks') }));
58+
const store = new Store();
59+
store.ngrx // type: Signal<WritableSignal<string>>
60+
```
61+
62+
3. Root state properties can be added dynamically:
63+
64+
```ts
65+
// signalState:
66+
const state = signalState<Record<string, string>>({});
67+
console.log(state()); // {}
68+
69+
patchState(state, { ngrx: 'rocks' });
70+
console.log(state()); // { ngrx: 'rocks' }
71+
72+
// withState:
73+
const Store = signalStore(
74+
{ protectedState: false },
75+
withState<Record<string, string>>({})
76+
);
77+
const store = new Store();
78+
console.log(getState(store)); // {}
79+
80+
patchState(store, { ngrx: 'rocks' });
81+
console.log(getState(store)); // { ngrx: 'rocks' }
82+
```
83+
84+
AFTER:
85+
86+
1. The initial state object reference is not preserved:
87+
88+
```ts
89+
const initialState = { ngrx: 'rocks' };
90+
91+
// signalState:
92+
const state = signalState(initialState);
93+
state() === initialState; // false
94+
95+
// withState:
96+
const Store = signalStore(withState(initialState));
97+
const store = new Store();
98+
getState(store) === initialState; // false
99+
```
100+
101+
2. Top-level `WritableSignal`s are not wrapped with `Signal`s:
102+
103+
```ts
104+
// signalState:
105+
const state = signalState({ ngrx: signal('rocks') });
106+
state.ngrx // type: Signal<string>
107+
108+
// withState:
109+
const Store = signalStore(withState({ ngrx: signal('rocks') }));
110+
const store = new Store();
111+
store.ngrx // type: Signal<string>
112+
```
113+
114+
3. Root state properties can not be added dynamically:
115+
116+
```ts
117+
// signalState:
118+
const state = signalState<Record<string, string>>({});
119+
console.log(state()); // {}
120+
121+
patchState(state, { ngrx: 'rocks' });
122+
console.log(state()); // {}
123+
124+
// withState:
125+
const Store = signalStore(
126+
{ protectedState: false },
127+
withState<Record<string, string>>({})
128+
);
129+
const store = new Store();
130+
console.log(getState(store)); // {}
131+
132+
patchState(store, { ngrx: 'rocks' });
133+
console.log(getState(store)); // {}
134+
```
135+
136+
### Entity
137+
138+
#### `getInitialState` is type-safe
139+
140+
`getInitialState` is now type-safe, meaning that the initial state must match the entity state type. This change ensures that the initial state is correctly typed and prevents additional properties from being added to the state.
141+
142+
BEFORE:
143+
144+
```ts
145+
import { EntityState, createEntityAdapter, EntityAdapter } from '@ngrx/entity';
146+
147+
interface Book {
148+
id: string;
149+
title: string;
150+
}
151+
interface BookState extends EntityState<Book> {
152+
selectedBookId: string | null;
153+
}
154+
155+
export const adapter: EntityAdapter<Book> = createEntityAdapter<Book>();
156+
export const initialState: BookState = adapter.getInitialState({
157+
selectedBookId: '1',
158+
otherProperty: 'value',
159+
});
160+
```
161+
162+
AFTER:
163+
164+
```ts
165+
166+
import { EntityState, createEntityAdapter, EntityAdapter } from '@ngrx/entity';
167+
168+
interface Book {
169+
id: string;
170+
title: string;
171+
}
172+
interface BookState extends EntityState<Book> {
173+
selectedBookId: string | null;
174+
}
175+
176+
export const adapter: EntityAdapter<Book> = createEntityAdapter<Book>();
177+
export const initialState: BookState = adapter.getInitialState({
178+
selectedBookId: '1',
179+
// 👇 this throws an error
180+
// otherProperty: 'value',
181+
});
182+
```
183+
184+
### Effects
185+
186+
#### `act` operator is removed
187+
188+
The `act` operator is removed in favor of core RxJS flattening operators and `mapResponse` from the `@ngrx/operators` package.
189+
190+
BEFORE:
191+
192+
```ts
193+
@Injectable()
194+
export class AuthEffects {
195+
readonly login$ = createEffect(() => {
196+
return this.actions$.pipe(
197+
ofType(LoginPageActions.loginFormSubmitted),
198+
act({
199+
project: ({ credentials }) =>
200+
this.authService
201+
.login(credentials)
202+
.pipe(map((user) => AuthApiActions.loginSuccess({ user }))),
203+
error: (error) => AuthApiActions.loginFailure({ error }),
204+
operator: exhaustMap,
205+
})
206+
);
207+
});
208+
}
209+
```
210+
211+
AFTER:
212+
213+
```ts
214+
@Injectable()
215+
export class AuthEffects {
216+
readonly login$ = createEffect(() => {
217+
return this.actions$.pipe(
218+
ofType(LoginPageActions.loginFormSubmitted),
219+
exhaustMap(({ credentials }) =>
220+
this.authService.login(credentials).pipe(
221+
mapResponse({
222+
next: (user) => AuthApiActions.loginSuccess({ user }),
223+
error: (error) => AuthApiActions.loginFailure({ error }),
224+
})
225+
)
226+
)
227+
);
228+
});
229+
}
230+
```

0 commit comments

Comments
 (0)