Files
dashfront/docs/artikelAutomat.md
2026-09-23 17:24:33 +02:00

6.6 KiB

artikelAutomat

Source File: src/automats/artikelAutomat.js
Registration Key: 'artikel' (Automat.get('artikel') / window.__AUTOMATS__.get('artikel'))


1. Overview & Purpose

artikelAutomat serves as the centralized cache and reactive distributor for individual article detail payloads (GET /api/article/:sku).

Core Responsibilities

  • In-flight Deduplication: Multiple components or route guards requesting the same SKU concurrently share a single HTTP request via an internal Map<string, Promise<object>>.
  • Zero-Flicker Navigation: Navigating between tabs or returning to previously viewed articles reuses the cached detail object instantly without round-trips.
  • Optimistic Updates: Immediate UI updates (such as renaming an article via setArticleName) without awaiting network round-trips.
  • Granular Subscriptions: Components can subscribe to an individual SKU via createArticleSelector(sku) rather than re-rendering on every cache change.

2. State Structure

interface ArtikelState {
  /**
   * Cached article detail payloads keyed by SKU string.
   * A missing key indicates the article is not yet loaded into memory.
   * Values include full article details: prices, photos, eans, brand, unit,
   * categoryIds, manifests, child variants, etc.
   */
  bySku: Record<string, object>;
}

Initial State

const initialState = {
  bySku: {},
};

3. Configuration & Options

new Automat(initialState, actions, { name: 'artikel' });
  • name: 'artikel' — registers the automat globally.
  • persist: false (default in-memory window lifecycle).

4. Actions & Methods

Access actions via artikelAutomat.actions.<method>().

loadArticle(sku: string): Promise<object>

Loads an article detail into the cache. If already present in bySku, returns the cached article immediately. If an HTTP request for this SKU is currently in-flight, it returns that shared promise. Otherwise, dispatches GET /api/article/:sku, normalizes the response, stores it in bySku, and cleans up the in-flight map.

reloadArticle(sku: string): Promise<object>

Bypasses the cached copy and silently fetches a fresh detail payload from the backend. The existing cache entry is preserved until the response arrives. Typically used during SSE mutation events or user pull-to-refresh.

setArticle(article: object): object

Puts an article detail object directly into bySku[article.sku]. Requires article to be an object with a defined sku. Used after create/update API mutations so that all subscribers immediately observe the latest data.

setArticleName(sku: string, name: string): object | null

Optimistically updates the name property of a cached article without making an API call. Returns the updated article object or null if the SKU was not found in cache.

clearArticle(sku: string): void

Removes a specific SKU from bySku. Useful after deleting an article or when forcing a complete refetch on next access.

clear(): void

Resets the entire bySku dictionary to empty ({}).


5. Selectors & Helper Functions

Exported alongside artikelAutomat:

getArticle(sku: string): object | null

Synchronous read from artikelAutomat.state.bySku[sku]. Returns null if not found or if sku is nullish.

const article = getArticle('10042');

hasArticle(sku: string): boolean

Returns true if sku is already loaded in bySku.

if (!hasArticle('10042')) {
  void artikelAutomat.actions.loadArticle('10042');
}

createArticleSelector(sku: string): (state: ArtikelState) => object | null

Produces a memoized selector function for a specific SKU. Pass this to artikelAutomat.subscribe(target, selector) so your component only re-renders when this specific article's data changes.

selectArticle(sku: string): ScopedSlice<object | null>

Returns a scoped slice for the given SKU (artikelAutomat.select(createArticleSelector(sku))).


6. Usage Examples

A. React PureComponent Detail Header

import { PureComponent } from 'react';
import PropTypes from 'prop-types';
import artikelAutomat, { getArticle, createArticleSelector } from '../automats/artikelAutomat';

export class ArticleHeader extends PureComponent {
  static propTypes = {
    sku: PropTypes.string.isRequired,
  };

  constructor(props) {
    super(props);
    // Synchronous initial state read prevents layout shift / loading spinner flicker
    this.state = {
      article: getArticle(props.sku),
    };
  }

  componentDidMount() {
    this.subscribe(this.props.sku);
    if (!this.state.article) {
      artikelAutomat.actions.loadArticle(this.props.sku).catch(console.error);
    }
  }

  componentDidUpdate(prevProps) {
    if (prevProps.sku !== this.props.sku) {
      this.subscribe(this.props.sku);
      if (!hasArticle(this.props.sku)) {
        artikelAutomat.actions.loadArticle(this.props.sku).catch(console.error);
      }
    }
  }

  componentWillUnmount() {
    this.unsub?.();
  }

  subscribe(sku) {
    this.unsub?.();
    this.setState({ article: getArticle(sku) });
    this.unsub = artikelAutomat.subscribe(this, createArticleSelector(sku));
  }

  render() {
    const { article } = this.state;
    if (!article) return <div>Artikel wird geladen...</div>;
    return (
      <div>
        <h2>{article.name}</h2>
        <span>SKU: {article.sku}</span>
      </div>
    );
  }
}

B. Inline Optimistic Rename

import { artikelAutomat } from '../automats';
import { article as articleApi } from '../api';

async function updateName(sku, newName) {
  // 1. Optimistic update in UI cache
  artikelAutomat.actions.setArticleName(sku, newName);

  try {
    // 2. Persist to backend
    const res = await articleApi.update(sku, { name: newName });
    // 3. Re-set authoritative response
    artikelAutomat.actions.setArticle(res.article);
  } catch (err) {
    // 4. Rollback on failure
    await artikelAutomat.actions.reloadArticle(sku);
    throw err;
  }
}

7. Best Practices & Gotchas

  1. Always clean up subscriptions: Save this.unsub = artikelAutomat.subscribe(...) and call this.unsub() in componentWillUnmount.
  2. Use createArticleSelector: Never subscribe an article view component with the raw artikelAutomat.subscribe(this) without a selector; otherwise, any other article loaded or modified anywhere in the app will trigger an unneeded re-render.
  3. Do not mutate directly: Never write artikelAutomat.state.bySku[sku].name = 'foo'. Always use setArticle or setArticleName.