Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs(signals): add explanation of mutable change protection to FAQ #2

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 85 additions & 8 deletions projects/ngrx.io/content/guide/signals/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,23 @@
There's no official connection between `@ngrx/signals` and the Redux Devtools.
We expect the Angular Devtools will provide support for signals soon, which can be used to track the state.
However, you could create a feature for this, or you can make use of the [`withDevtools` feature](https://github.com/angular-architects/ngrx-toolkit?tab=readme-ov-file#devtools-withdevtools) from the `@angular-architects/ngrx-toolkit` package.

</details>

<details>
<summary>Can I interact with my NgRx Actions within a SignalStore?</summary>

Signals are not meant to have a concept of time. Also, the effect is somewhat tied to Angular change detection, so you can't observe every action that would be dispatched over time through some sort of Signal API.
The global NgRx Store is still the best mechanism to dispatch action(s) over time and react to them across multiple features.

</details>

<details>
<summary>Can I use the Redux pattern (reducers) to build my state?</summary>

Just like `@ngrx/component-store`, there is no indirection between events and how it affects the state. To update the SignalStore's state use the `patchState` function.
However, SignalStore is extensible and you can build your own custom feature that uses the Redux pattern.

</details>

<details>
Expand All @@ -26,18 +29,20 @@
To create a class-based SignalStore, create a new class and extend from `signalStore`.

```ts

@Injectable()
export class CounterStore extends signalStore(
{ protectedState: false },
withState({ count: 0 })
{protectedState: false},
withState({count: 0})
) {
readonly doubleCount = computed(() => this.count() * 2);

increment(): void {
patchState(this, { count: this.count() + 1 });
patchState(this, {count: this.count() + 1});
}
}
```

</details>

<details>
Expand All @@ -46,14 +51,15 @@ export class CounterStore extends signalStore(
To get the type of a SignalStore, use the `InstanceType` utility type.

```ts
const CounterStore = signalStore(withState({ count: 0 }));
const CounterStore = signalStore(withState({count: 0}));

type CounterStore = InstanceType<typeof CounterStore>;

function logCount(store: CounterStore): void {
console.log(store.count());
}
```

</details>

<details>
Expand All @@ -63,16 +69,87 @@ function logCount(store: CounterStore): void {

```ts
// counter.store.ts
export const CounterStore = signalStore(withState({ count: 0 }));
export const CounterStore = signalStore(withState({count: 0}));

export type CounterStore = InstanceType<typeof CounterStore>;

// counter.component.ts
import { CounterStore } from './counter.store';
import {CounterStore} from './counter.store';

@Component({ /* ... */ })
@Component({ /* ... */})
export class CounterComponent {
constructor(readonly store: CounterStore) {}
constructor(readonly store: CounterStore) {
}
}
```

</details>

<details>
<summary>I get the error "Cannot assign to read only property 'X' of object '[object Object]'"</summary>

The state in the SignalStore must be immutable. If you make mutable changes, there’s a high risk of introducing subtle, hard-to-diagnose bugs. To protect against this, SignalStore introduced an additional check to enforce immutability.

The immutability requirement originates from Angular's Signal itself, which serves as the foundation of the SignalStore. Here’s an example to illustrate this:

```ts
const person = signal({name: 'Konrad', age: 25});
const personFormat = computed(
() => `${person().name} is ${person().age} years old`,
);

console.log(personFormat()); // shows 25 years

person().age = 30; // 👎 mutable change
console.log(personFormat()); // 👎 person did not notify personFormat. still 25 years

// another mutable change
person.update((value) => {
value.age++; // 👎
return value;
});

console.log(personFormat()); // 👎 no notification. 25 years.

// immutable change
person.update((value) => ({
...value, // 👍 immutable change
counter: 40,
}));
console.log(personFormat()); // 👍 personFormat has been notified and shows 40 years.

```

As you can see, the problem typically arises in computed, effect or the component's template, not directly at the root (the signal mutation itself). This is why these issues are so hard to debug.

You might look into your components wondering why they’re not updating, while the real error is buried deep within the SignalStore.

Therefore, both `signalState` and `signalStore` throw on those mutable changes. They protect you!

```typescript
const person = signalState({name: 'Konrad', age: 25});
patchState(person, (value) => {
value.age++
return value;
}) // 🔥 throws


person().age = 30; // 🔥 throws
```

If you require mutable properties in your state, then put them into `withProps`.

```ts
const PersonStore = signalStore(
withProps({name: 'Konrad', age: 25}),
withMethods(store => ({
setAge(age: number) {
store.age = age;
}
})));
const personStore = new PersonStore();

personStore.setAge(30);
```

</details>
Loading