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
- Always clean up subscriptions: Save
this.unsub = artikelAutomat.subscribe(...)and callthis.unsub()incomponentWillUnmount. - Use
createArticleSelector: Never subscribe an article view component with the rawartikelAutomat.subscribe(this)without a selector; otherwise, any other article loaded or modified anywhere in the app will trigger an unneeded re-render. - Do not mutate directly: Never write
artikelAutomat.state.bySku[sku].name = 'foo'. Always usesetArticleorsetArticleName.