# The configurator client store

> > Scope: configbox/store — the configurator page's client-side state: the tree, the actions, the > selectors, the legacy bridge, and how customization JavaSc…

Source: CBX documentation, version 4.0 preview (unreleased). Canonical page: https://docs.configbox.at/docs/4.0-preview/technical/configurator_store. Last updated 2026-08-25.

---
> **Scope:** `configbox/store` — the configurator page's client-side state: the tree, the actions, the
> selectors, the legacy bridge, and how customization JavaScript should read and watch it ·
> **Last reviewed:** 2026-08-25

The configurator page's client state has a single owner: a small Redux-style store in the AMD module
**`configbox/store`** (`assets/javascript/store.js`). This page is the reference for it. For the
round-trip it participates in — how a click becomes a server call and a re-render — read
[`com_configbox_configurator_questions.md`](https://docs.configbox.at/docs/4.0-preview/technical/configurator_questions) first; this page
is only about the state.

---

## 1. Why it exists

The state used to be a single mutable JSON blob in the jQuery `.data('json')` store of
`#configurator-data`, poked directly through `getConfiguratorData()` / `setConfiguratorDataItem()` /
`replaceConfiguratorData()`.

It worked, and it drifted. The blob was effectively **write-once**: interactions mutated the DOM and
fired events, but nothing owned the transitions, so the state object fell out of step with what the
page actually showed — a required question got answered and `missingProductSelections` never changed.
Every consumer then had a slightly different idea of the truth.

The store fixes that by making transitions the only way state changes:

- **One immutable tree.** Reducers never mutate; they shallow-copy and return a new tree.
- **`dispatch(action)` is the only writer.** Every transition is a named, inspectable action.
- **Reads go through `getState()` / `select(selector)`.**
- **`subscribe(listener)`** fires after every committed change and returns an unsubscribe function.

It is a minimal Redux written by hand — plain ES5 AMD, no dependency and no bundler, to fit this
codebase (see [`../customization/com_configbox_assets_and_amd.md`](https://docs.configbox.at/docs/4.0-preview/customization/assets_and_amd)).

## 2. The state tree

The tree keeps **every top-level key of the server-rendered seed blob** (`dateFormat`,
`pageSequence`, `cartPositionId`, …) so the legacy accessors resolve unchanged, and overlays the
store-owned containers on top:

```js
{
  …every original blob key…,

  entities: {
    questions: { "413": { … }, "414": { … } }   // normalised, by id
  },
  selections: {                                  // per question, filled as selections happen
    "413": { selection, outputValue, selectedBy }
  },
  missingPageSelections:    [ …question ids… ],  // guaranteed to exist
  missingProductSelections: [ …question ids… ],  // guaranteed to exist
  pricing: { … }
}
```

Two things are worth knowing about that shape. **Questions are normalised by id**, so a selector never
has to scan an array or touch the DOM to find one. And the required-selection arrays are **guaranteed
to exist** even when the seed omitted them, so selectors cannot trip on `undefined`.

## 3. The API

```js
cbrequire(['configbox/store'], function (store) {
    var s = store.getStore();          // the page's singleton store

    s.getState();                      // the current tree
    s.select(store.selectors.pricing); // run a selector against it
    s.dispatch(store.actions.setPricing(newPricing));

    var off = s.subscribe(function () { /* fired after every committed change */ });
    off();                             // unsubscribe
});
```

The module also exports `createStore(reducer, preloadedState)`, `resetStore()`, `reducer`, `actions`,
`selectors`, `ActionTypes`, `buildInitialState()` and `toLegacySnapshot()` — the lower half exists for
tests and for advanced use; day to day you want `getStore()`, the selectors and the actions.

### Actions

| Action type | Creator | Payload |
|---|---|---|
| `configbox/INIT` | `init(blob)` | Seed the whole tree from the server-rendered blob |
| `configbox/REPLACE` | `replace(blob)` | Replace the whole tree (legacy `replaceConfiguratorData`) |
| `configbox/SET_ITEM` | `setItem(key, value)` | One top-level key (legacy `setConfiguratorDataItem`) |
| `configbox/SET_SELECTION` | `setSelection(questionId, selection, outputValue, selectedBy)` | A question's current selection |
| `configbox/SET_PRICING` | `setPricing(pricing)` | The pricing block |
| `configbox/SET_MISSING_PAGE` | `setMissingPageSelections(ids)` | Unanswered required questions on this page |
| `configbox/SET_MISSING_PRODUCT` | `setMissingProductSelections(ids)` | Unanswered required questions in the product |
| `configbox/MERGE_SERVER_RESPONSE` | `mergeServerResponse(data)` | A whole configurator server response |

**`MERGE_SERVER_RESPONSE` is the one that matters.** Every configurator round-trip folds into the tree
through it, atomically — which is precisely what the old blob could not do, and why the state now stays
in step with the page across interactions.

### Selectors

`questions(state)` · `question(state, id)` · `questionProp(state, id, prop)` ·
`questionHasProp(state, id, prop)` · `selection(state, id)` · `pricing(state)` ·
`missingPageSelections(state)` · `missingProductSelections(state)`

`questionProp()` **throws** on an unknown question or property — deliberately, because that is the
legacy accessor's contract and code depended on it. Use `questionHasProp()` when absence is a normal
case rather than a bug.

## 4. The legacy bridge — and the one real gotcha

Nothing was broken to add this. The classic accessors — `configurator.getConfiguratorData()`,
`setConfiguratorDataItem()`, `replaceConfiguratorData()`, `getQuestionPropValue()`,
`questionHasProperty()` — are now **thin shims over the store**. And on every commit the store mirrors
a flat snapshot back into `#configurator-data`'s jQuery `.data('json')`, so code that still reads the
blob directly keeps seeing an up-to-date object. The normalised `entities` / `selections` containers
ride along under `_store` on that snapshot for anyone who wants them.

> **The gotcha:** the DOM **`data-json` *attribute*** on `#configurator-data` is only the **frozen
> initial seed**. It is not updated. Read the store, a selector, or the jQuery `.data('json')` mirror
> — never the attribute — for current state. This has bitten both customization code and tests.

> **jQuery is AMD-scoped.** Declare `cbj` in your module's `define([...])` list; `window.jQuery` and
> `$` are not the copies this component uses.

## 5. Using it from customization JavaScript

```js
define(['cbj', 'configbox/store'], function (cbj, store) {
    'use strict';

    var s = store.getStore();

    // React to price changes without polling or guessing at events.
    var previous = s.select(store.selectors.pricing);
    s.subscribe(function () {
        var now = s.select(store.selectors.pricing);
        if (now !== previous) {
            previous = now;
            // … update your own UI …
        }
    });
});
```

Two rules: **subscribe, do not poll** — the listener fires after every committed change; and **never
mutate what `getState()` hands back** — dispatch an action instead, or the next reducer will build its
new tree from a tree somebody else has already edited.

- [Registering and loading custom JS modules](https://docs.configbox.at/docs/4.0-preview/customization/assets_and_amd)
- [Adding a question type](https://docs.configbox.at/docs/4.0-preview/customization/custom_question_types)

## 6. Testing

`resetStore()` drops the singleton so the next `getStore()` rebuilds from the DOM seed — which is what
a test wants between cases. `createStore(reducer, preloadedState)` builds an isolated store with no DOM
mirror at all, for reducer and selector unit tests.

When driving the configurator from a browser test, wait for the page's readiness marker before
reaching into the store — see the readiness markers in
[`com_configbox_kenedo_view.md`](https://docs.configbox.at/docs/4.0-preview/technical/kenedo_view#51-the-wrapper-carries-the-manifest--getviewattributes-906),
and guard the AMD global itself (`typeof window.cbrequire === 'function'`) on cold loads.

## 7. See also

- [`com_configbox_configurator_questions.md`](https://docs.configbox.at/docs/4.0-preview/technical/configurator_questions) — the whole
  configurator loop, server and client
- [`com_configbox_frontend_requests.md`](https://docs.configbox.at/docs/4.0-preview/technical/frontend_requests) — how the page talks to
  the server
- [`com_configbox_runtime_api.md`](https://docs.configbox.at/docs/4.0-preview/technical/runtime_api) — the same round-trip without a browser
- [`../customization/com_configbox_assets_and_amd.md`](https://docs.configbox.at/docs/4.0-preview/customization/assets_and_amd)
  — the AMD loader and view initialization
- `assets/javascript/store.js` — the module itself, fully documented inline
