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 #4679

Closed
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
82 changes: 82 additions & 0 deletions projects/ngrx.io/content/guide/signals/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,86 @@ export class CounterComponent {
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.
rainerhahnekamp marked this conversation as resolved.
Show resolved Hide resolved

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
age: 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++
rainerhahnekamp marked this conversation as resolved.
Show resolved Hide resolved
return value;
}) // 🔥 throws


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

If you require mutable properties in your state, then put them into `withProps`.
rainerhahnekamp marked this conversation as resolved.
Show resolved Hide resolved

Common examples are `FormGroup` or instances from the router package:

```ts
const PersonStore = signalStore(
withProps(() => {
const router = inject(Router);

return {
_currentUrl$: router.events.pipe(
filter((event) => event instanceof NavigationEnd),
map(() => router.routerState.snapshot.url),
)
};
}),
withState({ name: 'Konrad', age: 25 }),
withMethods(store => ({
setAge(age: number) {
store.age = age;
}
})));
const personStore = new PersonStore();

personStore.setAge(30);
```

</details>
Loading