Redux effects are middleware — thunks dispatching thunks, sagas yielding sagas, observables piping into more observables. Every async operation becomes a dispatch chain that resolves at arbitrary times with no ordering guarantee. SDuX Vault™ eliminates middleware entirely. Asynchronous inputs resolve through pipeline stages with serialized execution, deterministic ordering, and atomic state commitment.
The Middleware Problem
Redux was designed around synchronous reducer composition. When applications needed async operations — API calls, WebSocket messages, timer coordination — the core model had no answer. The community responded with middleware: thunks, sagas, and observables.
Each middleware layer intercepts dispatched actions and performs side effects before (or instead of) reaching the reducer. The result is a parallel execution system layered on top of the store:
// A thunk dispatches multiple actions at arbitrary times
function fetchUsers() {
return async (dispatch) => {
dispatch({ type: 'FETCH_USERS_START' });
try {
const response = await fetch('/api/users');
const users = await response.json();
dispatch({ type: 'FETCH_USERS_SUCCESS', payload: users });
} catch (error) {
dispatch({ type: 'FETCH_USERS_FAILURE', payload: error });
}
};
}
// Dispatch the thunk
dispatch(fetchUsers());
This pattern introduces three separate dispatches for a single logical operation. Each dispatch broadcasts to the entire reducer tree. The timing of the success or failure dispatch depends on network latency — which means ordering relative to other operations is non-deterministic.
// A saga yields effects that resolve at framework-controlled times
function* fetchUsersSaga() {
yield put({ type: 'FETCH_USERS_START' });
try {
const users = yield call(api.fetchUsers);
yield put({ type: 'FETCH_USERS_SUCCESS', payload: users });
} catch (error) {
yield put({ type: 'FETCH_USERS_FAILURE', payload: error });
}
}
// Root saga watches for trigger actions
function* rootSaga() {
yield takeLatest('FETCH_USERS_REQUESTED', fetchUsersSaga);
}
Sagas add a generator-based orchestration layer. They listen for actions, perform async work, and dispatch new actions. The complexity compounds: watchers, forks, races, channels — an entire concurrency framework layered on top of a state container.
⚠️ Warning: The fundamental issue is not the middleware libraries themselves. It is that Redux has no built-in model for async state resolution. Middleware exists because the core architecture cannot coordinate asynchronous inputs within its own execution model.
How Async Resolution Works in the SDuX Pipeline
SDuX Vault does not use middleware. Asynchronous input is handled through the Resolve stage — a core pipeline stage that normalizes all incoming inputs into a canonical value before downstream processing begins.
The Resolve stage accepts multiple input forms and resolves them under pipeline coordination:
| Input Form | Resolution |
|---|---|
| Plain state values | Passed through immediately |
| Deferred factories (functions returning Promises) | Invoked and awaited under pipeline control |
| Observable-based inputs | Subscribed and resolved within pipeline lifecycle |
| Structured state envelopes | Unwrapped and normalized |
| Angular HttpResourceRef (@sdux-vault/angular) | Observed until a concrete value emits, then resolved |
Regardless of how the input originates, the Resolve stage guarantees that downstream pipeline stages — operators, filters, reducers — always receive a predictable, normalized upstream value. No stage ever reasons about transport, timing, or source-specific concerns.
Key takeaway: The Resolve stage is a core pipeline stage. It is always present and executes automatically. You do not install or configure it. Every FeatureCell™ includes resolve behavior by default.
Resolve Behaviors vs Thunks
A Redux thunk dispatches new actions at arbitrary times. The store has no knowledge of when those actions will arrive or in what order. Multiple thunks executing concurrently can interleave their dispatches unpredictably.
In SDuX Vault, you submit a deferred factory directly to the owning FeatureCell. The pipeline resolves the async value under its own coordination — serialized through the conductor queue, committed atomically in a microtask boundary.
import { FeatureCell } from '@sdux-vault/core';
const userCell = FeatureCell('users', { value: [] });
// Submit a deferred factory — the pipeline resolves it
userCell.mergeState({
value: () => fetch('/api/users').then((r) => r.json())
});
Compare the two approaches side by side:
| Concern | Redux Thunk | SDuX Resolve |
|---|---|---|
| Dispatch count per operation | 3+ (start, success, failure) | 1 (single mergeState call) |
| Ordering guarantee | None — resolves at arbitrary times | Serialized through conductor queue |
| Reentrancy risk | Possible — dispatch during dispatch | Structurally impossible |
| State commitment | Immediate on each dispatch | Deferred to microtask boundary |
| Middleware required | Yes — redux-thunk | No — built into the pipeline |
The pipeline does not dispatch new actions after resolution. It resolves the input, processes it through operators, filters, and reducers, and commits the final state — all within a single pipeline execution. No secondary dispatch. No intermediate states visible to observers.
Controllers vs Saga Orchestration
Redux Sagas provide orchestration through generators — watching for actions, forking concurrent tasks, racing competing effects. The entire concurrency model lives outside the store in a parallel execution layer.
SDuX Vault provides orchestration through Controllers. Controllers govern execution authority for pipeline attempts — they evaluate whether an update is allowed, denied, or aborted before pipeline computation begins.
| Concern | Redux Saga | SDuX Controller |
|---|---|---|
| Execution layer | External middleware | Pipeline Policy stage |
| Coordination model | Generator-based (yield, fork, race) | Policy evaluation before computation |
| Relationship to state | Dispatches actions that reach reducers | Does not touch data — governs execution authority only |
| Concurrency handling | Manual (takeLatest, takeEvery, race) | Serialized by conductor queue |
| Testability | Generator stepping with mocked effects | act → settle → assert with vaultSettled |
Controllers do not dispatch, yield, or fork. They declare policy. The pipeline enforces that policy deterministically. This separation means your coordination logic never interleaves with your data transformation logic — they occupy different pipeline layers entirely.
Key takeaway: Controllers operate in the Policy Layer — the first stage of pipeline execution. They evaluate before any data processing occurs. Reducers operate in the Processing Layer, separated by multiple stage boundaries. The two concerns never mix.
The Execution Guarantee
Both thunks and sagas share a fundamental limitation: they dispatch actions at arbitrary times with no guarantee about ordering relative to other concurrent operations. Two thunks resolving simultaneously can interleave their dispatches. Two sagas forked in parallel can commit conflicting state.
SDuX Vault eliminates this entire category of bug through architectural constraints:
- Every pipeline attempt is serialized through a FIFO conductor queue — one at a time, deterministic order
- Pipeline computation is pure and side-effect free — no state mutation until computation completes
- State commitment is deferred to a microtask boundary — observers never see partial results
- Reentrancy is structurally impossible — no observer can trigger a new pipeline run while a commit is in progress
These are not conventions you must remember. They are architectural guarantees enforced by the runtime. You cannot accidentally bypass them.
Redux Comparison
Redux handles async through middleware that dispatches new actions at arbitrary times. SDuX Vault resolves async inputs through pipeline-coordinated stages with serialized execution.
| Dimension | Redux (Thunk/Saga/Observable) | SDuX |
|---|---|---|
| Async model | External middleware layer | Built-in Resolve stage |
| Coordination | Manual (takeLatest, debounce, race) | Pipeline-managed serialization |
| Side effect scope | Anywhere in middleware chain | Contained within pipeline lifecycle |
| Ordering guarantee | None without manual effort | FIFO queue ensures deterministic order |
| State visibility | Intermediate states visible between dispatches | Only final atomic snapshots are observable |
| Dependencies | redux-thunk, redux-saga, or redux-observable | None — resolve is a core pipeline stage |
Try It Yourself
Explore how SDuX Vault handles async state resolution without middleware:
Top comments (0)