# DASH ANTIPATTERNS - xFarma Frontend
## Antipatrones Detectados y Troubleshooting

> **Propósito**: Catálogo completo de antipatrones Dash con detección automática, ejemplos y soluciones.
>
> **Para validación automática**: `python frontend/utils/dash_component_validator.py --all`

---

## Índice

- [DASH001: Listas sin Wrapper](#dash001-listas-sin-wrapper)
- [DASH002: Outputs Duplicados](#dash002-outputs-duplicados)
- [DASH003: callback_context Deprecado](#dash003-callback_context-deprecado)
- [DASH004: Imports Circulares](#dash004-imports-circulares)
- [DASH005: Atributos HTML Incorrectos](#dash005-atributos-html-incorrectos)
- [DASH006: Callbacks sin Auth Check](#dash006-callbacks-sin-auth-check)
- [Troubleshooting Común](#troubleshooting-común)

---

## DASH001: Listas sin Wrapper

### Descripción del Problema

Dash NO permite pasar listas directamente como `children` de componentes. React requiere que múltiples children estén envueltos en un componente contenedor.

### Por Qué Ocurre

```python
# ❌ INCORRECTO: Lista directa
dbc.Button(
    children=[
        html.Span("Icon"),
        html.Span("Text")
    ]
)
```

**Error en browser console**:
```
Warning: Each child in a list should have a unique "key" prop.
Error: Objects are not valid as a React child (found: object with keys {props, type, namespace}).
```

### Detección Automática

```bash
python frontend/utils/dash_component_validator.py --all
```

**Output**:
```
[DASH001] components/upload.py:45 - Lista sin wrapper en dbc.Button
  children=[html.Span(...), html.Span(...)]
  Fix: Envolver en html.Div() o usar Fragment
```

### Solución

#### Opción 1: Wrapper con `html.Div`

```python
# ✅ CORRECTO: Wrapper explícito
dbc.Button(
    children=html.Div([
        html.Span("📁", className="me-2"),
        html.Span("Subir Archivo")
    ])
)
```

#### Opción 2: Children Único

```python
# ✅ CORRECTO: Un solo child (concatenar texto)
dbc.Button(
    children="📁 Subir Archivo"
)
```

#### Opción 3: Dash Fragment (Dash 2.9+)

```python
from dash import html, dcc

# ✅ CORRECTO: Fragment (wrapper invisible)
dbc.Button(
    children=html.Fragment([
        html.Span("📁", className="me-2"),
        html.Span("Subir Archivo")
    ])
)
```

### Ejemplos Reales

#### Botón con Icono y Texto

```python
# ❌ INCORRECTO
dbc.Button([
    html.I(className="fas fa-upload me-2"),
    "Subir"
], id="upload-btn")

# ✅ CORRECTO
dbc.Button(
    children=html.Div([
        html.I(className="fas fa-upload me-2"),
        html.Span("Subir")
    ]),
    id="upload-btn"
)
```

#### Card con Header y Body

```python
# ❌ INCORRECTO
dbc.Card([
    dbc.CardHeader("Título"),
    dbc.CardBody("Contenido")
])

# ✅ CORRECTO: Componentes dbc.Card* son excepciones (diseñados para listas)
dbc.Card([
    dbc.CardHeader("Título"),
    dbc.CardBody("Contenido")
])
# Nota: dbc.Card acepta lista de CardHeader/CardBody por diseño
```

### Cuándo Es Válido No Usar Wrapper

**Componentes que ACEPTAN listas**:
- `dbc.Card` (con `dbc.CardHeader`, `dbc.CardBody`)
- `dbc.Accordion` (con `dbc.AccordionItem`)
- `html.Div`, `html.Span` (contenedores genéricos)
- `dbc.Container`, `dbc.Row`, `dbc.Col` (layout)

**Componentes que NO ACEPTAN listas**:
- `dbc.Button`
- `dbc.Badge`
- `html.A`
- `html.P`, `html.H1-H6`

---

## DASH002: Outputs Duplicados

### Descripción del Problema

Dos o más callbacks intentan escribir al mismo Output sin usar `allow_duplicate`.

### Por Qué Ocurre

```python
# ❌ INCORRECTO: Dos callbacks, mismo Output
@app.callback(Output('result', 'children'), Input('btn1', 'n_clicks'))
def update_from_btn1(n):
    return "From Button 1"

@app.callback(Output('result', 'children'), Input('btn2', 'n_clicks'))
def update_from_btn2(n):
    return "From Button 2"
```

**Error al arrancar la app**:
```
dash.exceptions.DuplicateCallbackOutput:
You have already assigned a callback to the output with ID "result" and property "children".
```

### Detección Automática

```bash
python frontend/utils/validate_callbacks.py
```

**Output**:
```
[ERROR] Duplicate Output detected:
  - update_from_btn1: Output('result', 'children')
  - update_from_btn2: Output('result', 'children')
```

### Solución

#### Opción 1: Un Callback con Múltiples Inputs

```python
# ✅ CORRECTO: Un callback escucha múltiples Inputs
from dash import ctx

@app.callback(
    Output('result', 'children'),
    [Input('btn1', 'n_clicks'), Input('btn2', 'n_clicks')]
)
def update_from_buttons(n1, n2):
    triggered_id = ctx.triggered_id

    if triggered_id == 'btn1':
        return "From Button 1"
    elif triggered_id == 'btn2':
        return "From Button 2"

    return "No button clicked yet"
```

#### Opción 2: allow_duplicate (Casos Específicos)

```python
# ✅ CORRECTO: allow_duplicate para casos especiales
@app.callback(
    Output('cache-store', 'data', allow_duplicate=True),
    Input('btn1', 'n_clicks'),
    State('cache-store', 'data'),
    prevent_initial_call=True  # ← OBLIGATORIO con allow_duplicate
)
def update_cache_from_btn1(n, cache):
    cache['btn1_clicks'] = n
    return cache

@app.callback(
    Output('cache-store', 'data', allow_duplicate=True),
    Input('btn2', 'n_clicks'),
    State('cache-store', 'data'),
    prevent_initial_call=True  # ← OBLIGATORIO con allow_duplicate
)
def update_cache_from_btn2(n, cache):
    cache['btn2_clicks'] = n
    return cache
```

**Cuándo usar `allow_duplicate`**:
- ✅ Callbacks que actualizan partes diferentes de un store
- ✅ Callbacks con lifecycles independientes (uno en page load, otro en user action)
- ❌ NO usar como workaround para mala arquitectura

**Requisito obligatorio**: `prevent_initial_call=True` con `allow_duplicate=True`

### Ejemplos Reales

#### Store con Múltiples Escritores

```python
# ❌ INCORRECTO: Sin allow_duplicate
@app.callback(
    Output('partners-store', 'data'),
    Input('save-btn', 'n_clicks')
)
def save_partners(n):
    return {"selected": [...]}

@app.callback(
    Output('partners-store', 'data'),  # ← Duplicate!
    Input('reset-btn', 'n_clicks')
)
def reset_partners(n):
    return {"selected": []}

# ✅ CORRECTO: Un callback con múltiples Inputs
@app.callback(
    Output('partners-store', 'data'),
    [Input('save-btn', 'n_clicks'), Input('reset-btn', 'n_clicks')],
    State('partners-dropdown', 'value')
)
def manage_partners(save_n, reset_n, selected):
    if ctx.triggered_id == 'save-btn':
        return {"selected": selected}
    elif ctx.triggered_id == 'reset-btn':
        return {"selected": []}
    return dash.no_update
```

---

## DASH003: callback_context Deprecado

### Descripción del Problema

Uso de `dash.callback_context` (API deprecada en Dash 2.0+) en lugar de `from dash import ctx`.

### Por Qué Ocurre

```python
# ❌ INCORRECTO: API deprecada
from dash import dash

@app.callback(...)
def my_callback(...):
    ctx = dash.callback_context
    triggered_id = ctx.triggered[0]['prop_id'].split('.')[0]
```

**Warning en logs**:
```
DeprecationWarning: dash.callback_context is deprecated. Use 'from dash import ctx' instead.
```

### Detección Automática

```bash
python frontend/utils/dash_component_validator.py --check DASH003
```

**Output**:
```
[DASH003] callbacks/dashboard.py:78 - Uso de dash.callback_context deprecado
  Fix: Reemplazar con 'from dash import ctx'
```

### Solución

```python
# ✅ CORRECTO: API moderna
from dash import ctx, callback, Input, Output

@callback(
    Output('result', 'children'),
    [Input('btn1', 'n_clicks'), Input('btn2', 'n_clicks')]
)
def my_callback(n1, n2):
    # ✅ Usar ctx directamente
    triggered_id = ctx.triggered_id  # Más simple y directo

    if triggered_id == 'btn1':
        return "Button 1"
    elif triggered_id == 'btn2':
        return "Button 2"

    return dash.no_update
```

### Migración Completa

#### Antes (Dash 1.x)

```python
from dash import dash

@app.callback(...)
def my_callback(...):
    ctx = dash.callback_context

    # Obtener triggered ID (verboso)
    if ctx.triggered:
        prop_id = ctx.triggered[0]['prop_id']
        triggered_id = prop_id.split('.')[0]
    else:
        triggered_id = None

    # Obtener triggered property
    if ctx.triggered:
        triggered_prop = ctx.triggered[0]['prop_id'].split('.')[1]
    else:
        triggered_prop = None
```

#### Después (Dash 2.0+)

```python
from dash import ctx

@callback(...)
def my_callback(...):
    # ✅ Acceso directo y simple
    triggered_id = ctx.triggered_id  # 'btn1' o None
    triggered_prop = ctx.triggered_prop_ids  # {'btn1.n_clicks': 5}

    # Verificar si hubo trigger
    if not ctx.triggered:
        raise PreventUpdate
```

### Propiedades de `ctx`

| Propiedad | Descripción | Ejemplo |
|-----------|-------------|---------|
| `ctx.triggered` | Lista de triggers | `[{'prop_id': 'btn.n_clicks', 'value': 1}]` |
| `ctx.triggered_id` | ID del componente que disparó | `'btn'` |
| `ctx.triggered_prop_ids` | Dict de prop_ids y valores | `{'btn.n_clicks': 1}` |
| `ctx.inputs` | Dict de todos los Inputs | `{'btn.n_clicks': 1, 'input.value': 'text'}` |
| `ctx.states` | Dict de todos los States | `{'store.data': {...}}` |
| `ctx.outputs_list` | Lista de Outputs | `['result.children']` |

---

## DASH004: Imports Circulares

### Descripción del Problema

Dos o más módulos se importan mutuamente, causando `ImportError` o comportamiento indefinido.

### Por Qué Ocurre

```python
# callbacks/dashboard.py
from layouts.dashboard import create_layout  # ← Import A

# layouts/dashboard.py
from callbacks.dashboard import register_callbacks  # ← Import B (circular!)
```

**Error**:
```
ImportError: cannot import name 'create_layout' from partially initialized module 'layouts.dashboard'
```

### Detección Automática

```bash
python frontend/utils/dash_component_validator.py --check DASH004
```

### Solución

#### Opción 1: Lazy Import

```python
# layouts/dashboard.py
def create_layout():
    # ✅ Importar solo cuando se usa
    from callbacks.dashboard import some_helper

    result = some_helper()
    return html.Div(result)
```

#### Opción 2: Separar Lógica Compartida

```python
# utils/dashboard_helpers.py (nuevo archivo)
def shared_logic():
    return "shared data"

# callbacks/dashboard.py
from utils.dashboard_helpers import shared_logic

# layouts/dashboard.py
from utils.dashboard_helpers import shared_logic
```

#### Opción 3: Arquitectura de Registro

```python
# app.py (central)
from layouts.dashboard import create_layout
from callbacks.dashboard import register_callbacks

# 1. Crear app
app = dash.Dash(__name__)

# 2. Registrar callbacks (SIN importar layouts)
register_callbacks(app)

# 3. Routing usa layouts (SIN importar callbacks)
@app.callback(...)
def display_page(pathname):
    if pathname == '/dashboard':
        return create_layout()
```

---

## DASH005: Atributos HTML Incorrectos

### Descripción del Problema

Uso de atributos HTML puros (como `aria-label`) en componentes `dbc.*` que tienen equivalentes Dash/React.

### Por Qué Ocurre

```python
# ❌ INCORRECTO: aria-label en dbc.Button
dbc.Button("Click me", aria-label="My Button")  # ← No funciona como esperado
```

Dash Bootstrap Components (dbc) usa props de React, no atributos HTML directos.

### Detección Automática

```bash
python frontend/utils/dash_component_validator.py --check DASH005
```

### Solución

```python
# ✅ CORRECTO: Usar prop 'title' en su lugar
dbc.Button("Click me", title="My Button")  # ← Genera tooltip + accessibility

# ✅ CORRECTO: Para accessibility, usar html.* con aria-label
html.Button("Click me", **{"aria-label": "My Button"})  # ← Requiere ** unpacking
```

### Equivalencias Comunes

| HTML Atributo | dbc.* Prop | html.* Atributo |
|---------------|------------|-----------------|
| `aria-label` | `title` | `**{"aria-label": "..."}` |
| `aria-describedby` | N/A | `**{"aria-describedby": "..."}` |
| `role` | N/A | `**{"role": "..."}` |
| `for` (label) | `htmlFor` (dbc.Label) | `htmlFor` |
| `class` | `className` | `className` |

### Ejemplos Reales

#### Botón Accesible

```python
# ❌ INCORRECTO
dbc.Button(
    html.I(className="fas fa-trash"),
    aria-label="Eliminar"  # ← No funciona
)

# ✅ CORRECTO: Opción 1 (title)
dbc.Button(
    html.I(className="fas fa-trash"),
    title="Eliminar"  # ← Genera tooltip + accesible
)

# ✅ CORRECTO: Opción 2 (html.Button)
html.Button(
    html.I(className="fas fa-trash"),
    **{"aria-label": "Eliminar"},
    className="btn btn-danger"
)
```

#### Label para Input

```python
# ❌ INCORRECTO
html.Label("Email:", for="email-input")  # ← 'for' es palabra reservada Python

# ✅ CORRECTO
dbc.Label("Email:", htmlFor="email-input")
```

---

## DASH006: Callbacks sin Auth Check

### Descripción del Problema

Callbacks con `prevent_initial_call=False` + API calls SIN verificar autenticación causan timeouts de 180s en sesiones no autenticadas.

### Por Qué Ocurre

```python
# ❌ INCORRECTO: Callback vulnerable
@app.callback(
    Output('data-container', 'children'),
    Input('data-store', 'data'),
    prevent_initial_call=False  # ← Se ejecuta en page load
)
def update_data(data):
    # ❌ Sin verificar autenticación
    response = requests.get(f"{API_URL}/data")  # ← API call
    # Usuario no autenticado → 401 → 3 retries × 45s = 135s → Nginx timeout 180s
    return response.json()
```

**Síntomas**:
- Usuario no autenticado visita página
- Callback se dispara automáticamente
- API call → 401 Unauthorized
- Request coordinator hace 3 retries × 45s cada uno
- Total: 135s de espera → Nginx timeout 180s → Error 504

### Detección Automática

```bash
python frontend/utils/dash_component_validator.py --check DASH006
```

**Output**:
```
[DASH006] callbacks/dashboard.py:123 - Callback con prevent_initial_call=False sin auth check
  Callback: update_data_container
  Inputs: ['data-store.data']
  Fix: Agregar State('auth-state', 'data') y verificar autenticación
```

### Solución (Patrón Issue #403)

```python
# ✅ CORRECTO: Verificación proactiva
from utils.auth_helpers import is_user_authenticated
from dash.exceptions import PreventUpdate

@app.callback(
    Output('data-container', 'children'),
    Input('data-store', 'data'),
    State('auth-state', 'data'),  # ← Agregar State
    prevent_initial_call=False
)
def update_data(data, auth_state):  # ← Agregar parámetro
    # ✅ Verificar ANTES de API calls
    if not is_user_authenticated(auth_state):
        logger.debug("[update_data] User not authenticated - skipping API calls")
        raise PreventUpdate  # ← NO hacer llamadas API

    # ✅ AHORA SÍ: Hacer llamadas API con seguridad
    response = requests.get(f"{API_URL}/data")

    if response.status_code == 401:
        # Token expiró, Auth Guard se encargará
        raise PreventUpdate

    return response.json()
```

### Helper de Verificación

```python
# utils/auth_helpers.py
def is_user_authenticated(auth_state):
    """
    Verifica si el usuario está autenticado.

    Args:
        auth_state (dict): Estado de autenticación desde dcc.Store

    Returns:
        bool: True si autenticado, False en caso contrario
    """
    if not auth_state:
        return False

    if not isinstance(auth_state, dict):
        return False

    return auth_state.get("authenticated", False)
```

### Casos Especiales

#### Callback con dcc.Interval

```python
# ❌ VULNERABLE: Auto-refresh sin auth check
@app.callback(
    Output('metrics-store', 'data'),
    Input('refresh-interval', 'n_intervals')
)
def auto_refresh_metrics(n_intervals):
    # ❌ Se ejecuta cada 30s, incluso si usuario no autenticado
    response = requests.get(f"{API_URL}/metrics")  # ← 401 loop
    return response.json()

# ✅ CORRECTO: Con auth check
@app.callback(
    Output('metrics-store', 'data'),
    Input('refresh-interval', 'n_intervals'),
    State('auth-state', 'data')
)
def auto_refresh_metrics(n_intervals, auth_state):
    # ✅ Verificar primero
    if not is_user_authenticated(auth_state):
        logger.debug("[auto_refresh_metrics] User not authenticated - skip")
        raise PreventUpdate

    response = requests.get(f"{API_URL}/metrics")
    return response.json()
```

---

## Troubleshooting Común

### Error: "Objects are not valid as a React child"

**Síntoma**:
```
Error: Objects are not valid as a React child (found: object with keys {props, type, namespace}).
```

**Causas**:
1. Lista sin wrapper (DASH001)
2. Retornar objeto Python en lugar de componente Dash
3. Retornar dict sin serializar

**Solución**:
```python
# ❌ INCORRECTO: Retornar dict
@callback(Output('result', 'children'), ...)
def my_callback(...):
    return {"data": "value"}  # ← Error

# ✅ CORRECTO: Retornar componente o string
@callback(Output('result', 'children'), ...)
def my_callback(...):
    return html.Div(str({"data": "value"}))  # Convertir a string
    # O mejor:
    return html.Pre(json.dumps({"data": "value"}, indent=2))
```

### Error: "A nonexistent object was used in an Output"

**Síntoma**:
```
Warning: A nonexistent object was used in an Output of a Dash callback.
The id of this object is "my-component" and the property is "children".
```

**Causas**:
1. Skeleton pattern no implementado correctamente
2. Componente falta en layout de página
3. Callback se registra antes de renderizar componente

**Solución**: Ver `DASH_DETAILED_PATTERNS.md#skeleton-pattern`

### Error: "Circular dependency detected"

**Síntoma**:
```
dash.exceptions.CircularDependency:
A circular dependency was detected between callbacks...
```

**Causas**:
- Callback A escribe a Output X
- Callback B escribe a Output Y
- Callback A lee de Input Y
- Callback B lee de Input X
- → Ciclo infinito

**Solución**:
```python
# ❌ INCORRECTO: Dependencia circular
@callback(Output('store-a', 'data'), Input('store-b', 'data'))
def update_a(b):
    return {"a": b["value"]}

@callback(Output('store-b', 'data'), Input('store-a', 'data'))
def update_b(a):
    return {"b": a["value"]}  # ← Ciclo!

# ✅ CORRECTO: Romper el ciclo
@callback(
    [Output('store-a', 'data'), Output('store-b', 'data')],
    Input('trigger-btn', 'n_clicks')
)
def update_both(n):
    # Un solo callback actualiza ambos stores
    return {"a": "value"}, {"b": "value"}
```

### Error: "401 loops después de OAuth"

**Síntoma**:
- Usuario hace login con OAuth
- Múltiples requests 401 en browser console
- Redirect loop a /login

**Causas**:
1. Auth Guard no configurado correctamente
2. Orden de registro de callbacks incorrecto
3. Token no se guardó correctamente

**Solución**: Ver orden obligatorio en `DASH_DETAILED_PATTERNS.md#auth-patterns-frontend`

```python
# ✅ ORDEN CORRECTO en app.py
register_auth_context_callbacks(app)  # 1️⃣ PRIMERO
register_auth_guard_callbacks(app)     # 2️⃣ SEGUNDO
register_auth_callbacks(app)           # 3️⃣ TERCERO
```

### Performance: Callbacks Lentos

**Síntoma**:
- Callbacks tardan >5 segundos
- UI se congela durante ejecución
- Worker timeout errors en logs

**Causas**:
1. Queries DB lentas sin índices
2. API calls sin cache
3. Procesamiento síncrono de grandes datasets
4. Múltiples API calls en secuencia

**Solución**:
1. ✅ Optimizar queries (índices DB)
2. ✅ Implementar cache persistente (dcc.Store + TTL)
3. ✅ Usar Background Callbacks (Dash Enterprise)
4. ✅ Paralelizar API calls cuando sea posible

```python
# ❌ LENTO: API calls en secuencia
@callback(...)
def my_callback(...):
    data1 = requests.get(url1).json()  # 2s
    data2 = requests.get(url2).json()  # 2s
    data3 = requests.get(url3).json()  # 2s
    # Total: 6s

# ✅ RÁPIDO: API calls en paralelo
import concurrent.futures

@callback(...)
def my_callback(...):
    urls = [url1, url2, url3]

    with concurrent.futures.ThreadPoolExecutor() as executor:
        responses = list(executor.map(lambda url: requests.get(url).json(), urls))

    data1, data2, data3 = responses
    # Total: ~2s (limitado por el más lento)
```

---

## Validación Automática

### Ejecutar Todos los Checks

```bash
python frontend/utils/dash_component_validator.py --all
```

**Output**:
```
[DASH001] components/upload.py:45 - Lista sin wrapper
[DASH003] callbacks/dashboard.py:78 - callback_context deprecado
[DASH006] callbacks/generic.py:123 - Callback sin auth check
---
Total: 3 antipatrones detectados
```

### Ejecutar Check Específico

```bash
python frontend/utils/dash_component_validator.py --check DASH001
python frontend/utils/dash_component_validator.py --check DASH006
```

### Integración con Pre-commit

```bash
# .git/hooks/pre-commit (automático con install-git-hooks.ps1)
python frontend/utils/dash_component_validator.py --all --strict

# --strict: Bloquea commit si detecta antipatrones críticos (DASH002, DASH006)
```

---

## Referencias

- **frontend/CLAUDE.md**: Quick reference
- **frontend/docs/DASH_DETAILED_PATTERNS.md**: Patrones avanzados
- **frontend/docs/CALLBACK_PATTERNS.md**: Debugging de callbacks
- **Dash Best Practices**: https://dash.plotly.com/performance

---

> **DASH_ANTIPATTERNS.md** - Catálogo de antipatrones Dash para xFarma
> Last updated: 2025-11-13 (Created during frontend/CLAUDE.md optimization)
