---
name: mystique-data-flow
scope: frontend
description: "End-to-end data lifecycle: URL params, query variables, GraphQL execution, dataSource scoping, component data injection, template rendering"
load: on-demand
relevant-skills: [fluent-mystique-builder, fluent-mystique-validate, fluent-mystique-component, fluent-mystique-analyze, fluent-mystique-scaffold, fluent-frontend-review]
---

# Mystique Data Flow — End-to-End Reference

Single canonical reference for how data moves through the Mystique UI framework, from URL navigation to rendered component output.

---

## 1. Data Lifecycle Overview

```
URL Navigation (#/orders/123)
    |
    v
Route Matching (path: "orders/:id")
    |
    v
Variable Resolution ({{params.id}} -> "123", {{activeContext.id}} -> "2")
    |
    v
GraphQL Execution (query orderById($id: ID!) { ... })
    |
    v
Page Data Context ({ orderById: { id: "123", ref: "ORD-001", ... } })
    |
    v
dataSource Scoping (component.dataSource = "orderById" -> data = { id, ref, ... })
    |
    v
StdProps.data Injection (component receives scoped data as props.data)
    |
    v
Template Rendering ("{{ref}}" -> "ORD-001")
```

Each stage is described in detail below.

---

## 2. Stage 1 — Route Matching and Variable Resolution

### URL parameters

Route params defined with `:paramName` are accessed via `{{params.paramName}}`:

```json
{
  "path": "orders/:id/:retailerId",
  "data": {
    "query": "query($id: ID!, $retailerId: ID!) { orderById(id: $id) { ref status } }",
    "variables": {
      "id": "{{params.id}}",
      "retailerId": "{{params.retailerId}}"
    }
  }
}
```

**CRITICAL: Use `{{params.id}}`, NOT `{{id}}`.** Without `params.`, the expression resolves against the parent data context (not the URL), which is empty at route level.

### Context variables (no query needed)

These are always available without any GraphQL query:

| Variable | Access | Description |
|----------|--------|-------------|
| User | `{{activeUser.username}}`, `{{activeUser.firstName}}`, `{{activeUser.timezone}}` | Logged-in user |
| Retailer | `{{activeRetailer.id}}`, `{{activeRetailer.ref}}` | Current retailer |
| Context | `{{activeContext.id}}`, `{{activeContext.ref}}` | Current retailer or location |
| Location | `{{activeLocation.id}}`, `{{activeLocation.ref}}` | Current location (Store app) |

### Query string parameters

Filter values from URL query strings use the same `params` prefix:

```json
{
  "variables": {
    "status": "{{params.filter_status}}",
    "fromDate": "{{params.filter_from}}"
  }
}
```

---

## 3. Stage 2 — GraphQL Execution

### Single data source (common)

```json
{
  "data": {
    "query": "query($id: ID!) { orderById(id: $id) { id ref status customer { firstName lastName } items { edges { node { ref quantity } } } } }",
    "variables": { "id": "{{params.id}}" }
  }
}
```

The query result becomes the **page data context**. The top-level field name (`orderById`) is how components reference this data via `dataSource`.

### Multiple data sources (`dataSources` — plural)

When a page needs data from multiple independent queries, use `dataSources`:

```json
{
  "dataSources": {
    "order": {
      "query": "query($id: ID!) { orderById(id: $id) { id ref status } }",
      "variables": { "id": "{{params.id}}" }
    },
    "me": {
      "query": "query { me { username primaryRetailer { id } roles { edges { node { role { name } } } } } }"
    }
  }
}
```

Each key in `dataSources` becomes a named entry in the page data context. Queries execute independently and in parallel. Components reference them via `dataSource: "order"` or `dataSource: "me"`.

### `data` vs `dataSources` — which to use

| Form | When | Notes |
|------|------|-------|
| `data: { query, variables }` | Page needs a single query | The query result fields become direct page data context keys |
| `dataSources: { name: { query, variables } }` | Page needs multiple independent queries | Each `name` becomes a page data context key; queries run in parallel |

**Convention:** Use `dataSources` (plural) when a page has user actions (needs `me` query alongside the entity query). Use `data` (singular) for simple single-entity pages.

### Query result naming

The **GraphQL root field name** determines how components reference the data:

```
Query: query { orderById(id: $id) { ref } }
Result: { orderById: { ref: "ORD-001" } }
Component: dataSource="orderById"   -->  data = { ref: "ORD-001" }

Query: query { orders(first: 50) { edges { node { ref } } } }
Result: { orders: { edges: [...] } }
Component: dataSource="orders"       -->  data = { edges: [...] }
```

With `dataSources`, the **key name** (not the query root) is used:

```
dataSources: { "recentOrders": { query: "query { orders(...) { ... } }" } }
Component: dataSource="recentOrders"   -->  data = query result of recentOrders
```

This is how you can have two `orders` queries on the same page — give them different keys in `dataSources`.

---

## 4. Stage 3 — dataSource Scoping (Parent to Child)

### How dataSource works

The `dataSource` prop on a component instance (outside `props`) scopes the data context for that component and its descendants. It is a **dot-notation JSONPath** into the page data context.

```json
{
  "component": "fc.card.attribute",
  "dataSource": "orderById",
  "props": {
    "attributes": [
      { "label": "Ref", "value": "{{ref}}" }
    ]
  }
}
```

Here `{{ref}}` resolves to `orderById.ref` because `dataSource` scopes the component's data to `orderById`.

### Nested scoping

Components can drill deeper into the data:

```json
{
  "component": "fc.card.attribute",
  "dataSource": "orderById.customer",
  "props": {
    "attributes": [
      { "label": "Name", "value": "{{firstName}} {{lastName}}" }
    ]
  }
}
```

### Scoping chain (ancestor inheritance)

When components are nested, descendant `dataSource` paths are resolved against the **current scoped context**, not re-evaluated from the page root. Mystique also keeps the accumulated path in `useData().context.dataSource`.

```
Page data: { orderById: { ref: "ORD-001", items: { edges: [...] }, customer: { firstName: "Jane" } } }

fc.page (no dataSource)                      --> data = entire page result
  fc.page.section (dataSource="orderById")  --> data = { ref, items, customer }
    fc.list (dataSource="items")            --> data = { edges: [...] }
    fc.card.attribute (dataSource="customer") --> data = { firstName: "Jane" }
```

If a nested component repeats the full root path (for example `orderById.customer` under a parent already scoped to `orderById`), that path will usually not resolve as intended.

### No dataSource = full page data

Components without a `dataSource` prop receive the full page data context. This is common for page-level components and layout wrappers.

### Placement rule (CRITICAL)

`dataSource` MUST be a sibling of `component` and `props`, NEVER inside `props`:

```json
// CORRECT
{ "component": "fc.list", "dataSource": "items", "props": { "attributes": [...] } }

// WRONG — silent failure, component renders empty
{ "component": "fc.list", "props": { "dataSource": "items", "attributes": [...] } }
```

This is the #1 manifest wiring mistake. Validated by rule S16.

### Path resolution algorithm

1. Start with the full page data context object
2. Split `dataSource` value by `.` (e.g., `"orderById.items"` -> `["orderById", "items"]`)
3. Walk into each segment: `context["orderById"]["items"]`
4. The result becomes the component's `data` prop
5. If any segment resolves to `undefined`, the component receives `undefined` as data (renders empty, no error)
6. Empty string `""` or omitted `dataSource` = full page data context

### Connection handling

For list components binding to Relay connections, use the connection path without `.edges`:

```json
{
  "component": "fc.list",
  "dataSource": "orderById.items"
}
```

The framework handles `.edges[].node` traversal automatically. Do NOT include `.edges` in the dataSource path for `fc.list`.

---

## 5. Stage 4 — Component Data Injection

### StdProps interface

All registered components receive:

```typescript
interface StdProps {
  data?: any;  // Page query data, scoped by dataSource chain
}
```

The `data` prop is **automatically injected** by the Mystique host. Custom props from the manifest `props` block are merged alongside it.

### Data injection per component category

```
Page Query Result
    |
    v
page component    <-- receives FULL query result as data
    |
    +-- layout component   <-- receives data scoped by its dataSource
    |       |
    |       +-- content component  <-- receives data scoped by its dataSource
    |       |
    |       +-- content component  <-- receives data scoped by its dataSource
    |
    +-- filter component   <-- reads/writes variables via useData().variables.setVariables()
```

| Category | `data` contains | Key mechanism |
|----------|----------------|---------------|
| `page` | Full page query result (unscoped) | `useData()`, `useUserActions()`, `<Children />` |
| `layout` | Scoped by parent `dataSource` | `<Children />`, `<Children dataOverride={...} />` |
| `content` | Scoped by parent `dataSource` | `StdProps.data`, `TemplateRegistry.render()` |
| `filter` | Scoped by parent `dataSource` | `useData().variables.setVariables(newVars, merge)` |

### FieldRegistry fields — NO page data

FieldRegistry fields in user action dialogs are rendered outside the page's React data context provider. Both `data` prop and `useData()` return empty/undefined. Fields that need entity data MUST use `useQuery()`:

```typescript
const MyField: FC<FormFieldProps<string>> = ({ entityContext, onChange }) => {
  const entityId = entityContext?.[0]?.entity?.id ?? null;
  const [result] = useQuery(MY_QUERY, entityId ? { id: entityId } : undefined);
  // ...
};
```

---

## 6. Stage 5 — Template Rendering

Template expressions use Handlebars syntax and evaluate against the component's scoped data context:

```json
{
  "value": "{{ref}}",
  "link": "#/orders/{{id}}",
  "condition": "{{and customer.id}}"
}
```

### Template evaluation context

- Inside `fc.list` attributes: templates evaluate per-row. Use `node.` prefix for row fields: `{{node.ref}}`, `{{node.status}}`
- Inside `fc.card.attribute`: templates evaluate against the component's scoped data
- Inside `fc.page` props (e.g., `title`): templates evaluate against full page data

### Attribute access

Entity attributes are arrays in GraphQL. Use `byName` shorthand:

```json
"{{order.attributes.byName.vipStatus}}"
```

NEVER use array index access (`attributes[0].value`) — attribute ordering is not guaranteed.

---

## 7. Mid-Tree Data Providers

### fc.provider.graphql

When a subtree of components needs data from a different query than the page query, use `fc.provider.graphql` instead of custom `useQuery()`:

```json
{
  "component": "fc.provider.graphql",
  "props": {
    "query": "query($ref: [String]) { inventoryPositions(productRef: $ref) { edges { node { ref qty status } } } }",
    "variables": { "ref": "{{product.ref}}" },
    "path": "inventoryPositions",
    "max": 100
  },
  "descendants": [
    {
      "component": "fc.list",
      "dataSource": "inventoryPositions",
      "props": {
        "attributes": [
          { "label": "Location", "value": "{{node.ref}}" },
          { "label": "Quantity", "value": "{{node.qty}}" }
        ]
      }
    }
  ]
}
```

| Prop | Required | Description |
|------|----------|-------------|
| `query` | Yes | GraphQL query string |
| `variables` | No | Query variables (supports templates from parent data context) |
| `path` | Yes | JSONPath into query result that becomes descendant data context |
| `max` | No | Max records to fetch (default varies) |
| `loadingBehavior` | No | Loading indicator strategy |

Descendants inside the provider receive the query result scoped by `path` as their data context. They can further scope with their own `dataSource`.

### fc.repeater

Iterates over an array or connection and renders descendants once per item:

```json
{
  "component": "fc.repeater",
  "dataSource": "orderById.items",
  "descendants": [
    {
      "component": "fc.card.attribute",
      "props": {
        "title": "Item {{node.ref}}",
        "width": "half",
        "attributes": [
          { "label": "Product", "value": "{{node.product.ref}}" },
          { "label": "Qty", "value": "{{node.quantity}}" }
        ]
      }
    }
  ]
}
```

**Data scoping inside fc.repeater:**
- `dataSource` on the repeater points to an array or connection (e.g., `orderById.items`)
- For each iteration, descendants receive the current item as their data context
- For connections (`edges[].node`), the framework auto-unwraps — each iteration's data is the `node` object
- Use `{{node.ref}}` in list attribute templates, but inside repeater descendants the fields are directly available: `{{ref}}`, `{{quantity}}`
- To access both the repeated item AND parent data, use `fc.provider.graphql` or a custom layout component with `<Children dataOverride />`

---

## 8. Data Fetch Strategy — Tier Hierarchy

When building new components or pages, choose the cheapest data access tier:

### Tier 1 — Free (zero network cost)

| Source | Hook/Mechanism | Use for |
|--------|---------------|---------|
| Page query data | `StdProps.data` / `useData()` | Entity fields already in the page query |
| User session | `useAuth()` | Username, roles, context ID |
| Settings (cached) | `useSettings()` | Feature flags, config, dropdown options |
| Context | `{{activeContext.*}}` in templates | Retailer/location scoping |

### Tier 2 — Cheap (expand existing query)

Instead of adding `useQuery()`, add fields to the page-level `data.query` in the manifest. One query, shared across all descendants via `dataSource`.

### Tier 3 — Expensive (separate API call)

| Source | Hook | Use ONLY when |
|--------|------|---------------|
| `useQuery()` | Component-level GraphQL | Data from a different entity not in page query |
| `useRest()` / `getRest()` | REST endpoint | Non-GraphQL APIs (label streams, event sync) |

**Anti-pattern: N+1 queries.** A component inside `fc.list` or `fc.repeater` that calls `useQuery()` fires a separate API call PER ROW. There is no shared query cache. Use `fc.provider.graphql` or expand the page query instead.

### Filter component variable updates

Filter components modify the page query variables, triggering a re-fetch:

```typescript
const { variables } = useData();
// true = MERGE with existing variables; false = REPLACE all variables
variables.setVariables({ status: 'ACTIVE' }, true);
```

The second argument controls merge behavior:
- `true` — merge `newVars` into existing variables (preserves other filters)
- `false` — replace all variables with `newVars` (resets other filters)

When variables change, the page query re-executes and all sibling components re-render with the new data.

---

## 9. Cross-Fragment Data Sharing

Fragments loaded by the same app share a single route space. Links can target pages in ANY loaded fragment using `#/` prefix.

**Data isolation:** Each page defines its own `data` or `dataSources`. Fragments do NOT share data contexts — a component in fragment A cannot access a query defined in fragment B's page. If two fragments need the same data on different pages, each page must define its own query.

**Shared queries across fragments:** If multiple pages across fragments need the same query structure, extract the query string into a Fluent setting and reference it. This avoids drift but does not reduce API calls — each page still executes its own copy.

---

## 10. Common Mistakes

| Mistake | Symptom | Fix |
|---------|---------|-----|
| `dataSource` inside `props` | Component renders empty, no error | Move `dataSource` to sibling of `component` |
| `dataSource: "orderById"` when query returns `order` | Empty data, no error | Match dataSource to the actual GraphQL root field name |
| `{{id}}` instead of `{{params.id}}` in variables | Query fails or returns wrong data | Use `params.` prefix for URL parameters |
| `useQuery()` in content component when data is in page query | Extra API call per component | Use `StdProps.data` with `dataSource` scoping |
| `useQuery()` inside list row component | N+1 API calls (one per row) | Use `fc.provider.graphql` or expand page query |
| Missing `me` query when `actions: { enabled: true }` | User action buttons don't appear | Add `me` to `dataSources` |
| `attributes[0].value` for entity attributes | Wrong value when attribute order changes | Use `attributes.byName.fieldName` |
| Missing `_locale='en'` on date helpers in variables | Non-ASCII digits break backend parsing | Add `_locale='en'` to date helpers in query variables |
