# Outcome Recipes

Use these guides when you know the behavior you want and need the exact combination of actions, middleware, and bindings to deliver it.

## 1. Optimistic updates with rollback

**Goal:** Update the UI instantly when a user edits data and roll back if the server rejects the change.

### Steps

1. Model the optimistic action so it records the previous value alongside the new value:

   ```typescript
   type AppState = { todos: Todo[] };

   export const updateTodo = (state: AppState, { id, patch }: { id: string; patch: Partial<Todo> }) => {
     return {
       ...state,
       todos: state.todos.map(todo => todo.id === id ? { ...todo, ...patch } : todo)
     };
   };
   ```
2. Dispatch the optimistic action before the API call:

   ```typescript
   const store = resolve(IStore<AppState>);

   async function saveTodo(todo: Todo) {
     const previous = structuredClone(todo);
     store.dispatch(updateTodo, { id: todo.id, patch: { title: todo.title } });

     try {
       await api.save(todo);
     } catch (error) {
       store.dispatch(updateTodo, { id: todo.id, patch: previous });
       notifications.error('Saving failed, changes were reverted');
       throw error;
     }
   }
   ```
3. Show a subtle status indicator (for example, `todo.pending = true`) by including an extra field in the patch and clearing it once the API responds.

### Checklist

* The UI reflects edits immediately, even before the API responds.
* Failed requests reapply the previous state via a second dispatch.
* Users see a toast or inline message when a rollback happens.

## 2. Persist slices of state to IndexedDB

**Goal:** Remember part of the global state between sessions without blocking the main thread.

### Steps

1. Create a persistence middleware:

   ```typescript
   import { MiddlewarePlacement, StoreMiddleware } from '@aurelia/state';

   const persistMiddleware: StoreMiddleware = store => next => action => {
     const result = next(action);
     const snapshot = store.getState();
     const subset = { filters: snapshot.filters };
     indexedDBStorage.save('app-cache', subset);
     return result;
   };
   ```
2. Hydrate the initial state from storage before registering the plugin:

   ```typescript
   const persisted = await indexedDBStorage.load('app-cache');
   const initialState = { ...defaultState, ...persisted };

   Aurelia.register(StateDefaultConfiguration.init(initialState, {
     middlewares: [{ middleware: persistMiddleware, placement: MiddlewarePlacement.After }]
   }, actionHandlers));
   ```

### Checklist

* Reloading the app restores the persisted filters without hitting the server.
* Only the desired slice is stored (inspect IndexedDB to confirm).
* Middleware placement `After` ensures the state snapshot reflects the latest changes.

## 3. Time-travel debugging / replay

**Goal:** Record every dispatched action so developers can scrub through state history or reproduce bugs.

### Steps

1. Add a logging middleware that pushes actions and snapshots into an array:

   ```typescript
   const timeline: { action: any; state: AppState }[] = [];

   const recordMiddleware: StoreMiddleware = store => next => action => {
     const result = next(action);
     timeline.push({ action, state: store.getState() });
     return result;
   };
   ```
2. Expose helper functions on a debugging service:

   ```typescript
   export class TimelineService {
     jumpTo(index: number) {
       const snapshot = timeline[index];
       if (snapshot) {
         store.dispatch(setState, snapshot.state);
       }
     }

     get entries() {
       return timeline;
     }
   }
   ```
3. Optionally wire it to Redux DevTools by enabling the built-in devtools middleware in `StateDefaultConfiguration` or `StoreConfiguration`.

### Checklist

* Timeline entries display in the browser console (or DevTools) for each dispatch.
* Calling `jumpTo(n)` restores the exact state at that point.
* Production builds can disable the middleware to reduce memory usage.

## 4. Micro-frontend friendly shared store

**Goal:** Let multiple Aurelia apps on the same page share a single store instance without clobbering each other.

### Steps

1. Create the store in a shared module and export the configured instance:

   ```typescript
   const configured = StoreDefaultConfiguration.init(initialState, actionHandlers);
   export const SharedStore = configured.register(new Container());
   ```
2. In each micro-frontend, register the already configured store instead of creating a new one:

   ```typescript
   Aurelia.register(SharedStore);
   ```
3. Namespaces actions or use tags to avoid collisions when different teams add handlers.

### Checklist

* All micro-frontends react to state changes regardless of which app dispatched the action.
* Only one store instance exists (inspect via logging middleware).
* Unmounting one micro-frontend leaves the store intact for the others.

## 5. Server-side pagination cache

**Goal:** Cache page results as users paginate through a large list so returning to earlier pages does not re-fetch data.

### Steps

1. Extend your state shape with a page cache and metadata:

   ```typescript
   interface AppState { pageCache: Record<number, User[]>; currentPage: number; }

   export const storePage = (state: AppState, { page, data }: { page: number; data: User[] }) => ({
     ...state,
     currentPage: page,
     pageCache: { ...state.pageCache, [page]: data }
   });
   ```
2. In your view-model, consult the cache before calling the API:

   ```typescript
   const cached = store.getState().pageCache[page];
   if (cached) {
     return cached;
   }

   const data = await api.fetchUsers(page);
   store.dispatch(storePage, { page, data });
   ```
3. Optionally add a `lastFetched` timestamp per page so you can invalidate stale entries.

### Checklist

* Navigating back to a previous page renders instantly without network calls.
* Cache invalidation logic (timestamp or manual clear) keeps data fresh when needed.
* The global state clearly indicates which page is currently shown.

## 6. Live WebSocket feed reducer

**Goal:** Keep a list of items in sync with a WebSocket stream while reusing the same dispatch pipeline.

### Steps

1. Create an action that inserts or updates items when messages arrive:

   ```typescript
   export const upsertQuote = (state: AppState, quote: Quote) => ({
     ...state,
     quotes: { ...state.quotes, [quote.id]: quote }
   });
   ```
2. Subscribe to the WebSocket once (for example in an `AppTask` or service) and dispatch on every message:

   ```typescript
   const socket = new WebSocket('/quotes');
   socket.addEventListener('message', event => {
     const quote = JSON.parse(event.data) as Quote;
     store.dispatch(upsertQuote, quote);
   });
   ```
3. Return a cleanup handle so you can close the socket when the app tears down.

### Checklist

* The state updates in near real time as messages arrive.
* Unmounting the feature (or navigating away) closes the socket to avoid leaks.
* UI bindings use `.state` or selectors so they react automatically to each dispatch.

## Reference material

* [State plugin guide](/aurelia-packages/state.md)
* [Store plugin guide](/aurelia-packages/store.md)
* [Middleware](/aurelia-packages/store/middleware.md)
* [Configuration and setup](/aurelia-packages/store/configuration-and-setup.md)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.aurelia.io/aurelia-packages/state/state-outcome-recipes.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
