vault backup: 2026-05-05 11:17:44
This commit is contained in:
parent
201137b96f
commit
26a5c0ca3c
10 changed files with 439 additions and 5 deletions
|
|
@ -59,3 +59,9 @@ tags: [daily]
|
|||
- 11:12 (<1min) | `3m-portal`
|
||||
- **Asked:** Set up a global communication rule to respond in Russian to users and English for everything else, and document git worktrees usage in Obsidian.
|
||||
- **Done:** Established language preference rule and created documentation for git worktrees workflow with Claude integration for multi-branch repositories.
|
||||
- 11:14 | `3m-portal`
|
||||
- **Asked:** Set global communication rules with Russian for user and English for everything else | Added communication rules to system prompt, documented git worktrees usage in Obsidian | system_prompt.md, Obsidian_Git_Workflow.md
|
||||
- **Done:** | Global communication rules + git worktrees documentation | Updated system prompt for language rules, created Obsidian guide for git worktrees and multi-branch workflows | system_prompt.md, Obsidian_Git_Workflow.md
|
||||
- 11:16 | `obsidian-vault`
|
||||
- **Asked:** Create a global communication rule to respond in Russian to users but use English for everything else, and stop occasionally responding in Ukrainian.
|
||||
- **Done:** Documented git worktrees workflow for running multiple Claude sessions in parallel and configured Claude to suggest this approach when a repository has multiple branches.
|
||||
|
|
|
|||
|
|
@ -20,20 +20,20 @@ This 3-hop pattern works for hundreds of articles without vector search.
|
|||
|-------|-------------|----------|
|
||||
| [[wiki/obsidian-rag/_index\|obsidian-rag/]] | Karpathy's LLM wiki method — Obsidian RAG, setup, vs true RAG | 3 |
|
||||
| [[wiki/projects-overview/_index\|projects-overview/]] | All 42 Oliver Agency projects — grouped by server (optical-web-1, optical-dev, baic, box-cli) | 1 |
|
||||
| [[wiki/tech-patterns/_index\|tech-patterns/]] | Recurring tech stacks: FastAPI, React/Vite, Next.js, Azure AD, AI, Box, One2Edit, Redis/Celery, cost-tracker | 21 |
|
||||
| [[wiki/tech-patterns/_index\|tech-patterns/]] | Recurring tech stacks: FastAPI, React/Vite, Next.js, Azure AD, AI, Box, One2Edit, Redis/Celery, cost-tracker | 22 |
|
||||
| [[wiki/architecture/_index\|architecture/]] | Cross-cutting architectural patterns: Docker Compose, multi-agent AI, GCP timeout, RAG, hotfolder, optical-dev deploy, cost-tracker, new-project checklist, troubleshooting playbooks, ADR log, Cloud Run Jobs | 11 |
|
||||
| [[wiki/client-knowledge/_index\|client-knowledge/]] | Per-client notes for Ford, H&M, L'Oréal, Barclays, Ferrero, 3M | 6 |
|
||||
| [[wiki/concepts/_index\|concepts/]] | Atomic knowledge extracted from Claude Code sessions | 93 |
|
||||
| [[wiki/concepts/_index\|concepts/]] | Atomic knowledge extracted from Claude Code sessions | 98 |
|
||||
| [[wiki/connections/_index\|connections/]] | Cross-cutting insights linking 2+ concepts: FastAPI+Azure AD+Docker trinity, AI→cost-tracker, Apache+Vite basePath, GCP→REST polling, Box+hotfolder, Docker DNS+AdGuard, Celery prefork×faster_whisper memory stacking | 10 |
|
||||
| [[wiki/qa/_index\|qa/]] | Filed answers to queries (saved with `--file-back`) | 0 |
|
||||
| [[wiki/homelab/_index\|homelab/]] | Self-hosted infra: Proxmox install, IOMMU/PCI passthrough, hypervisor setup, budget builds, HP Elitedesk G3, Homarr API + Apps + Boards + Certificates + Integrations + Settings + Tasks + AdGuard + Clock + Docker Stats + Docker Integration + Download Client + Firewall + Proxmox Integration + Radarr + Readarr + Sonarr + Bookmarks + Calendar + Icons + App Widget + Weather + GitHub + Nextcloud + qBittorrent + RSS Feed + Speedtest Tracker + System Health Monitoring + System Resources + Services Map + Media Stack | 42 |
|
||||
| [[wiki/homelab/_index\|homelab/]] | Self-hosted infra: Proxmox install, IOMMU/PCI passthrough, hypervisor setup, budget builds, HP Elitedesk G3, Homarr API + Apps + Boards + Certificates + Integrations + Settings + Tasks + AdGuard + Clock + Docker Stats + Docker Integration + Download Client + Firewall + Proxmox Integration + Radarr + Readarr + Sonarr + Bookmarks + Calendar + Icons + App Widget + Weather + GitHub + Nextcloud + qBittorrent + RSS Feed + Speedtest Tracker + System Health Monitoring + System Resources + Services Map + Media Stack | 43 |
|
||||
| [[wiki/web-agency/_index\|web-agency/]] | AI-assisted website building & selling: Claude Code, Nanobanana 2, Kling, LaunchPath MCP | 9 |
|
||||
| [[wiki/dotfiles/_index\|dotfiles/]] | Linux terminal ricing: Kitty, Fish, WezTerm CLI, modern Rust CLI tools, LazyVim, unified themes, Tabby | 21 |
|
||||
| [[wiki/agent-sdk/_index\|agent-sdk/]] | Claude Agent SDK (formerly Claude Code SDK) — build autonomous AI agents in Python and TypeScript | 30 |
|
||||
| [[wiki/llm-models/_index\|llm-models/]] | LLM model catalogs — OpenAI and Claude/Anthropic models, IDs, context, pricing | 2 |
|
||||
| [[wiki/claude-code/_index\|claude-code/]] | Claude Code product docs — install, capabilities, surfaces, MCP, hooks, scheduling, multi-agent, plugins, skills, channels, error recovery, LM Studio local | 30 |
|
||||
| [[wiki/reports/_index\|reports/]] | Weekly and monthly summaries — generate: `uv run python scripts/report-generator.py --weekly` | 1 |
|
||||
| [[wiki/infrastructure/_index\|infrastructure/]] | Server inventory: all 10 SSH hosts — optical, optical-dev, optical-prod, baic, librechat, modocmms, box-cli, aimpress, pve | 10 |
|
||||
| [[wiki/infrastructure/_index\|infrastructure/]] | Server inventory: all 10 SSH hosts — optical, optical-dev, optical-prod, baic, librechat, modocmms, box-cli, aimpress, pve | 11 |
|
||||
|
||||
| [[wiki/testing/_index\|testing/]] | Web app testing: functional, performance, security, UI types; TDD/BDD/Agile methodologies; Selenium/Cypress/Playwright/JMeter/OWASP ZAP tools | 1 |
|
||||
|
||||
|
|
|
|||
|
|
@ -110,5 +110,10 @@
|
|||
| [[wiki/concepts/mongodb-schema-validator-migration-verification]] | MongoDB `collMod` migration marked "applied" without actually running — verify with `getCollectionInfos`; fix with direct `collMod` + `validationLevel: moderate` | daily/2026-04-30.md | 2026-04-30 |
|
||||
| [[wiki/concepts/git-worktrees-parallel-claude]] | Git worktrees for parallel Claude Code sessions — isolate branches, exact commands, when to suggest, Claude Code `isolation: "worktree"` | manual | 2026-05-05 |
|
||||
|
||||
| [[wiki/concepts/docker-compose-restart-no-code-reload]] | `docker compose restart` does not rebuild the image — must run `build && up -d` after any .py change; service name typos fail silently | daily/2026-05-01.md | 2026-05-01 |
|
||||
| [[wiki/concepts/mongodb-unwind-preservenullandemptyarrays]] | Typo `preserveNullAndEmpty` (wrong) vs `preserveNullAndEmptyArrays` (correct); MongoDB 7.0 rejects unknown `$unwind` options with error 28811; older versions silently ignored them | daily/2026-05-01.md | 2026-05-01 |
|
||||
| [[wiki/concepts/fastapi-response-model-silent-field-strip]] | FastAPI `response_model` silently strips fields not declared in the Pydantic schema — data exists in DB but never reaches the JSON response | daily/2026-05-01.md | 2026-05-01 |
|
||||
| [[wiki/concepts/react-useref-event-handler-state]] | React `useState` setters are async — `pointerMove` sees stale state set in `pointerDown`; fix with `useRef` for drag flags; also: `act()` in RTL tests, `useMemo` declaration order | daily/2026-05-01.md | 2026-05-01 |
|
||||
|
||||
<!-- Articles added automatically by compile.py -->
|
||||
<!-- Format: | [[concepts/slug]] | One-line summary | daily/YYYY-MM-DD.md | date | -->
|
||||
|
|
|
|||
89
wiki/concepts/docker-compose-restart-no-code-reload.md
Normal file
89
wiki/concepts/docker-compose-restart-no-code-reload.md
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
---
|
||||
title: "docker compose restart Does Not Reload Code in Built Images"
|
||||
aliases: [docker-restart-stale-code, docker-compose-build-required, docker-restart-vs-build]
|
||||
tags: [docker, docker-compose, python, deployment, gotcha]
|
||||
sources:
|
||||
- "daily/2026-05-01.md"
|
||||
created: 2026-05-01
|
||||
updated: 2026-05-01
|
||||
---
|
||||
|
||||
# `docker compose restart` Does Not Reload Code in Built Images
|
||||
|
||||
When a Docker Compose service uses `build:` context (image is baked from source at build time), `docker compose restart <service>` restarts the existing container from the cached image — it does **not** rebuild. Any Python code changes since the last `docker compose build` are completely invisible to the running container.
|
||||
|
||||
## Key Points
|
||||
|
||||
- `docker compose restart` only recycles the container process; the image layer is unchanged
|
||||
- After any `.py` file change, the full sequence is: `docker compose build <service> && docker compose up -d <service>`
|
||||
- Common symptom: bug fix applied and restart done, but the bug persists — the stale image is still running
|
||||
- Service name precision matters: `docker compose restart backend` fails silently if the actual service is named `api`
|
||||
- `docker compose logs <service>` will still show the old code running with no error
|
||||
- Hot-reload (Uvicorn `--reload`) only works if the source directory is **volume-mounted** into the container, not just baked in
|
||||
|
||||
## Details
|
||||
|
||||
### The Wrong Pattern
|
||||
|
||||
```bash
|
||||
# ❌ Code change deployed, restart done — but stale image still runs
|
||||
vim app/routes/users.py
|
||||
docker compose restart api
|
||||
# Bug still present. No error. Container appears healthy.
|
||||
```
|
||||
|
||||
### The Correct Pattern
|
||||
|
||||
```bash
|
||||
# ✅ Rebuild the image, then recreate the container
|
||||
docker compose build api && docker compose up -d api
|
||||
```
|
||||
|
||||
### Diagnosing Stale Image
|
||||
|
||||
To confirm whether the running container has the latest code:
|
||||
|
||||
```bash
|
||||
# Check when the image was last built
|
||||
docker images | grep <project_name>
|
||||
|
||||
# Exec into the container and inspect the file directly
|
||||
docker compose exec api cat /app/routes/users.py
|
||||
```
|
||||
|
||||
### Volume-Mount Alternative (Dev Only)
|
||||
|
||||
For development, mount the source directory so Uvicorn's `--reload` works without rebuilding:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
api:
|
||||
build: .
|
||||
command: uvicorn app.main:app --reload --host 0.0.0.0
|
||||
volumes:
|
||||
- ./app:/app/app # live mount; changes are visible immediately
|
||||
```
|
||||
|
||||
> [!warning] Not for Production
|
||||
> Volume mounts bypass the baked image in production — secrets and build artifacts may differ between environments.
|
||||
|
||||
### Service Name Gotcha
|
||||
|
||||
```bash
|
||||
# ❌ Silently does nothing if the service is named "api" not "backend"
|
||||
docker compose restart backend
|
||||
|
||||
# ✅ Verify service names first
|
||||
docker compose ps
|
||||
docker compose restart api
|
||||
```
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- [[wiki/tech-patterns/fastapi-python-docker]] — FastAPI + Docker Compose deployment pattern this applies to
|
||||
- [[wiki/architecture/optical-dev-server-deploy]] — optical-dev deploy workflow where build-before-up is the standard
|
||||
- [[wiki/concepts/python-service-deployment-dotenv]] — full Python service deploy checklist (includes rebuild step)
|
||||
|
||||
## Sources
|
||||
|
||||
- [[daily/2026-05-01.md]] — Sessions 12:09 and 19:07: code fix applied, `docker compose restart` run, bug persisted; root cause traced to stale baked image
|
||||
90
wiki/concepts/fastapi-response-model-silent-field-strip.md
Normal file
90
wiki/concepts/fastapi-response-model-silent-field-strip.md
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
---
|
||||
title: "FastAPI Response Model Silently Strips Fields Not in Schema"
|
||||
aliases: [fastapi-field-strip, pydantic-response-model-missing-field, fastapi-silent-omit]
|
||||
tags: [fastapi, pydantic, api, gotcha, debugging]
|
||||
sources:
|
||||
- "daily/2026-05-01.md"
|
||||
created: 2026-05-01
|
||||
updated: 2026-05-01
|
||||
---
|
||||
|
||||
# FastAPI Response Model Silently Strips Fields Not in Schema
|
||||
|
||||
When a FastAPI route has a `response_model`, Pydantic serializes the return value using **only** the fields declared in that model. Any extra fields — even if they exist in the MongoDB document and are present in the Python object — are silently dropped from the JSON response. No warning is raised, no error is logged.
|
||||
|
||||
## Key Points
|
||||
|
||||
- FastAPI's `response_model` is a whitelist: fields not declared are silently excluded from the response
|
||||
- The data exists in the database and in the Python layer — it never reaches the HTTP response
|
||||
- Frontend receives `undefined` for the missing field and typically falls back to a default/null state, masking the bug
|
||||
- **Diagnostic first step**: when API data appears "missing", check the Pydantic response model schema before investigating the database or service layer
|
||||
- Fix: add the missing field to the response model, or use `response_model=None` to disable filtering (not recommended in production)
|
||||
- `response_model_exclude_none=True` does not cause this — that only removes `None` values that ARE in the schema
|
||||
|
||||
## Details
|
||||
|
||||
### Minimal Reproduction
|
||||
|
||||
```python
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
class TaskResponse(BaseModel):
|
||||
id: str
|
||||
title: str
|
||||
# ← 'failure' and 'error' fields are NOT declared here
|
||||
|
||||
@app.get("/tasks/{task_id}", response_model=TaskResponse)
|
||||
async def get_task(task_id: str):
|
||||
# DB returns: {"id": "abc", "title": "Export", "failure": "timeout", "error": "Connection refused"}
|
||||
doc = await db.tasks.find_one({"_id": task_id})
|
||||
return doc
|
||||
# Response JSON: {"id": "abc", "title": "Export"}
|
||||
# 'failure' and 'error' are silently stripped
|
||||
```
|
||||
|
||||
### The Fix
|
||||
|
||||
```python
|
||||
class TaskResponse(BaseModel):
|
||||
id: str
|
||||
title: str
|
||||
failure: str | None = None # ← add missing fields
|
||||
error: str | None = None
|
||||
```
|
||||
|
||||
### Debugging Workflow
|
||||
|
||||
```
|
||||
Bug: frontend shows no error message for failed tasks
|
||||
↓
|
||||
1. Check Pydantic response model — is 'failure' field declared? ← START HERE
|
||||
→ No → add it → bug fixed
|
||||
2. If field IS in model, check service layer returns it
|
||||
3. If service returns it, check MongoDB document structure
|
||||
4. If MongoDB has it, check field name spelling (camelCase vs snake_case)
|
||||
```
|
||||
|
||||
> [!warning] Silent Failure Pattern
|
||||
> This is one of the most deceptive FastAPI bugs: the data pipeline is fully correct, but the response contract silently discards fields. Unlike missing DB documents (which raise errors), this produces valid 200 responses with incomplete data.
|
||||
|
||||
### When to Use `response_model=None`
|
||||
|
||||
```python
|
||||
# Only for debugging or internal endpoints — disables all output validation
|
||||
@app.get("/debug/tasks/{task_id}", response_model=None)
|
||||
async def debug_task(task_id: str):
|
||||
return await db.tasks.find_one({"_id": task_id})
|
||||
```
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- [[wiki/concepts/pydantic-v2-alias-id-gotcha]] — another Pydantic field serialization gotcha where field names differ from JSON keys
|
||||
- [[wiki/concepts/pydantic-model-dict-interface]] — Pydantic model/dict boundary issues that cause silent data loss
|
||||
- [[wiki/tech-patterns/fastapi-python-docker]] — FastAPI deployment context where these issues surface
|
||||
|
||||
## Sources
|
||||
|
||||
- [[daily/2026-05-01.md]] — Session 17:13: frontend showed `undefined` for task failure reason; root cause traced to `failure` and `error` fields missing from the Pydantic `TaskResponse` model
|
||||
86
wiki/concepts/mongodb-unwind-preservenullandemptyarrays.md
Normal file
86
wiki/concepts/mongodb-unwind-preservenullandemptyarrays.md
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
---
|
||||
title: "MongoDB $unwind preserveNullAndEmptyArrays Typo Breaks on 7.0"
|
||||
aliases: [mongodb-unwind-typo, preserveNullAndEmpty-wrong, mongodb-28811-error]
|
||||
tags: [mongodb, aggregation, python, fastapi, gotcha, version-compatibility]
|
||||
sources:
|
||||
- "daily/2026-05-01.md"
|
||||
created: 2026-05-01
|
||||
updated: 2026-05-01
|
||||
---
|
||||
|
||||
# MongoDB `$unwind` `preserveNullAndEmptyArrays` Typo Breaks on 7.0
|
||||
|
||||
The `$unwind` aggregation stage option is `preserveNullAndEmptyArrays` (full word). The common typo `preserveNullAndEmpty` (truncated) was silently ignored by older MongoDB versions but is **rejected with error code 28811** in MongoDB 7.0, which treats unknown `$unwind` options as fatal errors.
|
||||
|
||||
## Key Points
|
||||
|
||||
- Correct spelling: `preserveNullAndEmptyArrays` — not `preserveNullAndEmpty`
|
||||
- MongoDB 7.0 raises `OperationFailure` code **28811** for unknown `$unwind` options; older versions silently ignored them
|
||||
- A pipeline that worked in development (older Mongo) breaks in production after a MongoDB upgrade
|
||||
- FastAPI wraps the root exception in `anyio.EndOfStream` — the real `OperationFailure` is in the second "During handling of the above exception" block in the traceback
|
||||
- Stack path to root cause: HTTP route → `service.aggregate(pipeline)` → `OperationFailure` code 28811
|
||||
|
||||
## Details
|
||||
|
||||
### The Typo
|
||||
|
||||
```python
|
||||
# ❌ WRONG — silently accepted by MongoDB < 7.0, fatal on 7.0+
|
||||
pipeline = [
|
||||
{
|
||||
"$unwind": {
|
||||
"path": "$items",
|
||||
"preserveNullAndEmpty": True # truncated — unknown option
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
```python
|
||||
# ✅ CORRECT
|
||||
pipeline = [
|
||||
{
|
||||
"$unwind": {
|
||||
"path": "$items",
|
||||
"preserveNullAndEmptyArrays": True
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Reading the FastAPI / anyio Traceback
|
||||
|
||||
When this error occurs through FastAPI, the traceback is misleading:
|
||||
|
||||
```
|
||||
anyio.EndOfStream: ...
|
||||
During handling of the above exception, another exception occurred:
|
||||
...
|
||||
pymongo.errors.OperationFailure: Unrecognized option to $unwind: preserveNullAndEmpty, full error: {'code': 28811, ...}
|
||||
```
|
||||
|
||||
> [!tip] Diagnosis Rule
|
||||
> When you see `anyio.EndOfStream` in a FastAPI route, always scroll to the **second** exception block — the first is just the ASGI transport wrapper. The real error code is in `OperationFailure`.
|
||||
|
||||
### Why It Went Unnoticed
|
||||
|
||||
| Environment | MongoDB Version | Behaviour |
|
||||
|---|---|---|
|
||||
| Local dev | 5.x / 6.x | Unknown options ignored — pipeline runs |
|
||||
| Production | 7.0 | Unknown options → fatal error code 28811 |
|
||||
|
||||
The error only surfaced after a server-side MongoDB upgrade, making it appear to be an unrelated regression.
|
||||
|
||||
### Defensive Practice
|
||||
|
||||
When writing aggregation pipelines with multiple options, cross-reference against the [official `$unwind` docs](https://www.mongodb.com/docs/manual/reference/operator/aggregation/unwind/) rather than relying on runtime silence. MongoDB 7.0's stricter validation is the correct behaviour.
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- [[wiki/concepts/fastapi-mongodb-role-migration]] — FastAPI + MongoDB aggregation pipeline patterns
|
||||
- [[wiki/concepts/mongodb-schema-validator-migration-verification]] — MongoDB 7.0 stricter validation affects schema validators too
|
||||
- [[wiki/tech-patterns/fastapi-python-docker]] — FastAPI + Docker environment where version mismatches surface
|
||||
|
||||
## Sources
|
||||
|
||||
- [[daily/2026-05-01.md]] — Sessions 18:57 and 19:07: `OperationFailure` code 28811 on production after MongoDB 7.0 upgrade; local dev on older Mongo never raised the error
|
||||
|
|
@ -5,7 +5,7 @@ tags: [browser, html, vtt, accessibility, react, javascript]
|
|||
sources:
|
||||
- "daily/2026-04-29.md"
|
||||
created: 2026-04-29
|
||||
updated: 2026-04-29
|
||||
updated: 2026-05-01
|
||||
---
|
||||
|
||||
# Native `<track>` Element Requires Blob URL, Not `data:` URI
|
||||
|
|
@ -117,6 +117,43 @@ And this is the second line.
|
|||
|
||||
The string must start with `WEBVTT` on the first line (no BOM, no extra whitespace before it).
|
||||
|
||||
## Gotchas
|
||||
|
||||
### `<track default>` + Custom JS Overlay = Double Captions
|
||||
|
||||
The `default` attribute on `<track>` tells the browser to **auto-activate** the text track, causing the browser's native caption renderer to display cues. If a custom JS overlay is also rendering captions from the same track, both renderers fire simultaneously and captions appear doubled.
|
||||
|
||||
```jsx
|
||||
// ❌ BROKEN — native renderer + JS overlay both show captions
|
||||
<track kind="subtitles" src={trackUrl} srcLang="en" default />
|
||||
|
||||
// ✅ CORRECT — remove 'default'; JS overlay is the sole renderer
|
||||
<track kind="subtitles" src={trackUrl} srcLang="en" />
|
||||
```
|
||||
|
||||
When using a custom caption overlay, always omit the `default` attribute and activate/read the track programmatically via the `TextTrack` API.
|
||||
|
||||
### VTT Cue Settings Are Not Auto-Applied to Custom Overlays
|
||||
|
||||
WebVTT cue settings (`line:`, `position:`, `size:`, `align:`) are instructions for the **browser's native renderer**. A custom JS overlay that reads `cue.text` does not automatically honour these settings — it receives the raw text string only.
|
||||
|
||||
```javascript
|
||||
// cue.text = "Hello world" — no position data here
|
||||
// cue.line, cue.position, cue.align ARE accessible, but the overlay
|
||||
// must explicitly read them and apply CSS positioning
|
||||
|
||||
track.addEventListener("cuechange", () => {
|
||||
const cues = track.activeCues;
|
||||
for (const cue of cues) {
|
||||
// Must read cue.line / cue.position and apply manually:
|
||||
overlayEl.style.top = cue.line !== "auto" ? `${cue.line}%` : "90%";
|
||||
overlayEl.textContent = cue.text;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Silently ignoring cue settings while not stripping them causes subtle positioning bugs — text appears at the default position regardless of VTT metadata.
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- [[wiki/tech-patterns/python-ai-agents]] — VTT is often generated by AI models (Gemini, Whisper) in accessibility pipelines
|
||||
|
|
|
|||
116
wiki/concepts/react-useref-event-handler-state.md
Normal file
116
wiki/concepts/react-useref-event-handler-state.md
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
---
|
||||
title: "React useRef for Event Handler State — Avoid Stale useState in Pointer Events"
|
||||
aliases: [react-useref-drag-state, pointer-event-stale-state, useref-event-handler]
|
||||
tags: [react, hooks, useref, usestate, events, drag, testing]
|
||||
sources:
|
||||
- "daily/2026-05-01.md"
|
||||
created: 2026-05-01
|
||||
updated: 2026-05-01
|
||||
---
|
||||
|
||||
# React `useRef` for Event Handler State — Avoid Stale `useState` in Pointer Events
|
||||
|
||||
React 18 `useState` setters are asynchronous — state updates are batched and do not take effect until the next render. Event handlers registered on the same element fire in the same tick, which means a `pointerMove` handler that runs immediately after `pointerDown` will read the **pre-update** (stale) state value set in `pointerDown`. The solution is to use `useRef` for flags that event handlers must read synchronously.
|
||||
|
||||
## Key Points
|
||||
|
||||
- `useState` setters are async: `setDraggingIndex(i)` in `pointerDown` is still `null` when `pointerMove` fires in the same event tick
|
||||
- `useRef` holds a mutable `.current` value that is visible synchronously to all code in the same render cycle
|
||||
- Use `useRef` for drag-in-progress flags, selected index tracking, and any state that event handlers must read without waiting for a re-render
|
||||
- React Testing Library `fireEvent` does **not** flush state synchronously — wrap state-dependent assertions in `act()`
|
||||
- `useMemo` hooks must be declared **after** all `useState` variables they reference (TypeScript TS2448: "used before declaration")
|
||||
|
||||
## Details
|
||||
|
||||
### The Stale State Bug
|
||||
|
||||
```tsx
|
||||
// ❌ BROKEN — pointerMove sees stale null because useState is async
|
||||
function DraggableList({ cues }) {
|
||||
const [draggingCueIndex, setDraggingCueIndex] = useState<number | null>(null);
|
||||
|
||||
const handlePointerDown = (index: number) => (e: React.PointerEvent) => {
|
||||
setDraggingCueIndex(index); // schedules update — NOT yet applied
|
||||
};
|
||||
|
||||
const handlePointerMove = (e: React.PointerEvent) => {
|
||||
if (draggingCueIndex === null) return; // ← always null! fires in same tick
|
||||
// drag logic never executes
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### The Fix: useRef for Synchronous Access
|
||||
|
||||
```tsx
|
||||
// ✅ CORRECT — ref is readable immediately in the same event tick
|
||||
function DraggableList({ cues }) {
|
||||
const draggingCueIndexRef = useRef<number | null>(null);
|
||||
const [draggingCueIndex, setDraggingCueIndex] = useState<number | null>(null);
|
||||
// ↑ useState still used for re-render trigger (visual feedback)
|
||||
|
||||
const handlePointerDown = (index: number) => (e: React.PointerEvent) => {
|
||||
draggingCueIndexRef.current = index; // synchronous, readable immediately
|
||||
setDraggingCueIndex(index); // triggers re-render for UI update
|
||||
e.currentTarget.setPointerCapture(e.pointerId);
|
||||
};
|
||||
|
||||
const handlePointerMove = (e: React.PointerEvent) => {
|
||||
if (draggingCueIndexRef.current === null) return; // ✓ correct value
|
||||
// drag logic executes correctly
|
||||
};
|
||||
|
||||
const handlePointerUp = () => {
|
||||
draggingCueIndexRef.current = null;
|
||||
setDraggingCueIndex(null);
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Testing: act() Requirement
|
||||
|
||||
```tsx
|
||||
// ❌ BROKEN — assertion runs before state flush
|
||||
fireEvent.pointerDown(element, { pointerId: 1 });
|
||||
expect(screen.getByTestId("dragging-indicator")).toBeInTheDocument();
|
||||
// Fails: state not yet applied
|
||||
|
||||
// ✅ CORRECT — wrap in act() to flush state updates
|
||||
import { act } from "@testing-library/react";
|
||||
|
||||
act(() => {
|
||||
fireEvent.pointerDown(element, { pointerId: 1 });
|
||||
});
|
||||
expect(screen.getByTestId("dragging-indicator")).toBeInTheDocument();
|
||||
```
|
||||
|
||||
### useMemo Declaration Order (TS2448)
|
||||
|
||||
```tsx
|
||||
// ❌ TypeScript error TS2448: Block-scoped variable 'items' used before its declaration
|
||||
const sortedItems = useMemo(() => [...items].sort(), [items]); // references items
|
||||
const [items, setItems] = useState<string[]>([]); // declared after
|
||||
|
||||
// ✅ Always declare useState before useMemo that depends on it
|
||||
const [items, setItems] = useState<string[]>([]);
|
||||
const sortedItems = useMemo(() => [...items].sort(), [items]);
|
||||
```
|
||||
|
||||
### Decision Guide: useState vs useRef
|
||||
|
||||
| Use case | Correct hook |
|
||||
|---|---|
|
||||
| Value that triggers a re-render (visual state) | `useState` |
|
||||
| Value read synchronously in event handlers | `useRef` |
|
||||
| Value that needs both (drag index) | Both — `useRef` for sync read, `useState` for render |
|
||||
| DOM node reference | `useRef` |
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- [[wiki/concepts/zustand-async-hydration]] — another async state access pattern where data is not available synchronously on first render
|
||||
- [[wiki/tech-patterns/react-vite-typescript]] — React + Vite + TypeScript stack where this pattern applies
|
||||
- [[wiki/concepts/websocket-react-token-guard]] — React `useEffect` + async state dependency management
|
||||
|
||||
## Sources
|
||||
|
||||
- [[daily/2026-05-01.md]] — Sessions 15:31 and 19:07: drag-to-reorder feature broke because `pointerMove` saw stale `draggingCueIndex`; fixed by introducing `draggingCueIndexRef`; also discovered `useMemo` ordering TS2448 and `act()` requirement in tests
|
||||
|
|
@ -1,6 +1,10 @@
|
|||
|
||||
# Build Log
|
||||
|
||||
## [2026-05-05T12:00:00+01:00] compile | 2026-05-01.md
|
||||
- Articles created: [[wiki/concepts/docker-compose-restart-no-code-reload]], [[wiki/concepts/mongodb-unwind-preservenullandemptyarrays]], [[wiki/concepts/fastapi-response-model-silent-field-strip]], [[wiki/concepts/react-useref-event-handler-state]]
|
||||
- Articles updated: [[wiki/concepts/native-track-blob-url]]
|
||||
|
||||
## [2026-04-30T23:59:00+01:00] compile | 2026-04-30.md (pass 3)
|
||||
- Source: daily/2026-04-30.md
|
||||
- Articles created: [[wiki/concepts/react-query-enabled-falsy-value]], [[wiki/concepts/mongodb-cross-collection-id-confusion]], [[wiki/concepts/browser-sequential-download-blocking]], [[wiki/concepts/mongodb-schema-validator-migration-verification]]
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ Recurring technology stacks used across Oliver Agency projects. Each article cov
|
|||
| [[wiki/tech-patterns/websocket-keepalive-terminal-close\|WebSocket Keepalive + Terminal Close Codes]] | Bidirectional 20s keepalive + terminal close codes (4001/4003/4004/4403) to prevent reconnect storms through Apache mod_proxy_wstunnel | video-accessibility, mod-comms |
|
||||
| [[wiki/tech-patterns/pydantic-empty-string-coercion\|Pydantic Empty String → None Coercion]] | field_validator with mode='before' to treat "" as absent optional field — prevents 400 on CC-only or AD-only payloads | video-accessibility |
|
||||
| [[wiki/tech-patterns/vtt-descriptive-transcript-regeneration\|VTT Edit → Descriptive Transcript Regeneration]] | Pattern for keeping descriptive_transcript.txt in sync when captions or AD VTTs are edited via PATCH /vtt | video-accessibility |
|
||||
| [[wiki/tech-patterns/git-worktrees-parallel-claude-sessions\|Using Git Worktrees for Parallel Claude Development Sessions]] | Use git worktrees when you need to run multiple independent Claude sessions simultaneously on differ | — |
|
||||
|
||||
## Quick Decision Guide
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue