Skip to content

RFC: withDirtyCheck #255

@Hypercubed

Description

@Hypercubed

Motivation

Add a new feature called withDirtyCheck that allows developers to deeply monitor dirty state in complex data structures. Unlike shallow dirty checks, this feature will track changes in nested objects and arrays, making it easier to determine if any part of the state has been modified. Unlike Angular forms' isDirty state, this fetaure will determine dirtiness based on structural changes rather than user interactions.

Part 1: Deep Dirty Signals (TBR)

In addition to the withDirtyCheck feature, this RFC proposes a new utility called deepDirtySignal. This utility will provide a way to create signals that can deeply track dirty state in nested data structures.

Note: I have a PoC of this utility available.

Usage Example

const source = signal({
  id: 1,
  name: 'Test',
  details: {
    age: 30,
    address: {
      city: 'New York',
      zip: '10001',
    },
  },
});

const isDirty = deepDirtySignal(source);
console.log(isDirty()); // false

source.update((obj) => {
  return {
    ...obj,
    name: 'Updated Test',
  };
});

console.log(isDirty()); // true
console.log(isDirty.name()); // true
console.log(isDirty.details()); // false
console.log(isDirty.details.address()); // false

Pros of deepDirtySignal:

  • provides a type-safe way to track dirty state in nested objects
  • signals are created lazily, so only accessed paths are tracked
  • signals internal caching optimizes performance by avoiding redundant computations

Cons of deepDirtySignal:

  • complex implementation
  • may not support complex types well (e.g., partials)

Alternative APIs considered

Instead of lazily creating signals for each path using the fluent API, we could eagerly create signals on initialization. For example, isNameDirty = deepDirtySignal(() => source().address) would eagerly create a dirty check signal for the address. It would dirty check based on structural changes to the address property but would not provide a way to dirty check nested properties.

Part 2: withDirtyCheck Feature

The withDirtyCheck feature will be implemented as a store feature that adds deep dirty checking to the store state. This feature will leverage the deepDirtySignal utility to provide a way to monitor dirty state in complex store states.

Proposed API

type BookSearchState = {
  books: Book[];
  isLoading: boolean;
  filter: { query: string; order: 'asc' | 'desc' };
};

const initialState: BookSearchState = {
  books: [],
  isLoading: false,
  filter: { query: '', order: 'asc' },
};

export const BooksStore = signalStore(
  withState(initialState),
  withDirtyCheck(), // <-- Adds deep dirty checking as `isDirty`
);

const booksStore = inject(BooksStore);
const enableSaveButton = computed(() => !booksStore.isLoading() && booksStore.isDirty.books());

Alternatives considered

Instead of adding store wide dirty checking, the withDirtyCheck feature could create dirty check signals only for specific slices of state. For example, withDirtyCheck({ isNameDirty: (state) => state().books }) would only create dirty check signals for the books slice of state.

This may be equivalant to using deepDirtySignal within a withComputed store feature.

export const BooksStore = signalStore(
  withState(initialState),
  withComputed(({ books }) => ({
    isBooksDirty: deepDirtySignal(() => books()),
  }))
);

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions