platform icon indicating copy to clipboard operation
platform copied to clipboard

@ngrx/signals: Improve Developer Experience for Immutability

Open rainerhahnekamp opened this issue 1 year ago • 6 comments

Which @ngrx/* package(s) are relevant/related to the feature request?

store

Information

Ensuring that state updates are immutable is crucial for Signal to emit correctly and trigger necessary DOM updates, derived signals, or side effects. Currently, an immutable runtime check is available, but only in development mode due to its potential performance impact.

To enhance the robustness of applications and facilitate debugging, I propose two additional behaviours:

  • Option to enable the immutable check in production mode: There are many Angular applications where this “safety net” outweighs performance penalties by far. Especially “classic enterprise applications” containing many forms where the business logic runs in the backend.
  • $update with Readonly Types: Compilation checks are highly preferable to runtime checks because they allow potential issues to be discovered before the runtime. The $update method could be enhanced to provide the state as a recursive Readonly<T> type to achieve this. This would extend to arrays as ReadonlyArray<T>, removing mutable methods like push.

Example:

import { signalStore, withMethods, withState } from '@ngrx/signals';
type User = { id: number; name: string };
type UsersState = { users: User[]; loading: boolean };

declare function withImmutabilityCheck(mode: 'disabled' | 'devMode' | 'always');

const UsersStore = signalStore(
  withImmutabilityCheck('always'), // <— immutable check
  withState<UsersState>({ users: [], loading: false }),
  withMethods(({ $update, users }) => {
    return {
      addUser(user: User): void {
        $update((state) => {
          state.users.push(user); // 💥compilation error because ReadonlyArray<User>
          state.loading = true; //  💥compilation error because Readonly<UsersState>

          return state;
        });
      },
    };
  })
);

If accepted, I would be happy to provide an initial PR.

Describe any alternatives/workarounds you're currently using

No response

I would be willing to submit a PR to fix this issue

  • [X] Yes
  • [ ] No

rainerhahnekamp avatar Sep 04 '23 13:09 rainerhahnekamp

What about tweaking signatures and return Readonly<T> on methods and similar? Good DX on compile time, no need to do it runtime, 0kb

samuelfernandez avatar Feb 14 '24 12:02 samuelfernandez

@samuelfernandez this is exactly what I am suggesting. You won't get away with a simple Readonly. It has to be nested as well. Here's the proposed type for that: https://github.com/markostanimirovic/ngrx-signal-store-playground/pull/2/files#diff-6230b858ba0b18996436aa5baa97a540a8e759a69f53b6880196d5fbf93e8f46

rainerhahnekamp avatar Feb 14 '24 12:02 rainerhahnekamp

@rainerhahnekamp — may I suggest using this utility from ts-fest: https://github.com/sindresorhus/type-fest/blob/main/source/readonly-deep.d.ts

jits avatar Feb 14 '24 12:02 jits

yes, but I'd rather copy it and not have it as a dependency of ngrx itself...

rainerhahnekamp avatar Feb 14 '24 12:02 rainerhahnekamp

That's our stance on it as well. Not everyone wants to use immer, and if they do they can provide their own wrapper around patchState. We discussed this also with the signalStore and we see it as an extension point.

See closed issue here https://github.com/ngrx/platform/issues/3998

brandonroberts avatar Feb 14 '24 13:02 brandonroberts

@brandonroberts, would it make sense to bring up a PR for the "type-safe"-only mode without immer or should we just close this issue?

rainerhahnekamp avatar Feb 14 '24 13:02 rainerhahnekamp