Skip to content

zuriscript/signalstory

Repository files navigation

Signalstory - Angular state management with signals
Baked into classical angular services!

Documentation   📚    Sample   🚀    Website   🔥    Release notes   ✨   

License: MIT npm version Commitizen friendly PRs coc-badge styled with prettier

signalstory is a state management library based on angular signals. It offers a range of architectural options, from simple repository-based state management (signal-in-a-service) to orchestrating decoupled commands, handling side effects through encapsulated objects, and facilitating inter-store communication using an event-driven approach. The ultimate goal is to provide a great user experience for all developers, whether junior or senior, while incorporating all the features you need to master your frontend state requirements.

Tip

Starting out? You can keep it nice and simple if you prefer to avoid exploring all the advanced features that a state management library can offer! Begin by checking out the store, and only dive into the rest if you're curious later on.

Here's a snapshot of some notable highlights:

✅  Signal-in-a-service approach
✅  Simple, non-intrusive and lightweight
✅  Optimized for Scalability
✅  Imperative-first with Declaritive capabilities
✅  Immutability on demand
✅  Rich plugin ecosystem
✅  Native IndexedDB support
✅  Transactional Undo/Redo
✅  Global State Snaphots and Rollbacks
✅  Devtools support
✅  Effect and Store status tracking
✅  Realtime store performance statistics
✅  Custom plugin support
✅  Built-in testing utilities
✅  SSR friendly
✅  Tree-shakeable

Let the store grow with your project

Guiding Principles

  • 🚀 Use class methods to provide controlled access and mutations to shared state.
  • 🌌 If your store becomes too complex and bloated, slice it into multiple stores.
  • ✨ Join and aggregate your state at the component level using signal mechanics.
  • 🌐 Need to sync states between stores synchronously? - Use events.
  • 🔮 Need to decouple actors and consumers as you do in redux? - Use events.
  • 🔄 Craving Immutability? - Just activate it.
  • 🏎️ Don't want full immutability because your store has to be super fast? - Don't activate it.
  • 🧙‍♂️ Seeking a way to encapsulate side effects in a reusable, maintainable, and testable way? - Use effect objects.
  • 🔍 Want a way to reuse and test queries spanning over multiple stores? - Use query objects.
  • 📦 Don't want to use a class for stores? - You don't have to.
  • 🛠️ Tired of debugging state changes in the console? - Enable redux devtools.
  • 🪄 Still want some good old logging magic? - Enable Store logger plugin
  • ⏳ Need to keep track of store history and perform undo/redo operations? - track the history.
  • 💾 Want to sync your state with local storage? - Enable the persistence plugin.
  • 🗄️ Need a more sophisticated store storage or building an offline app? - Use IndexedDB adapter
  • 📈 Need to get notified of whether your store is modified or currently loading? - Enable the Store Status plugin.
  • 📊 Wondering where your bottlenecks are? - Enable the performance counter plugin
  • 🎨 Something's missing? - Write a custom plugin.
  • 📖 Read the docs for more features and concepts.

Installation

Install the library using npm:

npm install signalstory

Sneak peek

import { produce } from 'immer';

// Immutable store class using immer.js for boosting immutable mutations
@Injectable({ providedIn: 'root' })
class BookStore extends ImmutableStore<Book[]> {
  constructor() {
    super({
        initialState: { ... },
        name: 'Books Store',
        mutationProducerFn: produce,
        plugins: [
          useDevtools(),
          usePerformanceCounter(),
          useLogger(),
          useStoreStatus(),
          useStorePersistence(
            configureIndexedDb({
              dbName: 'SharedDatabase',
          })),
        ],
    });
    
    // Handle store reset request events. Note, the storeResetRequestEvent would 
    // be created or imported, see the events documentation for more details
    this.registerHandler(storeResetRequestEvent, store => {
      store.set([], 'Reset');
    });
  }

  // Query
  public get getBooksInCollection() {
    return computed(() => this.state().filter(x => x.isInCollection));
  }

  // Command
  public addToCollection(bookId: string) {
    this.mutate(state => {
      const book = state.find(x => x.id === bookId);
      if (book) {
        book.isInCollection = true;
      }
    }, 'Add Book To Collection');
  }
}
// Encapsulated multi store query object
export const BooksAndPublishersByAuthorInSwitzerlandQuery = createQuery(
  [BookStore, PublisherStore],
  (books, publishers, authorId: string) => {
    const booksFromAuthor = books.state().filter(x => x.author === authorId);
    const publishersInSwitzerland = publishers
      .state()
      .filter(x => x.country === 'CH');

    return booksFromAuthor.map(book => ({
      book,
      publisher: publishersInSwitzerland.find(
        p => p.id === book.mainPublisherId
      ),
    }));
  }
);
// And then run it
const query = myBookStore.runQuery(
  BooksAndPublishersByAuthorInSwitzerlandQuery,
  'sapowski'
);
// Encapsulated effect object
export const fetchBooksEffect = createEffect(
  'Fetch Books',
  (store: BookStore) => {
    const service = inject(BooksService);
    const notification = inject(NotificationService);

    return service.fetchBooks().pipe(
      catchError(err => {
        notification.alertError(err);
        return of([]);
      }),
      tap(result => store.setBooks(result))
    );
  },
  {
    setLoadingStatus: true, // indicates that the store is loading while the effect runs
    setInitializedStatus: true, // it should mark the store as initialized upon completion
  }
);
// And then run it
myBookStore.runEffect(fetchBooksEffect).subscribe();
const loadingSignal = isLoading(myBookStore); // true while effect is running
const initializedSignal = initialized(myBookStore); // true after initializing effect completion
const modifiedSignal = modified(myBookStore); // true after store update
// Track history spanning multiple stores
const tracker = trackHistory(50, store1, store2);

// Undo single commands
store1.set({ value: 10 }, 'ChangeCommand');
tracker.undo();

tracker.beginTransaction('Transaction Label');
store1.set({ value: 42 }, 'ChangeCommand');
store2.set({ value: 23 }, 'AnotherCommand');
tracker.endTransaction();

// Undo both commands on store1 and store2 at once
tracker.undo();

// Redo the whole transaction
tracker.redo();

Sample Application

To set up and run the sample app locally, follow the steps below:

  1. Clone the repository: Clone the repository containing the signalstory library and the sample app.

  2. Install dependencies: Navigate to the root directory of the repository and run the following command to install the necessary dependencies:

    npm install
  3. Build the library: Run the following command to build the signalstory library:

    ng build signalstory
  4. Serve the sample app: Run the following command to serve the sample app locally:

    ng serve sample --open

signalstory

made with ❤️ by zuriscript