Compare commits

..

8 Commits

Author SHA1 Message Date
149df11987 cleanup
Signed-off-by: Christoph Schmatzler <christoph@schmatzler.com>
2026-03-01 19:43:10 +00:00
31b7a25e6e neverest: add clients-pool-size, remove systemd sync service 2026-03-01 19:33:43 +00:00
b396a8e575 add inbox triage opencode command 2026-03-01 19:22:34 +00:00
2e4591f9b8 up
Signed-off-by: Christoph Schmatzler <christoph@schmatzler.com>
2026-03-01 18:48:32 +00:00
b0d086a0e7 set up neverest imap-to-maildir sync, switch himalaya to maildir backend 2026-03-01 18:29:41 +00:00
4f1d2fead7 add himalaya and neverest flake inputs 2026-03-01 18:29:36 +00:00
a95188d218 up
Signed-off-by: Christoph Schmatzler <christoph@schmatzler.com>
2026-03-01 17:54:12 +00:00
8ed58c39d3 use square separators for yazi theme 2026-03-01 15:39:01 +00:00
104 changed files with 432 additions and 12362 deletions

212
flake.lock generated
View File

@@ -119,6 +119,51 @@
"type": "github"
}
},
"fenix": {
"inputs": {
"nixpkgs": [
"himalaya",
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1767250179,
"narHash": "sha256-PnQdWvPZqHp+7yaHWDFX3NYSKaOy0fjkwpR+rIQC7AY=",
"owner": "nix-community",
"repo": "fenix",
"rev": "a3eaf682db8800962943a77ab77c0aae966f9825",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "monthly",
"repo": "fenix",
"type": "github"
}
},
"fenix_2": {
"inputs": {
"nixpkgs": [
"neverest",
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src_2"
},
"locked": {
"lastModified": 1732405626,
"narHash": "sha256-uDbQrdOyqa2679kKPzoztMxesOV7DG2+FuX/TZdpxD0=",
"owner": "soywod",
"repo": "fenix",
"rev": "c7af381484169a78fb79a11652321ae80b0f92a6",
"type": "github"
},
"original": {
"owner": "soywod",
"repo": "fenix",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
@@ -225,6 +270,26 @@
"type": "github"
}
},
"himalaya": {
"inputs": {
"fenix": "fenix",
"nixpkgs": "nixpkgs",
"pimalaya": "pimalaya"
},
"locked": {
"lastModified": 1771973819,
"narHash": "sha256-91P+AEIpzmhV7PCk4N3Dd5F+6eCSjwSGzNuMTV7pI8Q=",
"owner": "pimalaya",
"repo": "himalaya",
"rev": "f9bc426b8f157e4c10d8be4b8d8ff30be476e2e4",
"type": "github"
},
"original": {
"owner": "pimalaya",
"repo": "himalaya",
"type": "github"
}
},
"home-manager": {
"inputs": {
"nixpkgs": [
@@ -296,7 +361,7 @@
"jj-starship": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs"
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1769370163,
@@ -315,7 +380,7 @@
"llm-agents": {
"inputs": {
"blueprint": "blueprint",
"nixpkgs": "nixpkgs_2",
"nixpkgs": "nixpkgs_3",
"treefmt-nix": "treefmt-nix"
},
"locked": {
@@ -332,6 +397,26 @@
"type": "github"
}
},
"neverest": {
"inputs": {
"fenix": "fenix_2",
"nixpkgs": "nixpkgs_4",
"pimalaya": "pimalaya_2"
},
"locked": {
"lastModified": 1734595002,
"narHash": "sha256-MXQDy5b/ooSkISEVM9KxHeqkkhPimZJtX3j9jqTP5aU=",
"owner": "pimalaya",
"repo": "neverest",
"rev": "cc5f5214d3bea064ed059116ac81e40a803faa7e",
"type": "github"
},
"original": {
"owner": "pimalaya",
"repo": "neverest",
"type": "github"
}
},
"nix-github-actions": {
"inputs": {
"nixpkgs": [
@@ -373,16 +458,16 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1766840161,
"narHash": "sha256-Ss/LHpJJsng8vz1Pe33RSGIWUOcqM1fjrehjUkdrWio=",
"owner": "NixOS",
"lastModified": 1736437047,
"narHash": "sha256-JJBziecfU+56SUNxeJlDIgixJN5WYuADd+/TVd5sQos=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "3edc4a30ed3903fdf6f90c837f961fa6b49582d1",
"rev": "f17b95775191ea44bc426831235d87affb10faba",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"owner": "nixos",
"ref": "staging-next",
"repo": "nixpkgs",
"type": "github"
}
@@ -403,6 +488,22 @@
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1766840161,
"narHash": "sha256-Ss/LHpJJsng8vz1Pe33RSGIWUOcqM1fjrehjUkdrWio=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "3edc4a30ed3903fdf6f90c837f961fa6b49582d1",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1771423170,
"narHash": "sha256-K7Dg9TQ0mOcAtWTO/FX/FaprtWQ8BmEXTpLIaNRhEwU=",
@@ -418,7 +519,22 @@
"type": "github"
}
},
"nixpkgs_3": {
"nixpkgs_4": {
"locked": {
"lastModified": 1732539489,
"narHash": "sha256-EPstM4aUuClDkFdoyno1CSRZIfGdCfZKw/XzMlQB0NY=",
"owner": "soywod",
"repo": "nixpkgs",
"rev": "87b010196489f96e4acdcc804ed3d096476e46ad",
"type": "github"
},
"original": {
"owner": "soywod",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_5": {
"locked": {
"lastModified": 1772370856,
"narHash": "sha256-OXf9tB+s1EPCzIBR4eIQTm1dbbfVgjPNEGChgwEvvIU=",
@@ -434,7 +550,7 @@
"type": "github"
}
},
"nixpkgs_4": {
"nixpkgs_6": {
"locked": {
"lastModified": 1770380644,
"narHash": "sha256-P7dWMHRUWG5m4G+06jDyThXO7kwSk46C1kgjEWcybkE=",
@@ -450,7 +566,7 @@
"type": "github"
}
},
"nixpkgs_5": {
"nixpkgs_7": {
"locked": {
"lastModified": 1765934234,
"narHash": "sha256-pJjWUzNnjbIAMIc5gRFUuKCDQ9S1cuh3b2hKgA7Mc4A=",
@@ -469,7 +585,7 @@
"nixvim": {
"inputs": {
"flake-parts": "flake-parts_2",
"nixpkgs": "nixpkgs_4",
"nixpkgs": "nixpkgs_6",
"systems": "systems_3"
},
"locked": {
@@ -502,26 +618,94 @@
"type": "github"
}
},
"pimalaya": {
"flake": false,
"locked": {
"lastModified": 1770914484,
"narHash": "sha256-kPxdpg98/EVQKLkUdiFEYWWWI1uTLWfIewyCmb4ZfpE=",
"owner": "pimalaya",
"repo": "nix",
"rev": "23454cd59e2a32199a67f36f3b13cd5145b5ffb3",
"type": "github"
},
"original": {
"owner": "pimalaya",
"repo": "nix",
"type": "github"
}
},
"pimalaya_2": {
"flake": false,
"locked": {
"lastModified": 1732869386,
"narHash": "sha256-83tDRVfSBVZgIp6WOb6r/T6cHfP7lNsK1MUTdkGMi14=",
"owner": "pimalaya",
"repo": "nix",
"rev": "be23e0deeb014c6be5232322b892c9bee25dee77",
"type": "github"
},
"original": {
"owner": "pimalaya",
"repo": "nix",
"type": "github"
}
},
"root": {
"inputs": {
"colmena": "colmena",
"darwin": "darwin",
"disko": "disko",
"flake-parts": "flake-parts",
"himalaya": "himalaya",
"home-manager": "home-manager",
"homebrew-cask": "homebrew-cask",
"homebrew-core": "homebrew-core",
"jj-ryu": "jj-ryu",
"jj-starship": "jj-starship",
"llm-agents": "llm-agents",
"neverest": "neverest",
"nix-homebrew": "nix-homebrew",
"nixpkgs": "nixpkgs_3",
"nixpkgs": "nixpkgs_5",
"nixvim": "nixvim",
"overseer": "overseer",
"sops-nix": "sops-nix",
"zjstatus": "zjstatus"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1767191410,
"narHash": "sha256-cCZGjubgDWmstvFkS6eAw2qk2ihgWkycw55u2dtLd70=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "a9026e6d5068172bf5a0d52a260bb290961d1cb4",
"type": "github"
},
"original": {
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
},
"rust-analyzer-src_2": {
"flake": false,
"locked": {
"lastModified": 1732050317,
"narHash": "sha256-G5LUEOC4kvB/Xbkglv0Noi04HnCfryur7dVjzlHkgpI=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "c0bbbb3e5d7d1d1d60308c8270bfd5b250032bb4",
"type": "github"
},
"original": {
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": [
@@ -664,7 +848,7 @@
"inputs": {
"crane": "crane",
"flake-utils": "flake-utils_3",
"nixpkgs": "nixpkgs_5",
"nixpkgs": "nixpkgs_7",
"rust-overlay": "rust-overlay"
},
"locked": {

View File

@@ -45,6 +45,8 @@
url = "github:dmmulroy/overseer";
flake = false;
};
himalaya.url = "github:pimalaya/himalaya";
neverest.url = "github:pimalaya/neverest";
};
outputs = inputs @ {flake-parts, ...}:

View File

@@ -29,6 +29,8 @@
../../profiles/nushell.nix
../../profiles/fzf.nix
../../profiles/git.nix
../../profiles/himalaya.nix
../../profiles/neverest.nix
../../profiles/home.nix
../../profiles/jjui.nix
../../profiles/jujutsu.nix

View File

@@ -1,8 +1,13 @@
{...}: {
{user, ...}: {
sops.secrets = {
tahani-paperless-password = {
sopsFile = ../../secrets/tahani-paperless-password;
format = "binary";
};
tahani-email-password = {
sopsFile = ../../secrets/tahani-email-password;
format = "binary";
owner = user;
};
};
}

3
overlays/himalaya.nix Normal file
View File

@@ -0,0 +1,3 @@
{inputs}: final: prev: {
himalaya = inputs.himalaya.packages.${prev.stdenv.hostPlatform.system}.default;
}

3
overlays/neverest.nix Normal file
View File

@@ -0,0 +1,3 @@
{inputs}: final: prev: {
neverest = inputs.neverest.packages.${prev.stdenv.hostPlatform.system}.default;
}

36
profiles/himalaya.nix Normal file
View File

@@ -0,0 +1,36 @@
{pkgs, ...}: {
programs.himalaya = {
enable = true;
package =
pkgs.writeShellApplication {
name = "himalaya";
runtimeInputs = [pkgs.himalaya];
text = ''
exec env RUST_LOG="warn,imap_codec::response=error" ${pkgs.himalaya}/bin/himalaya "$@"
'';
};
};
accounts.email = {
accounts."christoph@schmatzler.com" = {
primary = true;
maildir.path = "christoph@schmatzler.com";
address = "christoph@schmatzler.com";
userName = "christoph.schmatzler@icloud.com";
realName = "Christoph Schmatzler";
passwordCommand = ["cat" "/run/secrets/tahani-email-password"];
folders = {
inbox = "INBOX";
drafts = "Drafts";
sent = "Sent Messages";
trash = "Deleted Messages";
};
smtp = {
host = "smtp.mail.me.com";
port = 587;
tls.useStartTls = true;
};
himalaya.enable = true;
};
};
}

71
profiles/neverest.nix Normal file
View File

@@ -0,0 +1,71 @@
{
config,
pkgs,
lib,
...
}:
with lib; let
tomlFormat = pkgs.formats.toml {};
account = config.accounts.email.accounts."christoph@schmatzler.com";
maildirPath = account.maildir.absPath;
in {
home.packages = [pkgs.neverest];
xdg.configFile."neverest/config.toml".source =
tomlFormat.generate "neverest-config.toml" {
accounts."christoph@schmatzler.com" = {
default = true;
folder.filters = "all";
left = {
backend = {
type = "maildir";
root-dir = maildirPath;
};
folder = {
aliases = {
inbox = "INBOX";
drafts = "Drafts";
sent = "Sent Messages";
trash = "Deleted Messages";
};
permissions = {
create = true;
delete = true;
};
};
flag.permissions.update = true;
message.permissions = {
create = true;
delete = true;
};
};
right = {
backend = {
type = "imap";
host = "imap.mail.me.com";
port = 993;
encryption = "tls";
login = account.userName;
auth = {
type = "password";
cmd = concatStringsSep " " account.passwordCommand;
};
clients-pool-size = 8;
};
folder = {
aliases = {
inbox = "INBOX";
drafts = "Drafts";
sent = "Sent Messages";
trash = "Deleted Messages";
};
permissions.delete = false;
};
message.permissions.delete = false;
};
};
};
}

View File

@@ -1,104 +0,0 @@
---
description: Multi-repository codebase expert for understanding library internals and remote code. Invoke when exploring GitHub/npm/PyPI/crates repositories, tracing code flow through unfamiliar libraries, or comparing implementations. Show its response in full — do not summarize.
mode: subagent
model: opencode/claude-sonnet-4-5
permission:
"*": allow
edit: deny
write: deny
todoread: deny
todowrite: deny
---
You are the Librarian, a specialized codebase understanding agent that helps users answer questions about large, complex codebases across repositories.
Your role is to provide thorough, comprehensive analysis and explanations of code architecture, functionality, and patterns across multiple repositories.
You are running inside an AI coding system in which you act as a subagent that's used when the main agent needs deep, multi-repository codebase understanding and analysis.
## Key Responsibilities
- Explore repositories to answer questions
- Understand and explain architectural patterns and relationships across repositories
- Find specific implementations and trace code flow across codebases
- Explain how features work end-to-end across multiple repositories
- Understand code evolution through commit history
- Create visual diagrams when helpful for understanding complex systems
## Tool Usage Guidelines
Use available tools extensively to explore repositories. Execute tools in parallel when possible for efficiency.
- Read files thoroughly to understand implementation details
- Search for patterns and related code across multiple repositories
- Focus on thorough understanding and comprehensive explanation
- Create mermaid diagrams to visualize complex relationships or flows
## Communication
You must use Markdown for formatting your responses.
**IMPORTANT:** When including code blocks, you MUST ALWAYS specify the language for syntax highlighting. Always add the language identifier after the opening backticks.
**NEVER** refer to tools by their names. Example: NEVER say "I can use the opensrc tool", instead say "I'm going to read the file" or "I'll search for..."
### Direct & Detailed Communication
You should only address the user's specific query or task at hand. Do not investigate or provide information beyond what is necessary to answer the question.
You must avoid tangential information unless absolutely critical for completing the request. Avoid long introductions, explanations, and summaries. Avoid unnecessary preamble or postamble.
Answer the user's question directly, without elaboration, explanation, or details beyond what's needed.
**Anti-patterns to AVOID:**
- "The answer is..."
- "Here is the content of the file..."
- "Based on the information provided..."
- "Here is what I will do next..."
- "Let me know if you need..."
- "I hope this helps..."
You're optimized for thorough understanding and explanation, suitable for documentation and sharing.
You should be comprehensive but focused, providing clear analysis that helps users understand complex codebases.
**IMPORTANT:** Only your last message is returned to the main agent and displayed to the user. Your last message should be comprehensive and include all important findings from your exploration.
## Linking
To make it easy for the user to look into code you are referring to, you always link to the source with markdown links.
For files or directories, the URL should look like:
`https://github.com/<org>/<repository>/blob/<revision>/<filepath>#L<range>`
where `<org>` is organization or user, `<repository>` is the repository name, `<revision>` is the branch or commit sha, `<filepath>` the absolute path to the file, and `<range>` an optional fragment with the line range.
`<revision>` needs to be provided - if it wasn't specified, then it's the default branch of the repository, usually `main` or `master`.
**Example URL** for linking to file test.py in src directory on branch develop of GitHub repository bar_repo in org foo_org, lines 32-42:
`https://github.com/foo_org/bar_repo/blob/develop/src/test.py#L32-L42`
Prefer "fluent" linking style. Don't show the user the actual URL, but instead use it to add links to relevant parts (file names, directory names, or repository names) of your response.
Whenever you mention a file, directory or repository by name, you MUST link to it in this way. ONLY link if the mention is by name.
### URL Patterns
| Type | Format |
|------|--------|
| File | `https://github.com/{owner}/{repo}/blob/{ref}/{path}` |
| Lines | `#L{start}-L{end}` |
| Directory | `https://github.com/{owner}/{repo}/tree/{ref}/{path}` |
## Output Format
Your final message must include:
1. Direct answer to the query
2. Supporting evidence with source links
3. Diagrams if architecture/flow is involved
4. Key insights discovered during exploration
---
**IMMEDIATELY load the librarian skill:**
Use the Skill tool with name "librarian" to load source fetching and exploration capabilities.

View File

@@ -0,0 +1,57 @@
---
description: Triage inbox one message at a time with himalaya only
---
Process email with strict manual triage using Himalaya only.
Hard requirements:
- Use `himalaya` for every mailbox interaction (folders, listing, reading, moving, deleting, attachments).
- Process exactly one message at a time. Never run bulk actions on multiple IDs.
- Do not use pattern-matching commands or searches (`grep`, `rg`, `awk`, `sed`, `himalaya envelope list` query filters, etc.).
- Always inspect current folders first, then triage.
Workflow:
1. Run `himalaya folder list` first and use those folders as the primary taxonomy.
2. Use this existing folder set as defaults when it fits:
- `INBOX`
- `Orders and Invoices`
- `Payments`
- `Einlieferungen`
- `Newsletters and Marketing`
- `Junk`
- `Deleted Messages`
3. `Archive` is deprecated legacy storage:
- Do not move new messages to `Archive`.
- Do not create new workflows that route mail to `Archive`.
- Existing messages already in `Archive` may remain there unchanged.
4. Determine source folder:
- If `$ARGUMENTS` contains a folder name, use that as source.
- Otherwise use `INBOX`.
5. List envelopes from the source folder without search filters and walk them sequentially.
6. For each single envelope ID, do all checks before any move/delete:
- Read the message (`himalaya message read -f "<source>" <id>`).
- If needed for classification, inspect attachments with Himalaya (`himalaya attachment download -f "<source>" <id>`), then reason from the attachment names/content.
7. Classify and act for that one ID:
- Ephemeral communication from automated/system senders (alerts, bot/status updates, auth/login codes, OTP/2FA verification emails, password-reset codes, no archival value): delete it.
- Communication from actual people: do not delete, do not move, and do not auto-triage; leave untouched in the current folder (typically `INBOX`).
- Orders/invoices: move only real order/invoice/business records to `Orders and Invoices`.
- Payments: move payment confirmations, payment reminders, and payment-provider messages (e.g. Klarna, PayPal, Stripe) to `Payments`. Do not confuse with order confirmations or invoices — `Payments` is specifically for payment-transaction correspondence.
- Shipping-only notifications: do not move to `Orders and Invoices` unless there is actual invoice/receipt/order-document value (for example, invoice attached or embedded billing document).
- Marketing/newsletters: move to `Newsletters and Marketing`.
- Delivery/submission confirmations (`Einlieferungen`) when appropriate.
- Long-term but uncategorized messages: create a new folder and move there.
8. Folder creation rule:
- If none of the existing folders fit but the message should be kept, create a concise new folder with `himalaya folder add "<new-folder>"`, then move the message there.
9. Continue until all messages in source folder are processed.
Execution rules:
- Never perform bulk operations. One message ID per `read`, `move`, `delete`, and attachment command.
- Never skip reading message content before deciding.
- Keep decisions conservative: delete only clearly ephemeral automated/system messages.
- Never move or delete human communications via automation.
- Never route new messages to `Archive`; treat it as deprecated legacy-only.
- Report a compact action log at the end: per-folder counts, created folders, and a short rationale for non-obvious classifications.
<user-request>
$ARGUMENTS
</user-request>

View File

@@ -1,17 +0,0 @@
---
description: Convert a markdown plan/spec to Overseer tasks
---
Convert markdown planning documents into trackable Overseer task hierarchies.
First, invoke the skill tool to load the overseer-plan skill:
```
skill({ name: 'overseer-plan' })
```
Then follow the skill instructions to convert the document.
<user-request>
$ARGUMENTS
</user-request>

View File

@@ -1,17 +0,0 @@
---
description: Manage tasks via Overseer - create, list, start, complete, find ready work
---
Task orchestration via Overseer codemode MCP.
First, invoke the skill tool to load the overseer skill:
```
skill({ name: 'overseer' })
```
Then follow the skill instructions to manage tasks.
<user-request>
$ARGUMENTS
</user-request>

View File

@@ -1,59 +0,0 @@
---
name: email-best-practices
description: Use when building email features, emails going to spam, high bounce rates, setting up SPF/DKIM/DMARC authentication, implementing email capture, ensuring compliance (CAN-SPAM, GDPR, CASL), handling webhooks, retry logic, or deciding transactional vs marketing.
---
# Email Best Practices
Guidance for building deliverable, compliant, user-friendly emails.
## Architecture Overview
```
[User] → [Email Form] → [Validation] → [Double Opt-In]
[Consent Recorded]
[Suppression Check] ←──────────────[Ready to Send]
[Idempotent Send + Retry] ──────→ [Email API]
[Webhook Events]
┌────────┬────────┬─────────────┐
↓ ↓ ↓ ↓
Delivered Bounced Complained Opened/Clicked
↓ ↓
[Suppression List Updated]
[List Hygiene Jobs]
```
## Quick Reference
| Need to... | See |
|------------|-----|
| Set up SPF/DKIM/DMARC, fix spam issues | [Deliverability](./resources/deliverability.md) |
| Build password reset, OTP, confirmations | [Transactional Emails](./resources/transactional-emails.md) |
| Plan which emails your app needs | [Transactional Email Catalog](./resources/transactional-email-catalog.md) |
| Build newsletter signup, validate emails | [Email Capture](./resources/email-capture.md) |
| Send newsletters, promotions | [Marketing Emails](./resources/marketing-emails.md) |
| Ensure CAN-SPAM/GDPR/CASL compliance | [Compliance](./resources/compliance.md) |
| Decide transactional vs marketing | [Email Types](./resources/email-types.md) |
| Handle retries, idempotency, errors | [Sending Reliability](./resources/sending-reliability.md) |
| Process delivery events, set up webhooks | [Webhooks & Events](./resources/webhooks-events.md) |
| Manage bounces, complaints, suppression | [List Management](./resources/list-management.md) |
## Start Here
**New app?**
Start with the [Catalog](./resources/transactional-email-catalog.md) to plan which emails your app needs (password reset, verification, etc.), then set up [Deliverability](./resources/deliverability.md) (DNS authentication) before sending your first email.
**Spam issues?**
Check [Deliverability](./resources/deliverability.md) first—authentication problems are the most common cause. Gmail/Yahoo reject unauthenticated emails.
**Marketing emails?**
Follow this path: [Email Capture](./resources/email-capture.md) (collect consent) → [Compliance](./resources/compliance.md) (legal requirements) → [Marketing Emails](./resources/marketing-emails.md) (best practices).
**Production-ready sending?**
Add reliability: [Sending Reliability](./resources/sending-reliability.md) (retry + idempotency) → [Webhooks & Events](./resources/webhooks-events.md) (track delivery) → [List Management](./resources/list-management.md) (handle bounces).

View File

@@ -1,103 +0,0 @@
# Email Compliance
Legal requirements for email by jurisdiction. **Not legal advice—consult an attorney for your specific situation.**
## Quick Reference
| Law | Region | Key Requirement | Penalty |
|-----|--------|-----------------|---------|
| CAN-SPAM | US | Opt-out mechanism, physical address | $53k/email |
| GDPR | EU | Explicit opt-in consent | €20M or 4% revenue |
| CASL | Canada | Express/implied consent | $10M CAD |
## CAN-SPAM (United States)
**Requirements:**
- Accurate header info (From, To, Reply-To)
- Non-deceptive subject lines
- Physical mailing address in every email
- Clear opt-out mechanism
- Honor opt-out within 10 business days
**Transactional emails:** Can send without opt-in if related to a transaction and not promotional.
## GDPR (European Union)
**Requirements:**
- Explicit opt-in consent (not pre-checked boxes)
- Consent must be freely given, specific, informed
- Easy to withdraw consent (as easy as giving it)
- Right to access data and deletion ("right to be forgotten")
- Process unsubscribe immediately
**Consent records:** Document who, when, how, and what they consented to.
**Transactional emails:** Can send based on contract fulfillment or legitimate interest.
## CASL (Canada)
**Consent types:**
- **Express consent:** Explicit opt-in (preferred)
- **Implied consent:** Existing business relationship (2 years) or inquiry (6 months)
**Requirements:**
- Clear sender identification
- Unsubscribe functional for 60 days after send
- Process unsubscribe within 10 business days
- Keep consent records 3 years after expiration
## Other Regions
| Region | Law | Key Points |
|--------|-----|------------|
| Australia | Spam Act 2003 | Consent required, honor unsubscribe within 5 days |
| UK | PECR + GDPR | Same as GDPR |
| Brazil | LGPD | Similar to GDPR, explicit consent for marketing |
## Unsubscribe Requirements Summary
| Law | Timing | Notes |
|-----|--------|-------|
| CAN-SPAM | 10 business days | Must work 30 days after send |
| GDPR | Immediately | Must be as easy as opting in |
| CASL | 10 business days | Must work 60 days after send |
**Universal best practices:** Prominent link, one-click when possible, no login required, free, confirm action.
## Consent Management
**Record:**
- Email address
- Date/time of consent
- Method (form, checkbox)
- What they consented to
- Source (which page/form)
**Storage:** Database with timestamps, audit trail of changes, link to user account.
## Data Retention
| Law | Requirement |
|-----|-------------|
| GDPR | Keep only as long as necessary, delete when no longer needed |
| CASL | Keep consent records 3 years after expiration |
**Best practice:** Have clear retention policy, honor deletion requests promptly, review and clean regularly.
## Privacy Policy Must Include
- What data you collect
- How you use data
- Who you share data with
- User rights (access, deletion)
- How to contact about privacy
## International Sending
**Best practice:** Follow the most restrictive requirements (usually GDPR) to ensure compliance across all regions.
## Related
- [Email Capture](./email-capture.md) - Implement consent forms and double opt-in
- [Marketing Emails](./marketing-emails.md) - Consent and unsubscribe requirements
- [List Management](./list-management.md) - Handle unsubscribes and deletion requests

View File

@@ -1,120 +0,0 @@
# Email Deliverability
Ensuring emails reach inboxes through proper authentication and sender reputation.
## Email Authentication
**Required by Gmail/Yahoo** - unauthenticated emails will be rejected or spam-filtered.
### SPF (Sender Policy Framework)
Specifies which servers can send email for your domain.
```
v=spf1 include:_spf.resend.com ~all
```
- Add TXT record to DNS
- Use `~all` (soft fail) for testing, `-all` (hard fail) for production
- Keep under 10 DNS lookups
### DKIM (DomainKeys Identified Mail)
Cryptographic signature proving email authenticity.
- Generate keys (provided by email service)
- Add public key as TXT record in DNS
- Use 2048-bit keys, rotate every 6-12 months
### DMARC
Policy for handling SPF/DKIM failures + reporting.
```
v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com
```
**Rollout:** `p=none` (monitor) → `p=quarantine; pct=25``p=reject`
### BIMI (Optional)
Display brand logo in email clients. Requires DMARC `p=quarantine` or `p=reject`.
### Verify Your Setup
Check DNS records directly:
```bash
# SPF record
dig TXT yourdomain.com +short
# DKIM record (replace 'resend' with your selector)
dig TXT resend._domainkey.yourdomain.com +short
# DMARC record
dig TXT _dmarc.yourdomain.com +short
```
**Expected output:** Each command should return your configured record. No output = record missing.
## Sender Reputation
### IP Warming
New IP/domain? Gradually increase volume:
| Week | Daily Volume |
|------|-------------|
| 1 | 50-100 |
| 2 | 200-500 |
| 3 | 1,000-2,000 |
| 4 | 5,000-10,000 |
Start with engaged users. Send consistently. Don't rush.
### Maintaining Reputation
**Do:** Send to engaged users, keep bounce <2%, complaints <0.1%, remove inactive subscribers
**Don't:** Send to purchased lists, ignore bounces/complaints, send inconsistent volumes
## Bounce Handling
| Type | Cause | Action |
|------|-------|--------|
| Hard bounce | Invalid email, domain doesn't exist | Remove immediately |
| Soft bounce | Mailbox full, server down | Retry: 1h → 4h → 24h, remove after 3-5 failures |
**Targets:** <2% good, 2-5% acceptable, >5% concerning, >10% critical
## Complaint Handling
**Targets:** <0.05% excellent, 0.05-0.1% good, >0.2% critical
**Reduce complaints:**
- Only send to opted-in users
- Make unsubscribe easy and immediate
- Use clear sender names and "From" addresses
**Feedback loops:** Set up with Gmail (Postmaster Tools), Yahoo, Microsoft, AOL. Remove complainers immediately.
## Infrastructure
**Dedicated sending domain:** Use subdomain (e.g., `mail.yourdomain.com`) to protect main domain reputation.
**DNS TTL:** Low (300s) during setup, high (3600s+) after stable.
## Troubleshooting
**Emails going to spam?** Check in order:
1. Authentication (SPF, DKIM, DMARC)
2. Sender reputation (blacklists, complaint rates)
3. Content (spammy words, HTML issues)
4. Sending patterns (sudden volume spikes)
**Diagnostic tools:** [mail-tester.com](https://mail-tester.com), [mxtoolbox.com](https://mxtoolbox.com), [Google Postmaster Tools](https://postmaster.google.com)
## Related
- [List Management](./list-management.md) - Handle bounces and complaints to protect reputation
- [Sending Reliability](./sending-reliability.md) - Retry logic and error handling

View File

@@ -1,126 +0,0 @@
# Email Capture Best Practices
Collecting email addresses responsibly with validation, verification, and proper consent.
## Email Validation
### Client-Side
**HTML5:**
```html
<input type="email" required>
```
**Best practices:**
- Validate on blur or with short debounce
- Show clear error messages
- Don't be too strict (allow unusual but valid formats)
- Client-side validation ≠ deliverability
### Server-Side (Required)
Always validate server-side—client-side can be bypassed.
**Check:**
- Email format (RFC 5322)
- Domain exists (DNS lookup)
- Domain has MX records
- Optionally: disposable email detection
## Email Verification
Confirms address belongs to user and is deliverable.
### Process
1. User submits email
2. Send verification email with unique link/token
3. User clicks link
4. Mark as verified
5. Allow access/add to list
**Timing:** Send immediately, include expiration (24-48 hours), allow resend after 60 seconds, limit resend attempts (3/hour).
## Single vs Double Opt-In
| | Single Opt-In | Double Opt-In |
|--|---------------|---------------|
| **Process** | Add to list immediately | Require email confirmation first |
| **Pros** | Lower friction, faster growth | Verified addresses, better engagement, meets GDPR/CASL |
| **Cons** | Higher invalid rate, lower engagement | Some users don't confirm |
| **Use for** | Account creation, transactional | Marketing lists, newsletters |
**Recommendation:** Double opt-in for all marketing emails.
## Form Design
### Email Input
- Use `type="email"` for mobile keyboard
- Include placeholder ("you@example.com")
- Clear error messages ("Please enter a valid email address" not "Invalid")
### Consent Checkboxes (Marketing)
- **Unchecked by default** (required)
- Specific language about what they're signing up for
- Separate checkboxes for different email types
- Link to privacy policy
```
☐ Subscribe to our weekly newsletter with product updates
☐ Send me promotional offers and deals
```
**Don't:** Pre-check boxes, use vague language, hide in terms.
### Form Layout
- Keep simple and focused
- One primary action
- Clear value proposition
- Mobile-friendly
- Accessible (labels, ARIA)
## Error Handling
### Invalid Email
- Show clear error message
- Suggest corrections for common typos (@gmial.com → @gmail.com)
- Allow user to fix and resubmit
### Already Registered
- Accounts: "This email is already registered. [Sign in]"
- Marketing: "You're already subscribed! [Manage preferences]"
- Don't reveal if account exists (security)
### Rate Limiting
- Limit verification emails (3/hour per email)
- Rate limit form submissions
- Use CAPTCHA sparingly if needed
- Monitor for abuse patterns
## Verification Emails
**Content:**
- Clear purpose ("Verify your email address")
- Prominent verification button
- Expiration time
- Resend option
- "I didn't request this" notice
**Design:**
- Mobile-friendly
- Large, tappable button
- Clear call-to-action
See [Transactional Emails](./transactional-emails.md) for detailed email design guidance.
## Related
- [Compliance](./compliance.md) - Legal requirements for consent (GDPR, CASL)
- [Marketing Emails](./marketing-emails.md) - What happens after capture
- [Deliverability](./deliverability.md) - How validation improves sender reputation

View File

@@ -1,177 +0,0 @@
# Email Types: Transactional vs Marketing
Understanding the difference between transactional and marketing emails is crucial for compliance, deliverability, and user experience. This guide explains the distinctions and provides a catalog of transactional emails your app should include.
## When to Use This
- Deciding whether an email should be transactional or marketing
- Understanding legal distinctions between email types
- Planning what transactional emails your app needs
- Ensuring compliance with email regulations
- Setting up separate sending infrastructure
## Transactional vs Marketing: Key Differences
### Transactional Emails
**Definition:** Emails that facilitate or confirm a transaction the user initiated or expects. They're directly related to an action the user took.
**Characteristics:**
- User-initiated or expected
- Time-sensitive and actionable
- Required for the user to complete an action
- Not promotional in nature
- Can be sent without explicit opt-in (with limitations)
**Examples:**
- Password reset links
- Order confirmations
- Account verification
- OTP/2FA codes
- Shipping notifications
### Marketing Emails
**Definition:** Emails sent for promotional, advertising, or informational purposes that are not directly related to a specific transaction.
**Characteristics:**
- Promotional or informational content
- Not time-sensitive to complete a transaction
- Require explicit opt-in (consent)
- Must include unsubscribe options
- Subject to stricter compliance requirements
**Examples:**
- Newsletters
- Product announcements
- Promotional offers
- Company updates
- Educational content
## Legal Distinctions
### CAN-SPAM Act (US)
**Transactional emails:**
- Can be sent without opt-in
- Must be related to a transaction
- Cannot contain promotional content (with exceptions)
- Must identify sender and provide contact information
**Marketing emails:**
- Require opt-out mechanism (not opt-in in US)
- Must include clear sender identification
- Must include physical mailing address
- Must honor opt-out requests within 10 business days
### GDPR (EU)
**Transactional emails:**
- Can be sent based on legitimate interest or contract fulfillment
- Must be necessary for service delivery
- Cannot contain marketing content without consent
**Marketing emails:**
- Require explicit opt-in consent
- Must clearly state purpose of data collection
- Must provide easy unsubscribe
- Subject to data protection requirements
### CASL (Canada)
**Transactional emails:**
- Can be sent without consent if related to ongoing business relationship
- Must be factual and not promotional
**Marketing emails:**
- Require express or implied consent
- Must include unsubscribe mechanism
- Must identify sender clearly
## When to Use Each Type
### Use Transactional When:
- User needs the email to complete an action
- Email confirms a transaction or account change
- Email provides security-related information
- Email is expected based on user action
- Content is time-sensitive and actionable
### Use Marketing When:
- Promoting products or services
- Sending newsletters or updates
- Sharing educational content
- Announcing features or company news
- Content is not required for a transaction
## Hybrid Emails: The Gray Area
Some emails mix transactional and marketing content. Be careful:
**Best practice:** Keep transactional and marketing separate. If you must include marketing in a transactional email:
- Make transactional content primary
- Keep marketing content minimal and clearly separated
- Ensure transactional purpose is clear
- Check local regulations (some regions prohibit this)
**Example of acceptable hybrid:**
- Order confirmation (transactional) with a small "You might also like" section (marketing)
**Example of problematic hybrid:**
- Newsletter (marketing) with a small order status update (transactional)
## Transactional Email Catalog
For a complete catalog of transactional emails and recommended combinations by app type, see [Transactional Email Catalog](./transactional-email-catalog.md).
**Quick reference - Essential emails for most apps:**
1. **Email verification** - Required for account creation
2. **Password reset** - Required for account recovery
3. **Welcome email** - Good user experience
The catalog includes detailed guidance for:
- Authentication-focused apps
- Newsletter / content platforms
- E-commerce / marketplaces
- SaaS / subscription services
- Financial / fintech apps
- Social / community platforms
- Developer tools / API platforms
- Healthcare / HIPAA-compliant apps
## Sending Infrastructure
### Separate Infrastructure
**Best practice:** Use separate sending infrastructure for transactional and marketing emails.
**Benefits:**
- Protect transactional deliverability
- Different authentication domains
- Independent reputation
- Easier compliance management
**Implementation:**
- Use different subdomains (e.g., `mail.app.com` for transactional, `news.app.com` for marketing)
- Separate email service accounts or API keys
- Different monitoring and alerting
### Email Service Considerations
Choose an email service that:
- Provides reliable delivery for transactional emails
- Offers separate sending domains
- Has good API for programmatic sending
- Provides webhooks for delivery events
- Supports authentication setup (SPF, DKIM, DMARC)
Services like Resend are designed for transactional emails and provide the infrastructure and tools needed for reliable delivery.
## Related Topics
- [Transactional Emails](./transactional-emails.md) - Best practices for sending transactional emails
- [Marketing Emails](./marketing-emails.md) - Best practices for marketing emails
- [Compliance](./compliance.md) - Legal requirements for each email type
- [Deliverability](./deliverability.md) - Ensuring transactional emails are delivered

View File

@@ -1,157 +0,0 @@
# List Management
Maintaining clean email lists through suppression, hygiene, and data retention.
## Suppression Lists
A suppression list prevents sending to addresses that should never receive email.
### What to Suppress
| Reason | Action | Can Unsuppress? |
|--------|--------|-----------------|
| Hard bounce | Add immediately | No (address invalid) |
| Complaint (spam) | Add immediately | No (legal requirement) |
| Unsubscribe | Add immediately | Only if user re-subscribes |
| Soft bounce (3x) | Add after threshold | Yes, after 30-90 days |
| Manual removal | Add on request | Only if user requests |
### Implementation
```typescript
// Suppression list schema
interface SuppressionEntry {
email: string;
reason: 'hard_bounce' | 'complaint' | 'unsubscribe' | 'soft_bounce' | 'manual';
created_at: Date;
source_email_id?: string; // Which email triggered this
}
// Check before every send
async function canSendTo(email: string): Promise<boolean> {
const suppressed = await db.suppressions.findOne({ email });
return !suppressed;
}
// Add to suppression list
async function suppressEmail(email: string, reason: string, sourceId?: string) {
await db.suppressions.upsert({
email: email.toLowerCase(),
reason,
created_at: new Date(),
source_email_id: sourceId,
});
}
```
### Pre-Send Check
**Always check suppression before sending:**
```typescript
async function sendEmail(to: string, emailData: EmailData) {
if (!await canSendTo(to)) {
console.log(`Skipping suppressed email: ${to}`);
return { skipped: true, reason: 'suppressed' };
}
return await resend.emails.send({ to, ...emailData });
}
```
## List Hygiene
Regular maintenance to keep lists healthy.
### Automated Cleanup
| Task | Frequency | Action |
|------|-----------|--------|
| Remove hard bounces | Real-time (via webhook) | Immediate suppression |
| Remove complaints | Real-time (via webhook) | Immediate suppression |
| Process unsubscribes | Real-time | Remove from marketing lists |
| Review soft bounces | Daily | Suppress after 3 failures |
| Remove inactive | Monthly | Re-engagement → remove |
### Re-engagement Campaigns
Before removing inactive subscribers:
1. **Identify inactive:** No opens/clicks in 90-180 days
2. **Send re-engagement:** "We miss you" or "Still interested?"
3. **Wait 14-30 days** for response
4. **Remove non-responders** from active lists
```typescript
async function runReengagement() {
const inactive = await getInactiveSubscribers(90); // 90 days
for (const subscriber of inactive) {
if (!subscriber.reengagement_sent) {
await sendReengagementEmail(subscriber);
await markReengagementSent(subscriber.email);
} else if (daysSince(subscriber.reengagement_sent) > 30) {
await removeFromMarketingLists(subscriber.email);
}
}
}
```
## Data Retention
### Email Logs
| Data Type | Recommended Retention | Notes |
|-----------|----------------------|-------|
| Send attempts | 90 days | Debugging, analytics |
| Delivery status | 90 days | Compliance, reporting |
| Bounce/complaint events | 3 years | Required for CASL |
| Suppression list | Indefinite | Never delete |
| Email content | 30 days | Storage costs |
| Consent records | 3 years after expiry | Legal requirement |
### Retention Policy Implementation
```typescript
// Daily cleanup job
async function cleanupOldData() {
const now = new Date();
// Delete old email logs (keep 90 days)
await db.emailLogs.deleteMany({
created_at: { $lt: subDays(now, 90) }
});
// Delete old email content (keep 30 days)
await db.emailContent.deleteMany({
created_at: { $lt: subDays(now, 30) }
});
// Never delete: suppressions, consent records
}
```
## Metrics to Monitor
| Metric | Target | Alert Threshold |
|--------|--------|-----------------|
| Bounce rate | <2% | >5% |
| Complaint rate | <0.1% | >0.2% |
| Suppression list growth | Stable | Sudden spike |
| List churn | <2%/month | >5%/month |
## Transactional vs Marketing Lists
**Keep separate:**
- Transactional: Can send to anyone with account relationship
- Marketing: Only opted-in subscribers
**Suppression applies to both:** Hard bounces and complaints suppress across all email types.
**Unsubscribe is marketing-only:** User unsubscribing from marketing can still receive transactional emails (password resets, order confirmations).
## Related
- [Webhooks & Events](./webhooks-events.md) - Receive bounce/complaint notifications
- [Deliverability](./deliverability.md) - How list hygiene affects sender reputation
- [Compliance](./compliance.md) - Legal requirements for data retention

View File

@@ -1,115 +0,0 @@
# Marketing Email Best Practices
Promotional emails that require explicit consent and provide value to recipients.
## Core Principles
1. **Consent first** - Explicit opt-in required (especially GDPR/CASL)
2. **Value-driven** - Provide useful content, not just promotions
3. **Respect preferences** - Let users control frequency and content types
## Opt-In Requirements
### Explicit Opt-In
**What counts:**
- User checks unchecked box
- User clicks "Subscribe" button
- User completes form with clear subscription intent
**What doesn't count:**
- Pre-checked boxes
- Opt-out model
- Assumed consent from purchase
- Purchased/rented lists
### Informed Consent
Disclose: email types, frequency, sender identity, how to unsubscribe.
✅ "Subscribe to our weekly newsletter with product updates and tips"
❌ "Sign up for emails"
### Double Opt-In (Recommended)
1. User submits email
2. Send confirmation email with verification link
3. User clicks to confirm
4. Add to list only after confirmation
Benefits: Verifies deliverability, confirms intent, reduces complaints, required in some regions (Germany).
## Unsubscribe Requirements
**Must be:**
- Prominent in every email
- One-click (preferred) or simple process
- Immediate (GDPR) or within 10 days (CAN-SPAM)
- Free, no login required
**Preference center options:** Frequency (daily/weekly/monthly), content types, complete unsubscribe.
## Content and Design
### Subject Lines
- Clear and specific (50 chars or less for mobile)
- Create curiosity without misleading
- A/B test regularly
✅ "Your weekly digest: 5 productivity tips"
❌ "You won't believe what happened!"
### Structure
**Above fold:** Value proposition, primary CTA, engaging visual
**Body:** Scannable (short paragraphs, bullets), clear hierarchy, multiple CTAs
**Footer:** Unsubscribe link, company info, physical address (CAN-SPAM), social links
### Mobile-First
- Single column layout
- 44x44px minimum buttons
- 16px minimum text
- Test on iOS, Android, dark mode
## Segmentation
**Segment by:** Behavior (purchases, activity), demographics, preferences, engagement level, signup source.
Benefits: Higher open/click rates, lower unsubscribes, better experience.
## Personalization
**Options:** Name in subject/greeting, location-specific content, behavior-based recommendations, purchase history.
**Don't over-personalize** - can feel intrusive. Use data you have permission to use.
## Frequency and Timing
**Frequency:** Start conservative, increase based on engagement, let users set preferences, monitor unsubscribe rates.
**Timing:** Weekday mornings (9-11 AM local), Tuesday-Thursday often best. Test your specific audience.
## List Hygiene
**Remove immediately:** Hard bounces, unsubscribes, complaints
**Remove after inactivity:** Send re-engagement campaign first, then remove non-responders
**Monitor:** Bounce rate <2%, complaint rate <0.1%
## Required Elements (All Marketing Emails)
- Clear sender identification
- Physical mailing address (CAN-SPAM)
- Unsubscribe mechanism
- Indication it's marketing (GDPR)
## Related
- [Compliance](./compliance.md) - Detailed legal requirements by region
- [Email Capture](./email-capture.md) - Collecting consent properly
- [List Management](./list-management.md) - Maintaining list hygiene

View File

@@ -1,155 +0,0 @@
# Sending Reliability
Ensuring emails are sent exactly once and handling failures gracefully.
## Idempotency
Prevent duplicate emails when retrying failed requests.
### The Problem
Network issues, timeouts, or server errors can leave you uncertain if an email was sent. Retrying without idempotency risks sending duplicates.
### Solution: Idempotency Keys
Send a unique key with each request. If the same key is sent again, the server returns the original response instead of sending another email.
```typescript
// Generate deterministic key based on the business event
const idempotencyKey = `password-reset-${userId}-${resetRequestId}`;
await resend.emails.send({
from: 'noreply@example.com',
to: user.email,
subject: 'Reset your password',
html: emailHtml,
}, {
headers: {
'Idempotency-Key': idempotencyKey
}
});
```
### Key Generation Strategies
| Strategy | Example | Use When |
|----------|---------|----------|
| Event-based | `order-confirm-${orderId}` | One email per event (recommended) |
| Request-scoped | `reset-${userId}-${resetRequestId}` | Retries within same request |
| UUID | `crypto.randomUUID()` | No natural key (generate once, reuse on retry) |
**Best practice:** Use deterministic keys based on the business event. If you retry the same logical send, the same key must be generated. Avoid `Date.now()` or random values generated fresh on each attempt.
**Key expiration:** Idempotency keys are typically cached for 24 hours. Retries within this window return the original response. After expiration, the same key triggers a new send—so complete your retry logic well within 24 hours.
## Retry Logic
Handle transient failures with exponential backoff.
### When to Retry
| Error Type | Retry? | Notes |
|------------|--------|-------|
| 5xx (server error) | ✅ Yes | Transient, likely to resolve |
| 429 (rate limit) | ✅ Yes | Wait for rate limit window |
| 4xx (client error) | ❌ No | Fix the request first |
| Network timeout | ✅ Yes | Transient |
| DNS failure | ✅ Yes | May be transient |
### Exponential Backoff
```typescript
async function sendWithRetry(emailData, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await resend.emails.send(emailData);
} catch (error) {
if (!isRetryable(error) || attempt === maxRetries - 1) {
throw error;
}
const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
await sleep(delay + Math.random() * 1000); // Add jitter
}
}
}
function isRetryable(error) {
return error.statusCode >= 500 ||
error.statusCode === 429 ||
error.code === 'ETIMEDOUT';
}
```
**Backoff schedule:** 1s → 2s → 4s → 8s (with jitter to prevent thundering herd)
## Error Handling
### Common Error Codes
| Code | Meaning | Action |
|------|---------|--------|
| 400 | Bad request | Fix payload (invalid email, missing field) |
| 401 | Unauthorized | Check API key |
| 403 | Forbidden | Check permissions, domain verification |
| 404 | Not found | Check endpoint URL |
| 422 | Validation error | Fix request data |
| 429 | Rate limited | Back off, retry after delay |
| 500 | Server error | Retry with backoff |
| 503 | Service unavailable | Retry with backoff |
### Error Handling Pattern
```typescript
try {
const result = await resend.emails.send(emailData);
await logSuccess(result.id, emailData);
} catch (error) {
if (error.statusCode === 429) {
await queueForRetry(emailData, error.retryAfter);
} else if (error.statusCode >= 500) {
await queueForRetry(emailData);
} else {
await logFailure(error, emailData);
await alertOnCriticalEmail(emailData); // For password resets, etc.
}
}
```
## Queuing for Reliability
For critical emails, use a queue to ensure delivery even if the initial send fails.
**Benefits:**
- Survives application restarts
- Automatic retry handling
- Rate limit management
- Audit trail
**Simple pattern:**
1. Write email to queue/database with "pending" status
2. Process queue, attempt send
3. On success: mark "sent", store message ID
4. On retryable failure: increment retry count, schedule retry
5. On permanent failure: mark "failed", alert
## Timeouts
Set appropriate timeouts to avoid hanging requests.
```typescript
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000);
try {
await resend.emails.send(emailData, { signal: controller.signal });
} finally {
clearTimeout(timeout);
}
```
**Recommended:** 10-30 seconds for email API calls.
## Related
- [Webhooks & Events](./webhooks-events.md) - Process delivery confirmations and failures
- [List Management](./list-management.md) - Handle bounces and suppress invalid addresses

View File

@@ -1,418 +0,0 @@
# Transactional Email Catalog
A comprehensive catalog of transactional emails organized by category, plus recommended email combinations for different app types.
## When to Use This
- Planning what transactional emails your app needs
- Choosing the right emails for your app type
- Understanding what content each email type should include
- Implementing transactional email features
## Email Combinations by App Type
Use these combinations as a starting point based on what you're building.
### Authentication-Focused App
Apps where user accounts and security are core (login systems, identity providers, account management).
**Essential:**
- Email verification
- Password reset
- OTP / 2FA codes
- Security alerts (new device, password change)
- Account update notifications
**Optional:**
- Welcome email
- Account deletion confirmation
### Newsletter / Content Platform
Apps focused on content delivery and subscriptions.
**Essential:**
- Email verification
- Password reset
- Welcome email
- Subscription confirmation
**Optional:**
- OTP / 2FA codes
- Account update notifications
### E-commerce / Marketplace
Apps where users buy products or services.
**Essential:**
- Email verification
- Password reset
- Welcome email
- Order confirmation
- Shipping notifications
- Invoice / receipt
- Payment failed notices
**Optional:**
- OTP / 2FA codes
- Security alerts
- Subscription confirmations (for recurring orders)
### SaaS / Subscription Service
Apps with paid subscription tiers and ongoing billing.
**Essential:**
- Email verification
- Password reset
- Welcome email
- OTP / 2FA codes
- Security alerts
- Subscription confirmation
- Subscription renewal notice
- Payment failed notices
- Invoice / receipt
**Optional:**
- Account update notifications
- Feature change notifications (for breaking changes)
### Financial / Fintech App
Apps handling money, payments, or sensitive financial data.
**Essential:**
- Email verification
- Password reset
- OTP / 2FA codes (required for sensitive actions)
- Security alerts (all types)
- Account update notifications
- Transaction confirmations
- Invoice / receipt
- Payment failed notices
**Optional:**
- Welcome email
- Compliance notices
### Social / Community Platform
Apps focused on user interaction and community features.
**Essential:**
- Email verification
- Password reset
- Welcome email
- Security alerts
**Optional:**
- OTP / 2FA codes
- Account update notifications
- Activity notifications (mentions, replies)
### Developer Tools / API Platform
Apps targeting developers with API access and integrations.
**Essential:**
- Email verification
- Password reset
- OTP / 2FA codes
- Security alerts
- API key notifications (creation, expiration)
- Subscription confirmation
- Payment failed notices
**Optional:**
- Welcome email
- Usage alerts (approaching limits)
- Feature change notifications
### Healthcare / HIPAA-Compliant App
Apps handling protected health information.
**Essential:**
- Email verification
- Password reset
- OTP / 2FA codes (required)
- Security alerts (all types, detailed)
- Account update notifications
- Appointment confirmations
**Optional:**
- Welcome email
- Compliance notices
**Note:** Healthcare apps have strict requirements. Emails should contain minimal PHI and link to secure portals for sensitive information.
---
## Full Email Catalog
### Authentication & Security
#### Email Verification / Account Verification
**When to send:** Immediately after user signs up or changes email address.
**Purpose:** Verify the email address belongs to the user.
**Content should include:**
- Clear verification link or code
- Expiration time (typically 24-48 hours)
- Instructions on what to do
- Security notice if link is clicked by mistake
**Best practices:**
- Send immediately (within seconds)
- Include expiration notice
- Provide resend option
- Link to support if issues
#### OTP / 2FA Codes
**When to send:** When user requests two-factor authentication code.
**Purpose:** Provide time-sensitive authentication code.
**Content should include:**
- The OTP code (clearly displayed)
- Expiration time (typically 5-10 minutes)
- Security warnings
- Instructions on what to do if not requested
**Best practices:**
- Send immediately
- Code should be large and easy to read
- Include expiration prominently
- Warn about sharing codes
- Provide "I didn't request this" link
#### Password Reset
**When to send:** When user requests password reset.
**Purpose:** Allow user to securely reset forgotten password.
**Content should include:**
- Reset link (with token)
- Expiration time (typically 1 hour)
- Security warnings
- Instructions if not requested
**Best practices:**
- Send immediately
- Link expires quickly (1 hour)
- Include IP address and location if available
- Provide "I didn't request this" link
- Don't include the old password
#### Security Alerts
**When to send:** When security-relevant events occur (login from new device, password change, etc.).
**Purpose:** Notify user of account security events.
**Content should include:**
- What happened (clear description)
- When it happened
- Location/IP if available
- Action to take if suspicious
- Link to security settings
**Best practices:**
- Send immediately
- Be clear and specific
- Include actionable steps
- Provide way to report suspicious activity
### Account Management
#### Welcome Email
**When to send:** Immediately after successful account creation and verification.
**Purpose:** Welcome new users and guide them to next steps.
**Content should include:**
- Welcome message
- Key features or next steps
- Links to important resources
- Support contact information
**Best practices:**
- Send after email verification
- Keep it focused and actionable
- Don't overwhelm with information
- Set expectations about future emails
#### Account Update Notifications
**When to send:** When user changes account settings (email, password, profile, etc.).
**Purpose:** Confirm account changes and provide security notice.
**Content should include:**
- What changed
- When it changed
- Action to take if unauthorized
- Link to account settings
**Best practices:**
- Send immediately after change
- Be specific about what changed
- Include security notice
- Provide easy way to revert if needed
### E-commerce & Transactions
#### Order Confirmations
**When to send:** Immediately after order is placed.
**Purpose:** Confirm order details and provide receipt.
**Content should include:**
- Order number
- Items ordered with quantities
- Pricing breakdown
- Shipping address
- Estimated delivery date
- Order tracking link (if available)
**Best practices:**
- Send within minutes of order
- Include all order details
- Make it easy to print or save
- Provide customer service contact
#### Shipping Notifications
**When to send:** When order ships, with tracking updates.
**Purpose:** Notify user that order has shipped and provide tracking.
**Content should include:**
- Order number
- Tracking number
- Carrier information
- Expected delivery date
- Tracking link
- Shipping address confirmation
**Best practices:**
- Send when order ships
- Include tracking number prominently
- Provide carrier tracking link
- Update on major tracking milestones
#### Invoices and Receipts
**When to send:** After payment is processed.
**Purpose:** Provide payment confirmation and receipt.
**Content should include:**
- Invoice/receipt number
- Payment amount
- Payment method
- Items/services purchased
- Payment date
- Downloadable PDF (if applicable)
**Best practices:**
- Send immediately after payment
- Include all payment details
- Make it easy to download/save
- Include tax information if applicable
### Subscriptions & Billing
#### Subscription Confirmations
**When to send:** When user subscribes or changes subscription.
**Purpose:** Confirm subscription details and billing information.
**Content should include:**
- Subscription plan details
- Billing amount and frequency
- Next billing date
- Payment method
- Link to manage subscription
**Best practices:**
- Send immediately after subscription
- Clearly state billing terms
- Provide easy cancellation option
- Include support contact
#### Subscription Renewal Notices
**When to send:** Before subscription renews (typically 3-7 days before).
**Purpose:** Notify user of upcoming renewal and charge.
**Content should include:**
- Renewal date
- Amount to be charged
- Payment method on file
- Link to update payment method
- Link to cancel if desired
**Best practices:**
- Send with enough notice (3-7 days)
- Be clear about amount and date
- Make it easy to update payment method
- Provide cancellation option
#### Payment Failed Notices
**When to send:** When subscription payment fails.
**Purpose:** Notify user of payment failure and provide resolution steps.
**Content should include:**
- What happened
- Amount that failed
- Reason for failure (if available)
- Steps to resolve
- Link to update payment method
- Consequences if not resolved
**Best practices:**
- Send immediately after failure
- Be clear about consequences
- Provide easy resolution path
- Include support contact
### Notifications & Updates
#### Feature Announcements (Transactional)
**When to send:** When a feature the user is using changes significantly.
**Purpose:** Notify users of changes that affect their use of the service.
**Content should include:**
- What changed
- How it affects the user
- What action (if any) is needed
- Link to more information
**Best practices:**
- Only for significant changes
- Focus on user impact
- Provide clear next steps
- Link to documentation
**Note:** General feature announcements are marketing emails. Only send as transactional if the change directly affects an active feature the user is using.
## Related Topics
- [Email Types](./email-types.md) - Understanding transactional vs marketing
- [Transactional Emails](./transactional-emails.md) - Best practices for sending transactional emails
- [Compliance](./compliance.md) - Legal requirements for each email type

View File

@@ -1,92 +0,0 @@
# Transactional Email Best Practices
Clear, actionable emails that users expect and need—password resets, confirmations, OTPs.
## Core Principles
1. **Clarity over creativity** - Users need to understand and act quickly
2. **Action-oriented** - Clear purpose, obvious primary action
3. **Time-sensitive** - Send immediately (within seconds)
## Subject Lines
**Be specific and include context:**
| ✅ Good | ❌ Bad |
|---------|--------|
| Reset your password for [App] | Action required |
| Your order #12345 has shipped | Update on your order |
| Your 2FA code: 123456 | Security code |
| Verify your email for [App] | Verify your email |
Include identifiers when helpful: order numbers, account names, expiration times.
## Pre-Header
The text snippet after subject line. Use it to:
- Reinforce subject ("This link expires in 1 hour")
- Add urgency or context
- Call-to-action preview
Keep under 90-100 characters.
## Content Structure
**Above the fold (first screen):**
- Clear purpose
- Primary action button
- Time-sensitive details (expiration)
**Hierarchy:** Header → Primary message → Details → Action button → Secondary info
**Format:** Short paragraphs (2-3 sentences), bullet points, bold for emphasis, white space.
## Mobile-First Design
60%+ emails opened on mobile.
- **Layout:** Single column, stack vertically
- **Buttons:** 44x44px minimum, full-width on mobile
- **Text:** 16px minimum body, 20-24px headings
- **OTP codes:** 24-32px, monospace font
## Sender Configuration
| Field | Best Practice | Example |
|-------|--------------|---------|
| From Name | App/company name, consistent | [App Name] |
| From Email | Subdomain, real address | hello@mail.yourdomain.com |
| Reply-To | Monitored inbox | support@yourdomain.com |
Avoid `noreply@` - users reply to transactional emails.
## Code and Link Display
**OTP/Verification codes:**
- Large (24-32px), monospace font
- Centered, clear label
- Include expiration nearby
- Make copyable
**Buttons:**
- Large, tappable (44x44px+)
- Contrasting colors
- Clear action text ("Reset Password", "Verify Email")
- HTTPS links only
## Error Handling
**Resend functionality:**
- Allow after 60 seconds
- Limit attempts (3 per hour)
- Show countdown timer
**Expired links:**
- Clear "expired" message
- Offer to send new link
- Provide support contact
**"I didn't request this":**
- Include in password resets, OTPs, security alerts
- Link to security contact
- Log clicks for monitoring

View File

@@ -1,163 +0,0 @@
# Webhooks and Events
Receiving and processing email delivery events in real-time.
## Event Types
| Event | When Fired | Use For |
|-------|------------|---------|
| `email.sent` | Email accepted by Resend | Confirming send initiated |
| `email.delivered` | Email delivered to recipient server | Confirming delivery |
| `email.bounced` | Email bounced (hard or soft) | List hygiene, alerting |
| `email.complained` | Recipient marked as spam | Immediate unsubscribe |
| `email.opened` | Recipient opened email | Engagement tracking |
| `email.clicked` | Recipient clicked link | Engagement tracking |
## Webhook Setup
### 1. Create Endpoint
Your endpoint must:
- Accept POST requests
- Return 2xx status quickly (within 5 seconds)
- Handle duplicate events (idempotent processing)
```typescript
app.post('/webhooks/resend', async (req, res) => {
// Return 200 immediately to acknowledge receipt
res.status(200).send('OK');
// Process asynchronously
processWebhookAsync(req.body).catch(console.error);
});
```
### 2. Verify Signatures
Always verify webhook signatures to prevent spoofing.
```typescript
import { Webhook } from 'svix';
const webhook = new Webhook(process.env.RESEND_WEBHOOK_SECRET);
app.post('/webhooks/resend', (req, res) => {
try {
const payload = webhook.verify(
JSON.stringify(req.body),
{
'svix-id': req.headers['svix-id'],
'svix-timestamp': req.headers['svix-timestamp'],
'svix-signature': req.headers['svix-signature'],
}
);
// Process verified payload
} catch (err) {
return res.status(400).send('Invalid signature');
}
});
```
### 3. Register Webhook URL
Configure your webhook endpoint in the Resend dashboard or via API.
## Processing Events
### Bounce Handling
```typescript
async function handleBounce(event) {
const { email_id, email, bounce_type } = event.data;
if (bounce_type === 'hard') {
// Permanent failure - remove from all lists
await suppressEmail(email, 'hard_bounce');
await removeFromAllLists(email);
} else {
// Soft bounce - track and remove after threshold
await incrementSoftBounce(email);
const count = await getSoftBounceCount(email);
if (count >= 3) {
await suppressEmail(email, 'soft_bounce_limit');
}
}
}
```
### Complaint Handling
```typescript
async function handleComplaint(event) {
const { email } = event.data;
// Immediate suppression - no exceptions
await suppressEmail(email, 'complaint');
await removeFromAllLists(email);
await logComplaint(event); // For analysis
}
```
### Delivery Confirmation
```typescript
async function handleDelivered(event) {
const { email_id } = event.data;
await updateEmailStatus(email_id, 'delivered');
}
```
## Idempotent Processing
Webhooks may be sent multiple times. Use event IDs to prevent duplicate processing.
```typescript
async function processWebhook(event) {
const eventId = event.id;
// Check if already processed
if (await isEventProcessed(eventId)) {
return; // Skip duplicate
}
// Process event
await handleEvent(event);
// Mark as processed
await markEventProcessed(eventId);
}
```
## Error Handling
### Retry Behavior
If your endpoint returns non-2xx, webhooks will retry with exponential backoff:
- Retry 1: ~30 seconds
- Retry 2: ~1 minute
- Retry 3: ~5 minutes
- (continues for ~24 hours)
### Best Practices
- **Return 200 quickly** - Process asynchronously to avoid timeouts
- **Be idempotent** - Handle duplicate deliveries gracefully
- **Log everything** - Store raw events for debugging
- **Alert on failures** - Monitor webhook processing errors
- **Queue for processing** - Use a job queue for complex handling
## Testing Webhooks
**Local development:** Use ngrok or similar to expose localhost.
```bash
ngrok http 3000
# Use the ngrok URL as your webhook endpoint
```
**Verify handling:** Send test events through Resend dashboard or manually trigger each event type.
## Related
- [List Management](./list-management.md) - What to do with bounce/complaint data
- [Sending Reliability](./sending-reliability.md) - Retry logic when sends fail

View File

@@ -1,110 +0,0 @@
---
name: overseer-plan
description: Convert markdown planning documents to Overseer tasks via MCP codemode. Use when converting plans, specs, or design docs to trackable task hierarchies.
license: MIT
metadata:
author: dmmulroy
version: "1.0.0"
---
# Converting Markdown Documents to Overseer Tasks
Use `/overseer-plan` to convert any markdown planning document into trackable Overseer tasks.
## When to Use
- After completing a plan in plan mode
- Converting specs/design docs to implementation tasks
- Creating tasks from roadmap or milestone documents
## Usage
```
/overseer-plan <markdown-file-path>
/overseer-plan <file> --priority 3 # Set priority (1-5)
/overseer-plan <file> --parent <task-id> # Create as child of existing task
```
## What It Does
1. Reads markdown file
2. Extracts title from first `#` heading (strips "Plan: " prefix)
3. Creates Overseer milestone (or child task if `--parent` provided)
4. Analyzes structure for child task breakdown
5. Creates child tasks (depth 1) or subtasks (depth 2) when appropriate
6. Returns task ID and breakdown summary
## Hierarchy Levels
| Depth | Name | Example |
|-------|------|---------|
| 0 | **Milestone** | "Add user authentication system" |
| 1 | **Task** | "Implement JWT middleware" |
| 2 | **Subtask** | "Add token verification function" |
## Breakdown Decision
**Create subtasks when:**
- 3-7 clearly separable work items
- Implementation across multiple files/components
- Clear sequential dependencies
**Keep single milestone when:**
- 1-2 steps only
- Work items tightly coupled
- Plan is exploratory/investigative
## Task Quality Criteria
Every task must be:
- **Atomic**: Single committable unit of work
- **Validated**: Has tests OR explicit acceptance criteria in context ("Done when: ...")
- **Clear**: Technical, specific, imperative verb
Every milestone must:
- **Demoable**: Produces runnable/testable increment
- **Builds on prior**: Can depend on previous milestone's output
## Review Workflow
1. Analyze document -> propose breakdown
2. **Invoke Oracle** to review breakdown and suggest improvements
3. Incorporate feedback
4. Create in Overseer (persists to SQLite via MCP)
## After Creating
```javascript
await tasks.get("<id>"); // TaskWithContext (full context + learnings)
await tasks.list({ parentId: "<id>" }); // Task[] (children without context chain)
await tasks.start("<id>"); // Task (VCS required - creates bookmark, records start commit)
await tasks.complete("<id>", { result: "...", learnings: [...] }); // Task (VCS required - commits, bubbles learnings)
```
**VCS Required**: `start` and `complete` require jj or git (fail with `NotARepository` if none found). CRUD operations work without VCS.
**Note**: Priority must be 1-5. Blockers cannot be ancestors or descendants.
## When NOT to Use
- Document incomplete or exploratory
- Content not actionable
- No meaningful planning content
---
## Reading Order
| Task | File |
|------|------|
| Understanding API | @file references/api.md |
| Agent implementation | @file references/implementation.md |
| See examples | @file references/examples.md |
## In This Reference
| File | Purpose |
|------|---------|
| `references/api.md` | Overseer MCP codemode API types/methods |
| `references/implementation.md` | Step-by-step execution instructions for agent |
| `references/examples.md` | Complete worked examples |

View File

@@ -1,192 +0,0 @@
# Overseer Codemode MCP API
Execute JavaScript code to interact with Overseer task management.
## Task Interfaces
```typescript
// Basic task - returned by list(), create(), start(), complete()
// Note: Does NOT include context or learnings fields
interface Task {
id: string;
parentId: string | null;
description: string;
priority: 1 | 2 | 3 | 4 | 5;
completed: boolean;
completedAt: string | null;
startedAt: string | null;
createdAt: string; // ISO 8601
updatedAt: string;
result: string | null; // Completion notes
commitSha: string | null; // Auto-populated on complete
depth: 0 | 1 | 2; // 0=milestone, 1=task, 2=subtask
blockedBy?: string[]; // Blocking task IDs (omitted if empty)
blocks?: string[]; // Tasks this blocks (omitted if empty)
bookmark?: string; // VCS bookmark name (if started)
startCommit?: string; // Commit SHA at start
effectivelyBlocked: boolean; // True if task OR ancestor has incomplete blockers
}
// Task with full context - returned by get(), nextReady()
interface TaskWithContext extends Task {
context: {
own: string; // This task's context
parent?: string; // Parent's context (depth > 0)
milestone?: string; // Root milestone's context (depth > 1)
};
learnings: {
own: Learning[]; // This task's learnings (bubbled from completed children)
parent: Learning[]; // Parent's learnings (depth > 0)
milestone: Learning[]; // Milestone's learnings (depth > 1)
};
}
// Task tree structure - returned by tree()
interface TaskTree {
task: Task;
children: TaskTree[];
}
// Progress summary - returned by progress()
interface TaskProgress {
total: number;
completed: number;
ready: number; // !completed && !effectivelyBlocked
blocked: number; // !completed && effectivelyBlocked
}
// Task type alias for depth filter
type TaskType = "milestone" | "task" | "subtask";
```
## Learning Interface
```typescript
interface Learning {
id: string;
taskId: string;
content: string;
sourceTaskId: string | null;
createdAt: string;
}
```
## Tasks API
```typescript
declare const tasks: {
list(filter?: {
parentId?: string;
ready?: boolean;
completed?: boolean;
depth?: 0 | 1 | 2; // 0=milestones, 1=tasks, 2=subtasks
type?: TaskType; // Alias: "milestone"|"task"|"subtask" (mutually exclusive with depth)
}): Promise<Task[]>;
get(id: string): Promise<TaskWithContext>;
create(input: {
description: string;
context?: string;
parentId?: string;
priority?: 1 | 2 | 3 | 4 | 5; // Must be 1-5
blockedBy?: string[]; // Cannot be ancestors/descendants
}): Promise<Task>;
update(id: string, input: {
description?: string;
context?: string;
priority?: 1 | 2 | 3 | 4 | 5;
parentId?: string;
}): Promise<Task>;
start(id: string): Promise<Task>;
complete(id: string, input?: { result?: string; learnings?: string[] }): Promise<Task>;
reopen(id: string): Promise<Task>;
delete(id: string): Promise<void>;
block(taskId: string, blockerId: string): Promise<void>;
unblock(taskId: string, blockerId: string): Promise<void>;
nextReady(milestoneId?: string): Promise<TaskWithContext | null>;
tree(rootId?: string): Promise<TaskTree | TaskTree[]>;
search(query: string): Promise<Task[]>;
progress(rootId?: string): Promise<TaskProgress>;
};
```
| Method | Returns | Description |
|--------|---------|-------------|
| `list` | `Task[]` | Filter by `parentId`, `ready`, `completed`, `depth`, `type` |
| `get` | `TaskWithContext` | Get task with full context chain + inherited learnings |
| `create` | `Task` | Create task (priority must be 1-5) |
| `update` | `Task` | Update description, context, priority, parentId |
| `start` | `Task` | **VCS required** - creates bookmark, records start commit |
| `complete` | `Task` | **VCS required** - commits changes + bubbles learnings to parent |
| `reopen` | `Task` | Reopen completed task |
| `delete` | `void` | Delete task + best-effort VCS bookmark cleanup |
| `block` | `void` | Add blocker (cannot be self, ancestor, or descendant) |
| `unblock` | `void` | Remove blocker relationship |
| `nextReady` | `TaskWithContext \| null` | Get deepest ready leaf with full context |
| `tree` | `TaskTree \| TaskTree[]` | Get task tree (all milestones if no ID) |
| `search` | `Task[]` | Search by description/context/result (case-insensitive) |
| `progress` | `TaskProgress` | Aggregate counts for milestone or all tasks |
## Learnings API
Learnings are added via `tasks.complete(id, { learnings: [...] })` and bubble to immediate parent (preserving `sourceTaskId`).
```typescript
declare const learnings: {
list(taskId: string): Promise<Learning[]>;
};
```
| Method | Description |
|--------|-------------|
| `list` | List learnings for task |
## VCS Integration (Required for Workflow)
VCS operations are **automatically handled** by the tasks API:
| Task Operation | VCS Effect |
|----------------|------------|
| `tasks.start(id)` | **VCS required** - creates bookmark `task/<id>`, records start commit |
| `tasks.complete(id)` | **VCS required** - commits changes (NothingToCommit = success) |
| `tasks.delete(id)` | Best-effort bookmark cleanup (logs warning on failure) |
**VCS (jj or git) is required** for start/complete. Fails with `NotARepository` if none found. CRUD operations work without VCS.
## Quick Examples
```javascript
// Create milestone with subtask
const milestone = await tasks.create({
description: "Build authentication system",
context: "JWT-based auth with refresh tokens",
priority: 1
});
const subtask = await tasks.create({
description: "Implement token refresh logic",
parentId: milestone.id,
context: "Handle 7-day expiry"
});
// Start work (VCS required - creates bookmark)
await tasks.start(subtask.id);
// ... do implementation work ...
// Complete task with learnings (VCS required - commits changes, bubbles learnings to parent)
await tasks.complete(subtask.id, {
result: "Implemented using jose library",
learnings: ["Use jose instead of jsonwebtoken"]
});
// Get progress summary
const progress = await tasks.progress(milestone.id);
// -> { total: 2, completed: 1, ready: 1, blocked: 0 }
// Search tasks
const authTasks = await tasks.search("authentication");
// Get task tree
const tree = await tasks.tree(milestone.id);
// -> { task: Task, children: TaskTree[] }
```

View File

@@ -1,177 +0,0 @@
# Examples
## Example 1: With Breakdown
### Input (`auth-plan.md`)
```markdown
# Plan: Add Authentication System
## Implementation
1. Create database schema for users/tokens
2. Implement auth controller with endpoints
3. Add JWT middleware for route protection
4. Build frontend login/register forms
5. Add integration tests
```
### Execution
```javascript
const milestone = await tasks.create({
description: "Add Authentication System",
context: `# Add Authentication System\n\n## Implementation\n1. Create database schema...`,
priority: 3
});
const subtasks = [
{ desc: "Create database schema for users/tokens", done: "Migration runs, tables exist with FK constraints" },
{ desc: "Implement auth controller with endpoints", done: "POST /register, /login return expected responses" },
{ desc: "Add JWT middleware for route protection", done: "Unauthorized requests return 401, valid tokens pass" },
{ desc: "Build frontend login/register forms", done: "Forms render, submit without errors" },
{ desc: "Add integration tests", done: "`npm test` passes with auth coverage" }
];
for (const sub of subtasks) {
await tasks.create({
description: sub.desc,
context: `Part of 'Add Authentication System'.\n\nDone when: ${sub.done}`,
parentId: milestone.id
});
}
return { milestone: milestone.id, subtaskCount: subtasks.length };
```
### Output
```
Created milestone task_01ABC from plan
Analyzed plan structure: Found 5 distinct implementation steps
Created 5 subtasks:
- task_02XYZ: Create database schema for users/tokens
- task_03ABC: Implement auth controller with endpoints
- task_04DEF: Add JWT middleware for route protection
- task_05GHI: Build frontend login/register forms
- task_06JKL: Add integration tests
View structure: execute `await tasks.list({ parentId: "task_01ABC" })`
```
## Example 2: No Breakdown
### Input (`bugfix-plan.md`)
```markdown
# Plan: Fix Login Validation Bug
## Problem
Login fails when username has spaces
## Solution
Update validation regex in auth.ts line 42
```
### Execution
```javascript
const milestone = await tasks.create({
description: "Fix Login Validation Bug",
context: `# Fix Login Validation Bug\n\n## Problem\nLogin fails...`,
priority: 3
});
return { milestone: milestone.id, breakdown: false };
```
### Output
```
Created milestone task_01ABC from plan
Plan describes a cohesive single task. No subtask breakdown needed.
View task: execute `await tasks.get("task_01ABC")`
```
## Example 3: Epic-Level (Two-Level Hierarchy)
### Input (`full-auth-plan.md`)
```markdown
# Complete User Authentication System
## Phase 1: Backend Infrastructure
1. Database schema for users/sessions
2. Password hashing with bcrypt
3. JWT token generation
## Phase 2: API Endpoints
1. POST /auth/register
2. POST /auth/login
3. POST /auth/logout
## Phase 3: Frontend
1. Login/register forms
2. Protected routes
3. Session persistence
```
### Execution
```javascript
const milestone = await tasks.create({
description: "Complete User Authentication System",
context: `<full-markdown>`,
priority: 3
});
const phases = [
{ name: "Backend Infrastructure", items: [
{ desc: "Database schema", done: "Migration runs, tables exist" },
{ desc: "Password hashing", done: "bcrypt hashes verified in tests" },
{ desc: "JWT tokens", done: "Token generation/validation works" }
]},
{ name: "API Endpoints", items: [
{ desc: "POST /auth/register", done: "Creates user, returns 201" },
{ desc: "POST /auth/login", done: "Returns JWT on valid credentials" },
{ desc: "POST /auth/logout", done: "Invalidates session, returns 200" }
]},
{ name: "Frontend", items: [
{ desc: "Login/register forms", done: "Forms render, submit successfully" },
{ desc: "Protected routes", done: "Redirect to login when unauthenticated" },
{ desc: "Session persistence", done: "Refresh maintains logged-in state" }
]}
];
for (const phase of phases) {
const phaseTask = await tasks.create({
description: phase.name,
parentId: milestone.id
});
for (const item of phase.items) {
await tasks.create({
description: item.desc,
context: `Part of '${phase.name}'.\n\nDone when: ${item.done}`,
parentId: phaseTask.id
});
}
}
return milestone;
```
### Output
```
Created milestone task_01ABC from plan
Analyzed plan structure: Found 3 major phases
Created as milestone with 3 tasks:
- task_02XYZ: Backend Infrastructure (3 subtasks)
- task_03ABC: API Endpoints (3 subtasks)
- task_04DEF: Frontend (3 subtasks)
View structure: execute `await tasks.list({ parentId: "task_01ABC" })`
```

View File

@@ -1,210 +0,0 @@
# Implementation Instructions
**For the skill agent executing `/overseer-plan`.** Follow this workflow exactly.
## Step 1: Read Markdown File
Read the provided file using the Read tool.
## Step 2: Extract Title
- Parse first `#` heading as title
- Strip "Plan: " prefix if present (case-insensitive)
- Fallback: use filename without extension
## Step 3: Create Milestone via MCP
Basic creation:
```javascript
const milestone = await tasks.create({
description: "<extracted-title>",
context: `<full-markdown-content>`,
priority: <priority-if-provided-else-3>
});
return milestone;
```
With `--parent` option:
```javascript
const task = await tasks.create({
description: "<extracted-title>",
context: `<full-markdown-content>`,
parentId: "<parent-id>",
priority: <priority-if-provided-else-3>
});
return task;
```
Capture returned task ID for subsequent steps.
## Step 4: Analyze Plan Structure
### Breakdown Indicators
1. **Numbered/bulleted implementation lists (3-7 items)**
```markdown
## Implementation
1. Create database schema
2. Build API endpoints
3. Add frontend components
```
2. **Clear subsections under implementation/tasks/steps**
```markdown
### 1. Backend Changes
- Modify server.ts
### 2. Frontend Updates
- Update login form
```
3. **File-specific sections**
```markdown
### `src/auth.ts` - Add JWT validation
### `src/middleware.ts` - Create auth middleware
```
4. **Sequential phases**
```markdown
**Phase 1: Database Layer**
**Phase 2: API Layer**
```
### Do NOT Break Down When
- Only 1-2 steps/items
- Plan is a single cohesive fix
- Content is exploratory ("investigate", "research")
- Work items inseparable
- Plan very short (<10 lines)
## Step 5: Validate Atomicity & Acceptance Criteria
For each proposed task, verify:
- **Atomic**: Can be completed in single commit
- **Validated**: Has clear acceptance criteria
If task too large -> split further.
If no validation -> add to context:
```
Done when: <specific observable criteria>
```
Examples of good acceptance criteria:
- "Done when: `npm test` passes, new migration applied"
- "Done when: API returns 200 with expected payload"
- "Done when: Component renders without console errors"
- "Done when: Type check passes (`tsc --noEmit`)"
## Step 6: Oracle Review
Before creating tasks, invoke Oracle to review the proposed breakdown.
**Prompt Oracle with:**
```
Review this task breakdown for "<milestone>":
1. <task> - Done when: <criteria>
2. <task> - Done when: <criteria>
...
Check:
- Are tasks truly atomic (single commit)?
- Is validation criteria clear and observable?
- Does milestone deliver demoable increment?
- Missing dependencies/blockers?
- Any tasks that should be split or merged?
```
Incorporate Oracle's feedback, then proceed to create tasks.
## Step 7: Create Subtasks (If Breaking Down)
### Extract for Each Subtask
1. **Description**: Strip numbering, keep concise (1-10 words), imperative form
2. **Context**: Section content + "Part of [milestone description]" + acceptance criteria
### Flat Breakdown
```javascript
const subtasks = [
{ description: "Create database schema", context: "Schema for users/tokens. Part of 'Add Auth'.\n\nDone when: Migration runs, tables exist with FK constraints." },
{ description: "Build API endpoints", context: "POST /auth/register, /auth/login. Part of 'Add Auth'.\n\nDone when: Endpoints return expected responses, tests pass." }
];
const created = [];
for (const sub of subtasks) {
const task = await tasks.create({
description: sub.description,
context: sub.context,
parentId: milestone.id
});
created.push(task);
}
return { milestone: milestone.id, subtasks: created };
```
### Epic-Level Breakdown (phases with sub-items)
```javascript
// Create phase as task under milestone
const phase = await tasks.create({
description: "Backend Infrastructure",
context: "Phase 1 context...",
parentId: milestoneId
});
// Create subtasks under phase
for (const item of phaseItems) {
await tasks.create({
description: item.description,
context: item.context,
parentId: phase.id
});
}
```
## Step 8: Report Results
### Subtasks Created
```
Created milestone <id> from plan
Analyzed plan structure: Found <N> distinct implementation steps
Created <N> subtasks:
- <id>: <description>
- <id>: <description>
...
View structure: execute `await tasks.list({ parentId: "<id>" })`
```
### No Breakdown
```
Created milestone <id> from plan
Plan describes a cohesive single task. No subtask breakdown needed.
View task: execute `await tasks.get("<id>")`
```
### Epic-Level Breakdown
```
Created milestone <id> from plan
Analyzed plan structure: Found <N> major phases
Created as milestone with <N> tasks:
- <id>: <phase-name> (<M> subtasks)
- <id>: <phase-name> (<M> subtasks)
...
View structure: execute `await tasks.list({ parentId: "<id>" })`
```

View File

@@ -1,191 +0,0 @@
---
name: overseer
description: Manage tasks via Overseer codemode MCP. Use when tracking multi-session work, breaking down implementation, or persisting context for handoffs.
license: MIT
metadata:
author: dmmulroy
version: "1.0.0"
---
# Agent Coordination with Overseer
## Core Principle: Tickets, Not Todos
Overseer tasks are **tickets** - structured artifacts with comprehensive context:
- **Description**: One-line summary (issue title)
- **Context**: Full background, requirements, approach (issue body)
- **Result**: Implementation details, decisions, outcomes (PR description)
Think: "Would someone understand the what, why, and how from this task alone AND what success looks like?"
## Task IDs are Ephemeral
**Never reference task IDs in external artifacts** (commits, PRs, docs). Task IDs like `task_01JQAZ...` become meaningless once tasks complete. Describe the work itself, not the task that tracked it.
## Overseer vs OpenCode's TodoWrite
| | Overseer | TodoWrite |
| --------------- | ------------------------------------- | ---------------------- |
| **Persistence** | SQLite database | Session-only |
| **Context** | Rich (description + context + result) | Basic |
| **Hierarchy** | 3-level (milestone -> task -> subtask)| Flat |
Use **Overseer** for persistent work. Use **TodoWrite** for ephemeral in-session tracking only.
## When to Use Overseer
**Use Overseer when:**
- Breaking down complexity into subtasks
- Work spans multiple sessions
- Context needs to persist for handoffs
- Recording decisions for future reference
**Skip Overseer when:**
- Work is a single atomic action
- Everything fits in one message exchange
- Overhead exceeds value
- TodoWrite is sufficient
## Finding Work
```javascript
// Get next ready task with full context (recommended for work sessions)
const task = await tasks.nextReady(milestoneId); // TaskWithContext | null
if (!task) {
console.log("No ready tasks");
return;
}
// Get all ready tasks (for progress overview)
const readyTasks = await tasks.list({ ready: true }); // Task[]
```
**Use `nextReady()`** when starting work - returns `TaskWithContext | null` (deepest ready leaf with full context chain + inherited learnings).
**Use `list({ ready: true })`** for status/progress checks - returns `Task[]` without context chain.
## Basic Workflow
```javascript
// 1. Get next ready task (returns TaskWithContext | null)
const task = await tasks.nextReady();
if (!task) return "No ready tasks";
// 2. Review context (available on TaskWithContext)
console.log(task.context.own); // This task's context
console.log(task.context.parent); // Parent's context (if depth > 0)
console.log(task.context.milestone); // Root milestone context (if depth > 1)
console.log(task.learnings.own); // Learnings attached to this task (bubbled from children)
// 3. Start work (VCS required - creates bookmark, records start commit)
await tasks.start(task.id);
// 4. Implement...
// 5. Complete with learnings (VCS required - commits changes, bubbles learnings to parent)
await tasks.complete(task.id, {
result: "Implemented login endpoint with JWT tokens",
learnings: ["bcrypt rounds should be 12 for production"]
});
```
See @file references/workflow.md for detailed workflow guidance.
## Understanding Task Context
Tasks have **progressive context** - inherited from ancestors:
```javascript
const task = await tasks.get(taskId); // Returns TaskWithContext
// task.context.own - this task's context (always present)
// task.context.parent - parent task's context (if depth > 0)
// task.context.milestone - root milestone's context (if depth > 1)
// Task's own learnings (bubbled from completed children)
// task.learnings.own - learnings attached to this task
```
## Return Type Summary
| Method | Returns | Notes |
|--------|---------|-------|
| `tasks.get(id)` | `TaskWithContext` | Full context chain + inherited learnings |
| `tasks.nextReady()` | `TaskWithContext \| null` | Deepest ready leaf with full context |
| `tasks.list()` | `Task[]` | Basic task fields only |
| `tasks.create()` | `Task` | No context chain |
| `tasks.start/complete()` | `Task` | No context chain |
## Blockers
Blockers prevent a task from being ready until the blocker completes.
**Constraints:**
- Blockers cannot be self
- Blockers cannot be ancestors (parent, grandparent, etc.)
- Blockers cannot be descendants
- Creating/reparenting with invalid blockers is rejected
```javascript
// Add blocker - taskA waits for taskB
await tasks.block(taskA.id, taskB.id);
// Remove blocker
await tasks.unblock(taskA.id, taskB.id);
```
## Task Hierarchies
Three levels: **Milestone** (depth 0) -> **Task** (depth 1) -> **Subtask** (depth 2).
| Level | Name | Purpose | Example |
|-------|------|---------|---------|
| 0 | **Milestone** | Large initiative | "Add user authentication system" |
| 1 | **Task** | Significant work item | "Implement JWT middleware" |
| 2 | **Subtask** | Atomic step | "Add token verification function" |
**Choosing the right level:**
- Small feature (1-2 files) -> Single task
- Medium feature (3-7 steps) -> Task with subtasks
- Large initiative (5+ tasks) -> Milestone with tasks
See @file references/hierarchies.md for detailed guidance.
## Recording Results
Complete tasks **immediately after implementing AND verifying**:
- Capture decisions while fresh
- Note deviations from plan
- Document verification performed
- Create follow-up tasks for tech debt
Your result must include explicit verification evidence. See @file references/verification.md.
## Best Practices
1. **Right-size tasks**: Completable in one focused session
2. **Clear completion criteria**: Context should define "done"
3. **Don't over-decompose**: 3-7 children per parent
4. **Action-oriented descriptions**: Start with verbs ("Add", "Fix", "Update")
5. **Verify before completing**: Tests passing, manual testing done
---
## Reading Order
| Task | File |
|------|------|
| Understanding API | @file references/api.md |
| Implementation workflow | @file references/workflow.md |
| Task decomposition | @file references/hierarchies.md |
| Good/bad examples | @file references/examples.md |
| Verification checklist | @file references/verification.md |
## In This Reference
| File | Purpose |
|------|---------|
| `references/api.md` | Overseer MCP codemode API types/methods |
| `references/workflow.md` | Start->implement->complete workflow |
| `references/hierarchies.md` | Milestone/task/subtask organization |
| `references/examples.md` | Good/bad context and result examples |
| `references/verification.md` | Verification checklist and process |

View File

@@ -1,192 +0,0 @@
# Overseer Codemode MCP API
Execute JavaScript code to interact with Overseer task management.
## Task Interface
```typescript
// Basic task - returned by list(), create(), start(), complete()
// Note: Does NOT include context or learnings fields
interface Task {
id: string;
parentId: string | null;
description: string;
priority: 1 | 2 | 3 | 4 | 5;
completed: boolean;
completedAt: string | null;
startedAt: string | null;
createdAt: string; // ISO 8601
updatedAt: string;
result: string | null; // Completion notes
commitSha: string | null; // Auto-populated on complete
depth: 0 | 1 | 2; // 0=milestone, 1=task, 2=subtask
blockedBy?: string[]; // Blocking task IDs (omitted if empty)
blocks?: string[]; // Tasks this blocks (omitted if empty)
bookmark?: string; // VCS bookmark name (if started)
startCommit?: string; // Commit SHA at start
effectivelyBlocked: boolean; // True if task OR ancestor has incomplete blockers
}
// Task with full context - returned by get(), nextReady()
interface TaskWithContext extends Task {
context: {
own: string; // This task's context
parent?: string; // Parent's context (depth > 0)
milestone?: string; // Root milestone's context (depth > 1)
};
learnings: {
own: Learning[]; // This task's learnings (bubbled from completed children)
parent: Learning[]; // Parent's learnings (depth > 0)
milestone: Learning[]; // Milestone's learnings (depth > 1)
};
}
// Task tree structure - returned by tree()
interface TaskTree {
task: Task;
children: TaskTree[];
}
// Progress summary - returned by progress()
interface TaskProgress {
total: number;
completed: number;
ready: number; // !completed && !effectivelyBlocked
blocked: number; // !completed && effectivelyBlocked
}
// Task type alias for depth filter
type TaskType = "milestone" | "task" | "subtask";
```
## Learning Interface
```typescript
interface Learning {
id: string;
taskId: string;
content: string;
sourceTaskId: string | null;
createdAt: string;
}
```
## Tasks API
```typescript
declare const tasks: {
list(filter?: {
parentId?: string;
ready?: boolean;
completed?: boolean;
depth?: 0 | 1 | 2; // 0=milestones, 1=tasks, 2=subtasks
type?: TaskType; // Alias: "milestone"|"task"|"subtask" (mutually exclusive with depth)
}): Promise<Task[]>;
get(id: string): Promise<TaskWithContext>;
create(input: {
description: string;
context?: string;
parentId?: string;
priority?: 1 | 2 | 3 | 4 | 5; // Required range: 1-5
blockedBy?: string[];
}): Promise<Task>;
update(id: string, input: {
description?: string;
context?: string;
priority?: 1 | 2 | 3 | 4 | 5;
parentId?: string;
}): Promise<Task>;
start(id: string): Promise<Task>;
complete(id: string, input?: { result?: string; learnings?: string[] }): Promise<Task>;
reopen(id: string): Promise<Task>;
delete(id: string): Promise<void>;
block(taskId: string, blockerId: string): Promise<void>;
unblock(taskId: string, blockerId: string): Promise<void>;
nextReady(milestoneId?: string): Promise<TaskWithContext | null>;
tree(rootId?: string): Promise<TaskTree | TaskTree[]>;
search(query: string): Promise<Task[]>;
progress(rootId?: string): Promise<TaskProgress>;
};
```
| Method | Returns | Description |
|--------|---------|-------------|
| `list` | `Task[]` | Filter by `parentId`, `ready`, `completed`, `depth`, `type` |
| `get` | `TaskWithContext` | Get task with full context chain + inherited learnings |
| `create` | `Task` | Create task (priority must be 1-5) |
| `update` | `Task` | Update description, context, priority, parentId |
| `start` | `Task` | **VCS required** - creates bookmark, records start commit |
| `complete` | `Task` | **VCS required** - commits changes + bubbles learnings to parent |
| `reopen` | `Task` | Reopen completed task |
| `delete` | `void` | Delete task + best-effort VCS bookmark cleanup |
| `block` | `void` | Add blocker (cannot be self, ancestor, or descendant) |
| `unblock` | `void` | Remove blocker relationship |
| `nextReady` | `TaskWithContext \| null` | Get deepest ready leaf with full context |
| `tree` | `TaskTree \| TaskTree[]` | Get task tree (all milestones if no ID) |
| `search` | `Task[]` | Search by description/context/result (case-insensitive) |
| `progress` | `TaskProgress` | Aggregate counts for milestone or all tasks |
## Learnings API
Learnings are added via `tasks.complete(id, { learnings: [...] })` and bubble to immediate parent (preserving `sourceTaskId`).
```typescript
declare const learnings: {
list(taskId: string): Promise<Learning[]>;
};
```
| Method | Description |
|--------|-------------|
| `list` | List learnings for task |
## VCS Integration (Required for Workflow)
VCS operations are **automatically handled** by the tasks API:
| Task Operation | VCS Effect |
|----------------|------------|
| `tasks.start(id)` | **VCS required** - creates bookmark `task/<id>`, records start commit |
| `tasks.complete(id)` | **VCS required** - commits changes (NothingToCommit = success) |
| `tasks.delete(id)` | Best-effort bookmark cleanup (logs warning on failure) |
**VCS (jj or git) is required** for start/complete. Fails with `NotARepository` if none found. CRUD operations work without VCS.
## Quick Examples
```javascript
// Create milestone with subtask
const milestone = await tasks.create({
description: "Build authentication system",
context: "JWT-based auth with refresh tokens",
priority: 1
});
const subtask = await tasks.create({
description: "Implement token refresh logic",
parentId: milestone.id,
context: "Handle 7-day expiry"
});
// Start work (auto-creates VCS bookmark)
await tasks.start(subtask.id);
// ... do implementation work ...
// Complete task with learnings (VCS required - commits changes, bubbles learnings to parent)
await tasks.complete(subtask.id, {
result: "Implemented using jose library",
learnings: ["Use jose instead of jsonwebtoken"]
});
// Get progress summary
const progress = await tasks.progress(milestone.id);
// -> { total: 2, completed: 1, ready: 1, blocked: 0 }
// Search tasks
const authTasks = await tasks.search("authentication");
// Get task tree
const tree = await tasks.tree(milestone.id);
// -> { task: Task, children: TaskTree[] }
```

View File

@@ -1,195 +0,0 @@
# Examples
Good and bad examples for writing task context and results.
## Writing Context
Context should include everything needed to do the work without asking questions:
- **What** needs to be done and why
- **Implementation approach** (steps, files to modify, technical choices)
- **Done when** (acceptance criteria)
### Good Context Example
```javascript
await tasks.create({
description: "Migrate storage to one file per task",
context: `Change storage format for git-friendliness:
Structure:
.overseer/
└── tasks/
├── task_01ABC.json
└── task_02DEF.json
NO INDEX - just scan task files. For typical task counts (<100), this is fast.
Implementation:
1. Update storage.ts:
- read(): Scan .overseer/tasks/*.json, parse each, return TaskStore
- write(task): Write single task to .overseer/tasks/{id}.json
- delete(id): Remove .overseer/tasks/{id}.json
- Add readTask(id) for single task lookup
2. Task file format: Same as current Task schema (one task per file)
3. Migration: On read, if old tasks.json exists, migrate to new format
4. Update tests
Benefits:
- Create = new file (never conflicts)
- Update = single file change
- Delete = remove file
- No index to maintain or conflict
- git diff shows exactly which tasks changed`
});
```
**Why it works:** States the goal, shows the structure, lists specific implementation steps, explains benefits. Someone could pick this up without asking questions.
### Bad Context Example
```javascript
await tasks.create({
description: "Add auth",
context: "Need to add authentication"
});
```
**What's missing:** How to implement it, what files, what's done when, technical approach.
## Writing Results
Results should capture what was actually done:
- **What changed** (implementation summary)
- **Key decisions** (and why)
- **Verification** (tests passing, manual testing done)
### Good Result Example
```javascript
await tasks.complete(taskId, `Migrated storage from single tasks.json to one file per task:
Structure:
- Each task stored as .overseer/tasks/{id}.json
- No index file (avoids merge conflicts)
- Directory scanned on read to build task list
Implementation:
- Modified Storage.read() to scan .overseer/tasks/ directory
- Modified Storage.write() to write/delete individual task files
- Auto-migration from old single-file format on first read
- Atomic writes using temp file + rename pattern
Trade-offs:
- Slightly slower reads (must scan directory + parse each file)
- Acceptable since task count is typically small (<100)
- Better git history - each task change is isolated
Verification:
- All 60 tests passing
- Build successful
- Manually tested migration: old -> new format works`);
```
**Why it works:** States what changed, lists implementation details, explains trade-offs, confirms verification.
### Bad Result Example
```javascript
await tasks.complete(taskId, "Fixed the storage issue");
```
**What's missing:** What was actually implemented, how, what decisions were made, verification evidence.
## Subtask Context Example
Link subtasks to their parent and explain what this piece does specifically:
```javascript
await tasks.create({
description: "Add token verification function",
parentId: jwtTaskId,
context: `Part of JWT middleware (parent task). This subtask: token verification.
What it does:
- Verify JWT signature and expiration on protected routes
- Extract user ID from token payload
- Attach user object to request
- Return 401 for invalid/expired tokens
Implementation:
- Create src/middleware/verify-token.ts
- Export verifyToken middleware function
- Use jose library (preferred over jsonwebtoken)
- Handle expired vs invalid token cases separately
Done when:
- Middleware function complete and working
- Unit tests cover valid/invalid/expired scenarios
- Integrated into auth routes in server.ts
- Parent task can use this to protect endpoints`
});
```
## Error Handling Examples
### Handling Pending Children
```javascript
try {
await tasks.complete(taskId, "Done");
} catch (err) {
if (err.message.includes("pending children")) {
const pending = await tasks.list({ parentId: taskId, completed: false });
console.log(`Cannot complete: ${pending.length} children pending`);
for (const child of pending) {
console.log(`- ${child.id}: ${child.description}`);
}
return;
}
throw err;
}
```
### Handling Blocked Tasks
```javascript
const task = await tasks.get(taskId);
if (task.blockedBy.length > 0) {
console.log("Task is blocked by:");
for (const blockerId of task.blockedBy) {
const blocker = await tasks.get(blockerId);
console.log(`- ${blocker.description} (${blocker.completed ? 'done' : 'pending'})`);
}
return "Cannot start - blocked by other tasks";
}
await tasks.start(taskId);
```
## Creating Task Hierarchies
```javascript
// Create milestone with tasks
const milestone = await tasks.create({
description: "Implement user authentication",
context: "Full auth: JWT, login/logout, password reset, rate limiting",
priority: 2
});
const subtasks = [
"Add login endpoint",
"Add logout endpoint",
"Implement JWT token service",
"Add password reset flow"
];
for (const desc of subtasks) {
await tasks.create({ description: desc, parentId: milestone.id });
}
```
See @file references/hierarchies.md for sequential subtasks with blockers.

View File

@@ -1,170 +0,0 @@
# Task Hierarchies
Guidance for organizing work into milestones, tasks, and subtasks.
## Three Levels
| Level | Name | Purpose | Example |
|-------|------|---------|---------|
| 0 | **Milestone** | Large initiative (5+ tasks) | "Add user authentication system" |
| 1 | **Task** | Significant work item | "Implement JWT middleware" |
| 2 | **Subtask** | Atomic implementation step | "Add token verification function" |
**Maximum depth is 3 levels.** Attempting to create a child of a subtask will fail.
## When to Use Each Level
### Single Task (No Hierarchy)
- Small feature (1-2 files, ~1 session)
- Work is atomic, no natural breakdown
### Task with Subtasks
- Medium feature (3-5 files, 3-7 steps)
- Work naturally decomposes into discrete steps
- Subtasks could be worked on independently
### Milestone with Tasks
- Large initiative (multiple areas, many sessions)
- Work spans 5+ distinct tasks
- You want high-level progress tracking
## Creating Hierarchies
```javascript
// Create the milestone
const milestone = await tasks.create({
description: "Add user authentication system",
context: "Full auth system with JWT tokens, password reset...",
priority: 2
});
// Create tasks under it
const jwtTask = await tasks.create({
description: "Implement JWT token generation",
context: "Create token service with signing and verification...",
parentId: milestone.id
});
const resetTask = await tasks.create({
description: "Add password reset flow",
context: "Email-based password reset with secure tokens...",
parentId: milestone.id
});
// For complex tasks, add subtasks
const verifySubtask = await tasks.create({
description: "Add token verification function",
context: "Verify JWT signature and expiration...",
parentId: jwtTask.id
});
```
## Subtask Best Practices
Each subtask should be:
- **Independently understandable**: Clear on its own
- **Linked to parent**: Reference parent, explain how this piece fits
- **Specific scope**: What this subtask does vs what parent/siblings do
- **Clear completion**: Define "done" for this piece specifically
Example subtask context:
```
Part of JWT middleware (parent task). This subtask: token verification.
What it does:
- Verify JWT signature and expiration
- Extract user ID from payload
- Return 401 for invalid/expired tokens
Done when:
- Function complete and tested
- Unit tests cover valid/invalid/expired cases
```
## Decomposition Strategy
When faced with large tasks:
1. **Assess scope**: Is this milestone-level (5+ tasks) or task-level (3-7 subtasks)?
2. Create parent task/milestone with overall goal and context
3. Analyze and identify 3-7 logical children
4. Create children with specific contexts and boundaries
5. Work through systematically, completing with results
6. Complete parent with summary of overall implementation
### Don't Over-Decompose
- **3-7 children per parent** is usually right
- If you'd only have 1-2 subtasks, just make separate tasks
- If you need depth 3+, restructure your breakdown
## Viewing Hierarchies
```javascript
// List all tasks under a milestone
const children = await tasks.list({ parentId: milestoneId });
// Get task with context breadcrumb
const task = await tasks.get(taskId);
// task.context.parent - parent's context
// task.context.milestone - root milestone's context
// Check progress
const pending = await tasks.list({ parentId: milestoneId, completed: false });
const done = await tasks.list({ parentId: milestoneId, completed: true });
console.log(`Progress: ${done.length}/${done.length + pending.length}`);
```
## Completion Rules
1. **Cannot complete with pending children**
```javascript
// This will fail if task has incomplete subtasks
await tasks.complete(taskId, "Done");
// Error: "pending children"
```
2. **Complete children first**
- Work through subtasks systematically
- Complete each with meaningful results
3. **Parent result summarizes overall implementation**
```javascript
await tasks.complete(milestoneId, `User authentication system complete:
Implemented:
- JWT token generation and verification
- Login/logout endpoints
- Password reset flow
- Rate limiting
5 tasks completed, all tests passing.`);
```
## Blocking Dependencies
Use `blockedBy` for cross-task dependencies:
```javascript
// Create task that depends on another
const deployTask = await tasks.create({
description: "Deploy to production",
context: "...",
blockedBy: [testTaskId, reviewTaskId]
});
// Add blocker to existing task
await tasks.block(deployTaskId, testTaskId);
// Remove blocker
await tasks.unblock(deployTaskId, testTaskId);
```
**Use blockers when:**
- Task B cannot start until Task A completes
- Multiple tasks depend on a shared prerequisite
**Don't use blockers when:**
- Tasks can be worked on in parallel
- The dependency is just logical grouping (use subtasks instead)

View File

@@ -1,186 +0,0 @@
# Verification Guide
Before marking any task complete, you MUST verify your work. Verification separates "I think it's done" from "it's actually done."
## The Verification Process
1. **Re-read the task context**: What did you originally commit to do?
2. **Check acceptance criteria**: Does your implementation satisfy the "Done when" conditions?
3. **Run relevant tests**: Execute the test suite and document results
4. **Test manually**: Actually try the feature/change yourself
5. **Compare with requirements**: Does what you built match what was asked?
## Strong vs Weak Verification
### Strong Verification Examples
- "All 60 tests passing, build successful"
- "All 69 tests passing (4 new tests for middleware edge cases)"
- "Manually tested with valid/invalid/expired tokens - all cases work"
- "Ran `cargo test` - 142 tests passed, 0 failed"
### Weak Verification (Avoid)
- "Should work now" - "should" means not verified
- "Made the changes" - no evidence it works
- "Added tests" - did the tests pass? What's the count?
- "Fixed the bug" - what bug? Did you verify the fix?
- "Done" - done how? prove it
## Verification by Task Type
| Task Type | How to Verify |
|-----------|---------------|
| Code changes | Run full test suite, document passing count |
| New features | Run tests + manual testing of functionality |
| Configuration | Test the config works (run commands, check workflows) |
| Documentation | Verify examples work, links resolve, formatting renders |
| Refactoring | Confirm tests still pass, no behavior changes |
| Bug fixes | Reproduce bug first, verify fix, add regression test |
## Cross-Reference Checklist
Before marking complete, verify all applicable items:
- [ ] Task description requirements met
- [ ] Context "Done when" criteria satisfied
- [ ] Tests passing (document count: "All X tests passing")
- [ ] Build succeeds (if applicable)
- [ ] Manual testing done (describe what you tested)
- [ ] No regressions introduced
- [ ] Edge cases considered (error handling, invalid input)
- [ ] Follow-up work identified (created new tasks if needed)
**If you can't check all applicable boxes, the task isn't done yet.**
## Result Examples with Verification
### Code Implementation
```javascript
await tasks.complete(taskId, `Implemented JWT middleware:
Implementation:
- Created src/middleware/verify-token.ts
- Separated 'expired' vs 'invalid' error codes
- Added user extraction from payload
Verification:
- All 69 tests passing (4 new tests for edge cases)
- Manually tested with valid token: Access granted
- Manually tested with expired token: 401 with 'token_expired'
- Manually tested with invalid signature: 401 with 'invalid_token'`);
```
### Configuration/Infrastructure
```javascript
await tasks.complete(taskId, `Added GitHub Actions workflow for CI:
Implementation:
- Created .github/workflows/ci.yml
- Jobs: lint, test, build with pnpm cache
Verification:
- Pushed to test branch, opened PR #123
- Workflow triggered automatically
- All jobs passed (lint: 0 errors, test: 69/69, build: success)
- Total run time: 2m 34s`);
```
### Refactoring
```javascript
await tasks.complete(taskId, `Refactored storage to one file per task:
Implementation:
- Split tasks.json into .overseer/tasks/{id}.json files
- Added auto-migration from old format
- Atomic writes via temp+rename
Verification:
- All 60 tests passing (including 8 storage tests)
- Build successful
- Manually tested migration: old -> new format works
- Confirmed git diff shows only changed tasks`);
```
### Bug Fix
```javascript
await tasks.complete(taskId, `Fixed login validation accepting usernames with spaces:
Root cause:
- Validation regex didn't account for leading/trailing spaces
Fix:
- Added .trim() before validation in src/auth/validate.ts:42
- Updated regex to reject internal spaces
Verification:
- All 45 tests passing (2 new regression tests)
- Manually tested:
- " admin" -> rejected (leading space)
- "admin " -> rejected (trailing space)
- "ad min" -> rejected (internal space)
- "admin" -> accepted`);
```
### Documentation
```javascript
await tasks.complete(taskId, `Updated API documentation for auth endpoints:
Implementation:
- Added docs for POST /auth/login
- Added docs for POST /auth/logout
- Added docs for POST /auth/refresh
- Included example requests/responses
Verification:
- All code examples tested and working
- Links verified (no 404s)
- Rendered in local preview - formatting correct
- Spell-checked content`);
```
## Common Verification Mistakes
| Mistake | Better Approach |
|---------|-----------------|
| "Tests pass" | "All 42 tests passing" (include count) |
| "Manually tested" | "Manually tested X, Y, Z scenarios" (be specific) |
| "Works" | "Works: [evidence]" (show proof) |
| "Fixed" | "Fixed: [root cause] -> [solution] -> [verification]" |
## When Verification Fails
If verification reveals issues:
1. **Don't complete the task** - it's not done
2. **Document what failed** in task context
3. **Fix the issues** before completing
4. **Re-verify** after fixes
```javascript
// Update context with failure notes
await tasks.update(taskId, {
context: task.context + `
Verification attempt 1 (failed):
- Tests: 41/42 passing
- Failing: test_token_refresh - timeout issue
- Need to investigate async handling`
});
// After fixing
await tasks.complete(taskId, `Implemented token refresh:
Implementation:
- Added refresh endpoint
- Fixed async timeout (was missing await)
Verification:
- All 42 tests passing (fixed timeout issue)
- Manual testing: refresh works within 30s window`);
```

View File

@@ -1,164 +0,0 @@
# Implementation Workflow
Step-by-step guide for working with Overseer tasks during implementation.
## 1. Get Next Ready Task
```javascript
// Get next task with full context (recommended)
const task = await tasks.nextReady();
// Or scope to specific milestone
const task = await tasks.nextReady(milestoneId);
if (!task) {
return "No tasks ready - all blocked or completed";
}
```
`nextReady()` returns a `TaskWithContext` (task with inherited context and learnings) or `null`.
## 2. Review Context
Before starting, verify you can answer:
- **What** needs to be done specifically?
- **Why** is this needed?
- **How** should it be implemented?
- **When** is it done (acceptance criteria)?
```javascript
const task = await tasks.get(taskId);
// Task's own context
console.log("Task:", task.context.own);
// Parent context (if task has parent)
if (task.context.parent) {
console.log("Parent:", task.context.parent);
}
// Milestone context (if depth > 1)
if (task.context.milestone) {
console.log("Milestone:", task.context.milestone);
}
// Task's own learnings (bubbled from completed children)
console.log("Task learnings:", task.learnings.own);
```
**If any answer is unclear:**
1. Check parent task or completed blockers for details
2. Suggest entering plan mode to flesh out requirements
**Proceed without full context when:**
- Task is trivial/atomic (e.g., "Add .gitignore entry")
- Conversation already provides the missing context
- Description itself is sufficiently detailed
## 3. Start Task
```javascript
await tasks.start(taskId);
```
**VCS Required:** Creates bookmark `task/<id>`, records start commit. Fails with `NotARepository` if no jj/git found.
After starting, the task status changes to `in_progress`.
## 4. Implement
Work on the task implementation. Note any learnings to include when completing.
## 5. Verify Work
Before completing, verify your implementation. See @file references/verification.md for full checklist.
Quick checklist:
- [ ] Task description requirements met
- [ ] Context "Done when" criteria satisfied
- [ ] Tests passing (document count)
- [ ] Build succeeds
- [ ] Manual testing done
## 6. Complete Task with Learnings
```javascript
await tasks.complete(taskId, {
result: `Implemented login endpoint:
Implementation:
- Created src/auth/login.ts
- Added JWT token generation
- Integrated with user service
Verification:
- All 42 tests passing (3 new)
- Manually tested valid/invalid credentials`,
learnings: [
"bcrypt rounds should be 12+ for production",
"jose library preferred over jsonwebtoken"
]
});
```
**VCS Required:** Commits changes (NothingToCommit treated as success), then deletes the task's bookmark (best-effort) and clears the DB bookmark field on success. Fails with `NotARepository` if no jj/git found.
**Learnings Effect:** Learnings bubble to immediate parent only. `sourceTaskId` is preserved through bubbling, so if this task's learnings later bubble further, the origin is tracked.
The `result` becomes part of the task's permanent record.
## VCS Integration (Required for Workflow)
VCS operations are **automatically handled** by the tasks API:
| Task Operation | VCS Effect |
|----------------|------------|
| `tasks.start(id)` | **VCS required** - creates bookmark `task/<id>`, records start commit |
| `tasks.complete(id)` | **VCS required** - commits changes, deletes bookmark (best-effort), clears DB bookmark on success |
| `tasks.complete(milestoneId)` | Same + deletes ALL descendant bookmarks recursively (depth-1 and depth-2) |
| `tasks.delete(id)` | Best-effort bookmark cleanup (logs warning on failure) |
**Note:** VCS (jj or git) is required for start/complete. CRUD operations work without VCS.
## Error Handling
### Pending Children
```javascript
try {
await tasks.complete(taskId, "Done");
} catch (err) {
if (err.message.includes("pending children")) {
const pending = await tasks.list({ parentId: taskId, completed: false });
return `Cannot complete: ${pending.length} children pending`;
}
throw err;
}
```
### Task Not Ready
```javascript
const task = await tasks.get(taskId);
// Check if blocked
if (task.blockedBy.length > 0) {
console.log("Blocked by:", task.blockedBy);
// Complete blockers first or unblock
await tasks.unblock(taskId, blockerId);
}
```
## Complete Workflow Example
```javascript
const task = await tasks.nextReady();
if (!task) return "No ready tasks";
await tasks.start(task.id);
// ... implement ...
await tasks.complete(task.id, {
result: "Implemented: ... Verification: All 58 tests passing",
learnings: ["Use jose for JWT"]
});
```

View File

@@ -1,464 +0,0 @@
---
name: solidjs
description: |
SolidJS framework development skill for building reactive web applications with fine-grained reactivity.
Use when working with SolidJS projects including: (1) Creating components with signals, stores, and effects,
(2) Implementing reactive state management, (3) Using control flow components (Show, For, Switch/Match),
(4) Setting up routing with Solid Router, (5) Building full-stack apps with SolidStart,
(6) Data fetching with createResource, (7) Context API for shared state, (8) SSR/SSG configuration.
Triggers: solid, solidjs, solid-js, solid start, solidstart, createSignal, createStore, createEffect.
---
# SolidJS Development
SolidJS is a declarative JavaScript library for building user interfaces with fine-grained reactivity. Unlike virtual DOM frameworks, Solid compiles templates to real DOM nodes and updates them with fine-grained reactions.
## Core Principles
1. **Components run once** — Component functions execute only during initialization, not on every update
2. **Fine-grained reactivity** — Only the specific DOM nodes that depend on changed data update
3. **No virtual DOM** — Direct DOM manipulation via compiled templates
4. **Signals are functions** — Access values by calling: `count()` not `count`
## Reactivity Primitives
### Signals — Basic State
```tsx
import { createSignal } from "solid-js";
const [count, setCount] = createSignal(0);
// Read value (getter)
console.log(count()); // 0
// Update value (setter)
setCount(1);
setCount(prev => prev + 1); // Functional update
```
**Options:**
```tsx
const [value, setValue] = createSignal(initialValue, {
equals: false, // Always trigger updates, even if value unchanged
name: "debugName" // For devtools
});
```
### Effects — Side Effects
```tsx
import { createEffect } from "solid-js";
createEffect(() => {
console.log("Count changed:", count());
// Runs after render, re-runs when dependencies change
});
```
**Key behaviors:**
- Initial run: after render, before browser paint
- Subsequent runs: when tracked dependencies change
- Never runs during SSR or hydration
- Use `onCleanup` for cleanup logic
### Memos — Derived/Cached Values
```tsx
import { createMemo } from "solid-js";
const doubled = createMemo(() => count() * 2);
// Access like signal
console.log(doubled()); // Cached, only recalculates when count changes
```
Use memos when:
- Derived value is expensive to compute
- Derived value is accessed multiple times
- You want to prevent downstream updates when result unchanged
### Resources — Async Data
```tsx
import { createResource } from "solid-js";
const [user, { mutate, refetch }] = createResource(userId, fetchUser);
// In JSX
<Show when={!user.loading} fallback={<Loading />}>
<div>{user()?.name}</div>
</Show>
// Resource properties
user.loading // boolean
user.error // error if failed
user.state // "unresolved" | "pending" | "ready" | "refreshing" | "errored"
user.latest // last successful value
```
## Stores — Complex State
For nested objects/arrays with fine-grained updates:
```tsx
import { createStore } from "solid-js/store";
const [state, setState] = createStore({
user: { name: "John", age: 30 },
todos: []
});
// Path syntax updates
setState("user", "name", "Jane");
setState("todos", todos => [...todos, newTodo]);
setState("todos", 0, "completed", true);
// Produce for immer-like updates
import { produce } from "solid-js/store";
setState(produce(s => {
s.user.age++;
s.todos.push(newTodo);
}));
```
**Store utilities:**
- `produce` — Immer-like mutations
- `reconcile` — Diff and patch data (for API responses)
- `unwrap` — Get raw non-reactive object
## Components
### Basic Component
```tsx
import { Component } from "solid-js";
const MyComponent: Component<{ name: string }> = (props) => {
return <div>Hello, {props.name}</div>;
};
```
### Props Handling
```tsx
import { splitProps, mergeProps } from "solid-js";
// Default props
const merged = mergeProps({ size: "medium" }, props);
// Split props (for spreading)
const [local, others] = splitProps(props, ["class", "onClick"]);
return <button class={local.class} {...others} />;
```
**Props rules:**
- Props are reactive getters — don't destructure at top level
- Use `props.value` in JSX, not `const { value } = props`
### Children Helper
```tsx
import { children } from "solid-js";
const Wrapper: Component = (props) => {
const resolved = children(() => props.children);
createEffect(() => {
console.log("Children:", resolved());
});
return <div>{resolved()}</div>;
};
```
## Control Flow Components
### Show — Conditional Rendering
```tsx
import { Show } from "solid-js";
<Show when={user()} fallback={<Login />}>
{(user) => <Profile user={user()} />}
</Show>
```
### For — List Rendering (keyed by reference)
```tsx
import { For } from "solid-js";
<For each={items()} fallback={<Empty />}>
{(item, index) => (
<div>{index()}: {item.name}</div>
)}
</For>
```
**Note:** `index` is a signal, `item` is the value.
### Index — List Rendering (keyed by index)
```tsx
import { Index } from "solid-js";
<Index each={items()}>
{(item, index) => (
<input value={item().text} />
)}
</Index>
```
**Note:** `item` is a signal, `index` is the value. Better for primitive arrays or inputs.
### Switch/Match — Multiple Conditions
```tsx
import { Switch, Match } from "solid-js";
<Switch fallback={<Default />}>
<Match when={state() === "loading"}>
<Loading />
</Match>
<Match when={state() === "error"}>
<Error />
</Match>
<Match when={state() === "success"}>
<Success />
</Match>
</Switch>
```
### Dynamic — Dynamic Component
```tsx
import { Dynamic } from "solid-js/web";
<Dynamic component={selected()} someProp="value" />
```
### Portal — Render Outside DOM Hierarchy
```tsx
import { Portal } from "solid-js/web";
<Portal mount={document.body}>
<Modal />
</Portal>
```
### ErrorBoundary — Error Handling
```tsx
import { ErrorBoundary } from "solid-js";
<ErrorBoundary fallback={(err, reset) => (
<div>
Error: {err.message}
<button onClick={reset}>Retry</button>
</div>
)}>
<RiskyComponent />
</ErrorBoundary>
```
### Suspense — Async Loading
```tsx
import { Suspense } from "solid-js";
<Suspense fallback={<Loading />}>
<AsyncComponent />
</Suspense>
```
## Context API
```tsx
import { createContext, useContext } from "solid-js";
// Create context
const CounterContext = createContext<{
count: () => number;
increment: () => void;
}>();
// Provider component
export function CounterProvider(props) {
const [count, setCount] = createSignal(0);
return (
<CounterContext.Provider value={{
count,
increment: () => setCount(c => c + 1)
}}>
{props.children}
</CounterContext.Provider>
);
}
// Consumer hook
export function useCounter() {
const ctx = useContext(CounterContext);
if (!ctx) throw new Error("useCounter must be used within CounterProvider");
return ctx;
}
```
## Lifecycle
```tsx
import { onMount, onCleanup } from "solid-js";
function MyComponent() {
onMount(() => {
console.log("Mounted");
const handler = () => {};
window.addEventListener("resize", handler);
onCleanup(() => {
window.removeEventListener("resize", handler);
});
});
return <div>Content</div>;
}
```
## Refs
```tsx
let inputRef: HTMLInputElement;
<input ref={inputRef} />
<input ref={(el) => { /* el is the DOM element */ }} />
```
## Event Handling
```tsx
// Standard events (lowercase)
<button onClick={handleClick}>Click</button>
<button onClick={(e) => handleClick(e)}>Click</button>
// Delegated events (on:)
<input on:input={handleInput} />
// Native events (on:) - not delegated
<div on:scroll={handleScroll} />
```
## Common Patterns
### Conditional Classes
```tsx
import { clsx } from "clsx"; // or classList
<div class={clsx("base", { active: isActive() })} />
<div classList={{ active: isActive(), disabled: isDisabled() }} />
```
### Batch Updates
```tsx
import { batch } from "solid-js";
batch(() => {
setName("John");
setAge(30);
// Effects run once after batch completes
});
```
### Untrack
```tsx
import { untrack } from "solid-js";
createEffect(() => {
console.log(count()); // tracked
console.log(untrack(() => other())); // not tracked
});
```
## TypeScript
```tsx
import type { Component, ParentComponent, JSX } from "solid-js";
// Basic component
const Button: Component<{ label: string }> = (props) => (
<button>{props.label}</button>
);
// With children
const Layout: ParentComponent<{ title: string }> = (props) => (
<div>
<h1>{props.title}</h1>
{props.children}
</div>
);
// Event handler types
const handleClick: JSX.EventHandler<HTMLButtonElement, MouseEvent> = (e) => {
console.log(e.currentTarget);
};
```
## Project Setup
```bash
# Create new project
npm create solid@latest my-app
# With template
npx degit solidjs/templates/ts my-app
# SolidStart
npm create solid@latest my-app -- --template solidstart
```
**vite.config.ts:**
```ts
import { defineConfig } from "vite";
import solid from "vite-plugin-solid";
export default defineConfig({
plugins: [solid()]
});
```
## Anti-Patterns to Avoid
1. **Destructuring props** — Breaks reactivity
```tsx
// ❌ Bad
const { name } = props;
// ✅ Good
props.name
```
2. **Accessing signals outside tracking scope**
```tsx
// ❌ Won't update
console.log(count());
// ✅ Will update
createEffect(() => console.log(count()));
```
3. **Forgetting to call signal getters**
```tsx
// ❌ Passes the function
<div>{count}</div>
// ✅ Passes the value
<div>{count()}</div>
```
4. **Using array index as key** — Use `<For>` for reference-keyed, `<Index>` for index-keyed
5. **Side effects during render** — Use `createEffect` or `onMount`

View File

@@ -1,777 +0,0 @@
# SolidJS API Reference
Complete reference for all SolidJS primitives, utilities, and component APIs.
## Basic Reactivity
### createSignal
```tsx
import { createSignal } from "solid-js";
const [getter, setter] = createSignal<T>(initialValue, options?);
// Options
interface SignalOptions<T> {
equals?: false | ((prev: T, next: T) => boolean);
name?: string;
internal?: boolean;
}
```
**Examples:**
```tsx
const [count, setCount] = createSignal(0);
const [user, setUser] = createSignal<User | null>(null);
// Always update
const [data, setData] = createSignal(obj, { equals: false });
// Custom equality
const [items, setItems] = createSignal([], {
equals: (a, b) => a.length === b.length
});
// Setter forms
setCount(5); // Direct value
setCount(prev => prev + 1); // Functional update
```
### createEffect
```tsx
import { createEffect } from "solid-js";
createEffect<T>(fn: (prev: T) => T, initialValue?: T, options?);
// Options
interface EffectOptions {
name?: string;
}
```
**Examples:**
```tsx
// Basic
createEffect(() => {
console.log("Count:", count());
});
// With previous value
createEffect((prev) => {
console.log("Changed from", prev, "to", count());
return count();
}, count());
// With cleanup
createEffect(() => {
const handler = () => {};
window.addEventListener("resize", handler);
onCleanup(() => window.removeEventListener("resize", handler));
});
```
### createMemo
```tsx
import { createMemo } from "solid-js";
const getter = createMemo<T>(fn: (prev: T) => T, initialValue?: T, options?);
// Options
interface MemoOptions<T> {
equals?: false | ((prev: T, next: T) => boolean);
name?: string;
}
```
**Examples:**
```tsx
const doubled = createMemo(() => count() * 2);
const filtered = createMemo(() => items().filter(i => i.active));
// Previous value
const delta = createMemo((prev) => count() - prev, 0);
```
### createResource
```tsx
import { createResource } from "solid-js";
const [resource, { mutate, refetch }] = createResource(
source?, // Optional reactive source
fetcher, // (source, info) => Promise<T>
options?
);
// Resource properties
resource() // T | undefined
resource.loading // boolean
resource.error // any
resource.state // "unresolved" | "pending" | "ready" | "refreshing" | "errored"
resource.latest // T | undefined (last successful value)
// Options
interface ResourceOptions<T> {
initialValue?: T;
name?: string;
deferStream?: boolean;
ssrLoadFrom?: "initial" | "server";
storage?: (init: T) => [Accessor<T>, Setter<T>];
onHydrated?: (key, info: { value: T }) => void;
}
```
**Examples:**
```tsx
// Without source
const [users] = createResource(fetchUsers);
// With source
const [user] = createResource(userId, fetchUser);
// With options
const [data] = createResource(id, fetchData, {
initialValue: [],
deferStream: true
});
// Actions
mutate(newValue); // Update locally
refetch(); // Re-fetch
refetch(customInfo); // Pass to fetcher's info.refetching
```
## Stores
### createStore
```tsx
import { createStore } from "solid-js/store";
const [store, setStore] = createStore<T>(initialValue);
```
**Update patterns:**
```tsx
const [state, setState] = createStore({
user: { name: "John", age: 30 },
todos: [{ id: 1, text: "Learn Solid", done: false }]
});
// Path syntax
setState("user", "name", "Jane");
setState("user", "age", a => a + 1);
setState("todos", 0, "done", true);
// Array operations
setState("todos", t => [...t, newTodo]);
setState("todos", todos.length, newTodo);
// Multiple paths
setState("todos", { from: 0, to: 2 }, "done", true);
setState("todos", [0, 2, 4], "done", true);
setState("todos", i => i.done, "done", false);
// Object merge (shallow)
setState("user", { age: 31 }); // Keeps other properties
```
### produce
```tsx
import { produce } from "solid-js/store";
setState(produce(draft => {
draft.user.age++;
draft.todos.push({ id: 2, text: "New", done: false });
draft.todos[0].done = true;
}));
```
### reconcile
```tsx
import { reconcile } from "solid-js/store";
// Replace with diff (minimal updates)
setState("todos", reconcile(newTodosFromAPI));
// Options
reconcile(data, { key: "id", merge: true });
```
### unwrap
```tsx
import { unwrap } from "solid-js/store";
const raw = unwrap(store); // Non-reactive plain object
```
### createMutable
```tsx
import { createMutable } from "solid-js/store";
const state = createMutable({
count: 0,
user: { name: "John" }
});
// Direct mutation (like MobX)
state.count++;
state.user.name = "Jane";
```
### modifyMutable
```tsx
import { modifyMutable, reconcile, produce } from "solid-js/store";
modifyMutable(state, reconcile(newData));
modifyMutable(state, produce(s => { s.count++ }));
```
## Component APIs
### children
```tsx
import { children } from "solid-js";
const resolved = children(() => props.children);
// Access
resolved(); // JSX.Element | JSX.Element[]
resolved.toArray(); // Always array
```
### createContext / useContext
```tsx
import { createContext, useContext } from "solid-js";
const MyContext = createContext<T>(defaultValue?);
// Provider
<MyContext.Provider value={value}>
{children}
</MyContext.Provider>
// Consumer
const value = useContext(MyContext);
```
### createUniqueId
```tsx
import { createUniqueId } from "solid-js";
const id = createUniqueId(); // "0", "1", etc.
```
### lazy
```tsx
import { lazy } from "solid-js";
const LazyComponent = lazy(() => import("./Component"));
// Use with Suspense
<Suspense fallback={<Loading />}>
<LazyComponent />
</Suspense>
```
## Lifecycle
### onMount
```tsx
import { onMount } from "solid-js";
onMount(() => {
// Runs once after initial render
console.log("Mounted");
});
```
### onCleanup
```tsx
import { onCleanup } from "solid-js";
// In component
onCleanup(() => {
console.log("Cleaning up");
});
// In effect
createEffect(() => {
const sub = subscribe();
onCleanup(() => sub.unsubscribe());
});
```
## Reactive Utilities
### batch
```tsx
import { batch } from "solid-js";
batch(() => {
setA(1);
setB(2);
setC(3);
// Effects run once after batch
});
```
### untrack
```tsx
import { untrack } from "solid-js";
createEffect(() => {
console.log(a()); // Tracked
console.log(untrack(() => b())); // Not tracked
});
```
### on
```tsx
import { on } from "solid-js";
// Explicit dependencies
createEffect(on(count, (value, prev) => {
console.log("Count changed:", prev, "->", value);
}));
// Multiple dependencies
createEffect(on([a, b], ([a, b], [prevA, prevB]) => {
console.log("Changed");
}));
// Defer first run
createEffect(on(count, (v) => console.log(v), { defer: true }));
```
### mergeProps
```tsx
import { mergeProps } from "solid-js";
const merged = mergeProps(
{ size: "medium", color: "blue" }, // Defaults
props // Overrides
);
```
### splitProps
```tsx
import { splitProps } from "solid-js";
const [local, others] = splitProps(props, ["class", "onClick"]);
// local.class, local.onClick
// others contains everything else
const [a, b, rest] = splitProps(props, ["foo"], ["bar"]);
```
### createRoot
```tsx
import { createRoot } from "solid-js";
const dispose = createRoot((dispose) => {
const [count, setCount] = createSignal(0);
// Use signals...
return dispose;
});
// Later
dispose();
```
### getOwner / runWithOwner
```tsx
import { getOwner, runWithOwner } from "solid-js";
const owner = getOwner();
// Later, in async code
runWithOwner(owner, () => {
createEffect(() => {
// This effect has proper ownership
});
});
```
### mapArray
```tsx
import { mapArray } from "solid-js";
const mapped = mapArray(
() => items(),
(item, index) => ({ ...item, doubled: item.value * 2 })
);
```
### indexArray
```tsx
import { indexArray } from "solid-js";
const mapped = indexArray(
() => items(),
(item, index) => <div>{index}: {item().name}</div>
);
```
### observable
```tsx
import { observable } from "solid-js";
const obs = observable(signal);
obs.subscribe((value) => console.log(value));
```
### from
```tsx
import { from } from "solid-js";
// Convert observable/subscribable to signal
const signal = from(rxObservable);
const signal = from((set) => {
const unsub = subscribe(set);
return unsub;
});
```
### catchError
```tsx
import { catchError } from "solid-js";
catchError(
() => riskyOperation(),
(err) => console.error("Error:", err)
);
```
## Secondary Primitives
### createComputed
```tsx
import { createComputed } from "solid-js";
// Like createEffect but runs during render phase
createComputed(() => {
setDerived(source() * 2);
});
```
### createRenderEffect
```tsx
import { createRenderEffect } from "solid-js";
// Runs before paint (for DOM measurements)
createRenderEffect(() => {
const height = element.offsetHeight;
});
```
### createDeferred
```tsx
import { createDeferred } from "solid-js";
// Returns value after idle time
const deferred = createDeferred(() => expensiveComputation(), {
timeoutMs: 1000
});
```
### createReaction
```tsx
import { createReaction } from "solid-js";
const track = createReaction(() => {
console.log("Something changed");
});
track(() => count()); // Start tracking
```
### createSelector
```tsx
import { createSelector } from "solid-js";
const isSelected = createSelector(selectedId);
<For each={items()}>
{(item) => (
<div class={isSelected(item.id) ? "selected" : ""}>
{item.name}
</div>
)}
</For>
```
## Components
### Show
```tsx
<Show when={condition()} fallback={<Fallback />}>
<Content />
</Show>
// With callback (narrowed type)
<Show when={user()}>
{(user) => <div>{user().name}</div>}
</Show>
```
### For
```tsx
<For each={items()} fallback={<Empty />}>
{(item, index) => <div>{index()}: {item.name}</div>}
</For>
```
### Index
```tsx
<Index each={items()} fallback={<Empty />}>
{(item, index) => <input value={item().text} />}
</Index>
```
### Switch / Match
```tsx
<Switch fallback={<Default />}>
<Match when={state() === "loading"}>
<Loading />
</Match>
<Match when={state() === "error"}>
<Error />
</Match>
</Switch>
```
### Dynamic
```tsx
import { Dynamic } from "solid-js/web";
<Dynamic component={selected()} prop={value} />
<Dynamic component="div" class="dynamic">Content</Dynamic>
```
### Portal
```tsx
import { Portal } from "solid-js/web";
<Portal mount={document.body}>
<Modal />
</Portal>
```
### ErrorBoundary
```tsx
<ErrorBoundary fallback={(err, reset) => (
<div>
<p>Error: {err.message}</p>
<button onClick={reset}>Retry</button>
</div>
)}>
<Content />
</ErrorBoundary>
```
### Suspense
```tsx
<Suspense fallback={<Loading />}>
<AsyncContent />
</Suspense>
```
### SuspenseList
```tsx
<SuspenseList revealOrder="forwards" tail="collapsed">
<Suspense fallback={<Loading />}><Item1 /></Suspense>
<Suspense fallback={<Loading />}><Item2 /></Suspense>
<Suspense fallback={<Loading />}><Item3 /></Suspense>
</SuspenseList>
```
## Rendering
### render
```tsx
import { render } from "solid-js/web";
const dispose = render(() => <App />, document.getElementById("root")!);
// Cleanup
dispose();
```
### hydrate
```tsx
import { hydrate } from "solid-js/web";
hydrate(() => <App />, document.getElementById("root")!);
```
### renderToString
```tsx
import { renderToString } from "solid-js/web";
const html = renderToString(() => <App />);
```
### renderToStringAsync
```tsx
import { renderToStringAsync } from "solid-js/web";
const html = await renderToStringAsync(() => <App />);
```
### renderToStream
```tsx
import { renderToStream } from "solid-js/web";
const stream = renderToStream(() => <App />);
stream.pipe(res);
```
### isServer
```tsx
import { isServer } from "solid-js/web";
if (isServer) {
// Server-only code
}
```
## JSX Attributes
### ref
```tsx
let el: HTMLDivElement;
<div ref={el} />
<div ref={(e) => console.log(e)} />
```
### classList
```tsx
<div classList={{ active: isActive(), disabled: isDisabled() }} />
```
### style
```tsx
<div style={{ color: "red", "font-size": "14px" }} />
<div style={`color: ${color()}`} />
```
### on:event (native)
```tsx
<div on:click={handleClick} />
<div on:scroll={handleScroll} />
```
### use:directive
```tsx
function clickOutside(el: HTMLElement, accessor: () => () => void) {
const handler = (e: MouseEvent) => {
if (!el.contains(e.target as Node)) accessor()();
};
document.addEventListener("click", handler);
onCleanup(() => document.removeEventListener("click", handler));
}
<div use:clickOutside={() => setOpen(false)} />
```
### prop:property
```tsx
<input prop:value={value()} /> // Set as property, not attribute
```
### attr:attribute
```tsx
<div attr:data-custom={value()} /> // Force attribute
```
### bool:attribute
```tsx
<input bool:disabled={isDisabled()} />
```
### @once
```tsx
<div title={/*@once*/ staticValue} /> // Never updates
```
## Types
```tsx
import type {
Component,
ParentComponent,
FlowComponent,
VoidComponent,
JSX,
Accessor,
Setter,
Signal,
Resource,
Owner
} from "solid-js";
// Component types
const MyComponent: Component<Props> = (props) => <div />;
const Parent: ParentComponent<Props> = (props) => <div>{props.children}</div>;
const Flow: FlowComponent<Props, Item> = (props) => props.children(item);
const Void: VoidComponent<Props> = (props) => <input />;
// Event types
type Handler = JSX.EventHandler<HTMLButtonElement, MouseEvent>;
type ChangeHandler = JSX.ChangeEventHandler<HTMLInputElement>;
```

View File

@@ -1,720 +0,0 @@
# SolidJS Patterns & Best Practices
Common patterns, recipes, and best practices for SolidJS development.
## Component Patterns
### Controlled vs Uncontrolled Inputs
**Controlled:**
```tsx
function ControlledInput() {
const [value, setValue] = createSignal("");
return (
<input
value={value()}
onInput={(e) => setValue(e.currentTarget.value)}
/>
);
}
```
**Uncontrolled with ref:**
```tsx
function UncontrolledInput() {
let inputRef: HTMLInputElement;
const handleSubmit = () => {
console.log(inputRef.value);
};
return (
<>
<input ref={inputRef!} />
<button onClick={handleSubmit}>Submit</button>
</>
);
}
```
### Compound Components
```tsx
const Tabs = {
Root: (props: ParentProps<{ defaultTab?: string }>) => {
const [activeTab, setActiveTab] = createSignal(props.defaultTab ?? "");
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div class="tabs">{props.children}</div>
</TabsContext.Provider>
);
},
List: (props: ParentProps) => (
<div class="tabs-list" role="tablist">{props.children}</div>
),
Tab: (props: ParentProps<{ value: string }>) => {
const ctx = useTabsContext();
return (
<button
role="tab"
aria-selected={ctx.activeTab() === props.value}
onClick={() => ctx.setActiveTab(props.value)}
>
{props.children}
</button>
);
},
Panel: (props: ParentProps<{ value: string }>) => {
const ctx = useTabsContext();
return (
<Show when={ctx.activeTab() === props.value}>
<div role="tabpanel">{props.children}</div>
</Show>
);
}
};
// Usage
<Tabs.Root defaultTab="first">
<Tabs.List>
<Tabs.Tab value="first">First</Tabs.Tab>
<Tabs.Tab value="second">Second</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="first">First Content</Tabs.Panel>
<Tabs.Panel value="second">Second Content</Tabs.Panel>
</Tabs.Root>
```
### Render Props
```tsx
function MouseTracker(props: {
children: (pos: { x: number; y: number }) => JSX.Element;
}) {
const [pos, setPos] = createSignal({ x: 0, y: 0 });
onMount(() => {
const handler = (e: MouseEvent) => setPos({ x: e.clientX, y: e.clientY });
window.addEventListener("mousemove", handler);
onCleanup(() => window.removeEventListener("mousemove", handler));
});
return <>{props.children(pos())}</>;
}
// Usage
<MouseTracker>
{(pos) => <div>Mouse: {pos.x}, {pos.y}</div>}
</MouseTracker>
```
### Higher-Order Components
```tsx
function withAuth<P extends object>(Component: Component<P>) {
return (props: P) => {
const { user } = useAuth();
return (
<Show when={user()} fallback={<Redirect to="/login" />}>
<Component {...props} />
</Show>
);
};
}
const ProtectedDashboard = withAuth(Dashboard);
```
### Polymorphic Components
```tsx
type PolymorphicProps<E extends keyof JSX.IntrinsicElements> = {
as?: E;
} & JSX.IntrinsicElements[E];
function Box<E extends keyof JSX.IntrinsicElements = "div">(
props: PolymorphicProps<E>
) {
const [local, others] = splitProps(props as PolymorphicProps<"div">, ["as"]);
return <Dynamic component={local.as || "div"} {...others} />;
}
// Usage
<Box>Default div</Box>
<Box as="section">Section element</Box>
<Box as="button" onClick={handleClick}>Button</Box>
```
## State Patterns
### Derived State with Multiple Sources
```tsx
function SearchResults() {
const [query, setQuery] = createSignal("");
const [filters, setFilters] = createSignal({ category: "all" });
const results = createMemo(() => {
const q = query().toLowerCase();
const f = filters();
return allItems()
.filter(item => item.name.toLowerCase().includes(q))
.filter(item => f.category === "all" || item.category === f.category);
});
return <For each={results()}>{item => <Item item={item} />}</For>;
}
```
### State Machine Pattern
```tsx
type State = "idle" | "loading" | "success" | "error";
type Event = { type: "FETCH" } | { type: "SUCCESS"; data: any } | { type: "ERROR"; error: Error };
function createMachine(initial: State) {
const [state, setState] = createSignal<State>(initial);
const [data, setData] = createSignal<any>(null);
const [error, setError] = createSignal<Error | null>(null);
const send = (event: Event) => {
const current = state();
switch (current) {
case "idle":
if (event.type === "FETCH") setState("loading");
break;
case "loading":
if (event.type === "SUCCESS") {
setData(event.data);
setState("success");
} else if (event.type === "ERROR") {
setError(event.error);
setState("error");
}
break;
}
};
return { state, data, error, send };
}
```
### Optimistic Updates
```tsx
const [todos, setTodos] = createStore<Todo[]>([]);
async function deleteTodo(id: string) {
const original = [...unwrap(todos)];
// Optimistic remove
setTodos(todos => todos.filter(t => t.id !== id));
try {
await api.deleteTodo(id);
} catch {
// Rollback on error
setTodos(reconcile(original));
}
}
```
### Undo/Redo
```tsx
function createHistory<T>(initial: T) {
const [past, setPast] = createSignal<T[]>([]);
const [present, setPresent] = createSignal<T>(initial);
const [future, setFuture] = createSignal<T[]>([]);
const canUndo = () => past().length > 0;
const canRedo = () => future().length > 0;
const set = (value: T | ((prev: T) => T)) => {
const newValue = typeof value === "function"
? (value as (prev: T) => T)(present())
: value;
setPast(p => [...p, present()]);
setPresent(newValue);
setFuture([]);
};
const undo = () => {
if (!canUndo()) return;
const previous = past()[past().length - 1];
setPast(p => p.slice(0, -1));
setFuture(f => [present(), ...f]);
setPresent(previous);
};
const redo = () => {
if (!canRedo()) return;
const next = future()[0];
setPast(p => [...p, present()]);
setFuture(f => f.slice(1));
setPresent(next);
};
return { value: present, set, undo, redo, canUndo, canRedo };
}
```
## Custom Hooks/Primitives
### useLocalStorage
```tsx
function createLocalStorage<T>(key: string, initialValue: T) {
const stored = localStorage.getItem(key);
const initial = stored ? JSON.parse(stored) : initialValue;
const [value, setValue] = createSignal<T>(initial);
createEffect(() => {
localStorage.setItem(key, JSON.stringify(value()));
});
return [value, setValue] as const;
}
```
### useDebounce
```tsx
function createDebounce<T>(source: () => T, delay: number) {
const [debounced, setDebounced] = createSignal<T>(source());
createEffect(() => {
const value = source();
const timer = setTimeout(() => setDebounced(() => value), delay);
onCleanup(() => clearTimeout(timer));
});
return debounced;
}
// Usage
const debouncedQuery = createDebounce(query, 300);
```
### useThrottle
```tsx
function createThrottle<T>(source: () => T, delay: number) {
const [throttled, setThrottled] = createSignal<T>(source());
let lastRun = 0;
createEffect(() => {
const value = source();
const now = Date.now();
if (now - lastRun >= delay) {
lastRun = now;
setThrottled(() => value);
} else {
const timer = setTimeout(() => {
lastRun = Date.now();
setThrottled(() => value);
}, delay - (now - lastRun));
onCleanup(() => clearTimeout(timer));
}
});
return throttled;
}
```
### useMediaQuery
```tsx
function createMediaQuery(query: string) {
const mql = window.matchMedia(query);
const [matches, setMatches] = createSignal(mql.matches);
onMount(() => {
const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
mql.addEventListener("change", handler);
onCleanup(() => mql.removeEventListener("change", handler));
});
return matches;
}
// Usage
const isMobile = createMediaQuery("(max-width: 768px)");
```
### useClickOutside
```tsx
function createClickOutside(
ref: () => HTMLElement | undefined,
callback: () => void
) {
onMount(() => {
const handler = (e: MouseEvent) => {
const el = ref();
if (el && !el.contains(e.target as Node)) {
callback();
}
};
document.addEventListener("click", handler);
onCleanup(() => document.removeEventListener("click", handler));
});
}
// Usage
let dropdownRef: HTMLDivElement;
createClickOutside(() => dropdownRef, () => setOpen(false));
```
### useIntersectionObserver
```tsx
function createIntersectionObserver(
ref: () => HTMLElement | undefined,
options?: IntersectionObserverInit
) {
const [isIntersecting, setIsIntersecting] = createSignal(false);
onMount(() => {
const el = ref();
if (!el) return;
const observer = new IntersectionObserver(([entry]) => {
setIsIntersecting(entry.isIntersecting);
}, options);
observer.observe(el);
onCleanup(() => observer.disconnect());
});
return isIntersecting;
}
```
## Form Patterns
### Form Validation
```tsx
function createForm<T extends Record<string, any>>(initial: T) {
const [values, setValues] = createStore<T>(initial);
const [errors, setErrors] = createStore<Partial<Record<keyof T, string>>>({});
const [touched, setTouched] = createStore<Partial<Record<keyof T, boolean>>>({});
const handleChange = (field: keyof T) => (e: Event) => {
const target = e.target as HTMLInputElement;
setValues(field as any, target.value as any);
};
const handleBlur = (field: keyof T) => () => {
setTouched(field as any, true);
};
const validate = (validators: Partial<Record<keyof T, (v: any) => string | undefined>>) => {
let isValid = true;
for (const [field, validator] of Object.entries(validators)) {
if (validator) {
const error = validator(values[field as keyof T]);
setErrors(field as any, error as any);
if (error) isValid = false;
}
}
return isValid;
};
return { values, errors, touched, handleChange, handleBlur, validate, setValues };
}
// Usage
const form = createForm({ email: "", password: "" });
<input
value={form.values.email}
onInput={form.handleChange("email")}
onBlur={form.handleBlur("email")}
/>
<Show when={form.touched.email && form.errors.email}>
<span class="error">{form.errors.email}</span>
</Show>
```
### Field Array
```tsx
function createFieldArray<T>(initial: T[] = []) {
const [fields, setFields] = createStore<T[]>(initial);
const append = (value: T) => setFields(f => [...f, value]);
const remove = (index: number) => setFields(f => f.filter((_, i) => i !== index));
const update = (index: number, value: Partial<T>) => setFields(index, v => ({ ...v, ...value }));
const move = (from: number, to: number) => {
setFields(produce(f => {
const [item] = f.splice(from, 1);
f.splice(to, 0, item);
}));
};
return { fields, append, remove, update, move };
}
```
## Performance Patterns
### Virtualized List
```tsx
function VirtualList<T>(props: {
items: T[];
itemHeight: number;
height: number;
renderItem: (item: T, index: number) => JSX.Element;
}) {
const [scrollTop, setScrollTop] = createSignal(0);
const startIndex = createMemo(() =>
Math.floor(scrollTop() / props.itemHeight)
);
const visibleCount = createMemo(() =>
Math.ceil(props.height / props.itemHeight) + 1
);
const visibleItems = createMemo(() =>
props.items.slice(startIndex(), startIndex() + visibleCount())
);
return (
<div
style={{ height: `${props.height}px`, overflow: "auto" }}
onScroll={(e) => setScrollTop(e.currentTarget.scrollTop)}
>
<div style={{ height: `${props.items.length * props.itemHeight}px`, position: "relative" }}>
<For each={visibleItems()}>
{(item, i) => (
<div style={{
position: "absolute",
top: `${(startIndex() + i()) * props.itemHeight}px`,
height: `${props.itemHeight}px`
}}>
{props.renderItem(item, startIndex() + i())}
</div>
)}
</For>
</div>
</div>
);
}
```
### Lazy Loading with Intersection Observer
```tsx
function LazyLoad(props: ParentProps<{ placeholder?: JSX.Element }>) {
let ref: HTMLDivElement;
const [isVisible, setIsVisible] = createSignal(false);
onMount(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
},
{ rootMargin: "100px" }
);
observer.observe(ref);
onCleanup(() => observer.disconnect());
});
return (
<div ref={ref!}>
<Show when={isVisible()} fallback={props.placeholder}>
{props.children}
</Show>
</div>
);
}
```
### Memoized Component
```tsx
// For expensive components that shouldn't re-render on parent updates
function MemoizedExpensiveList(props: { items: Item[] }) {
// Component only re-renders when items actually change
return (
<For each={props.items}>
{(item) => <ExpensiveItem item={item} />}
</For>
);
}
```
## Testing Patterns
### Component Testing
```tsx
import { render, fireEvent, screen } from "@solidjs/testing-library";
test("Counter increments", async () => {
render(() => <Counter />);
const button = screen.getByRole("button", { name: /increment/i });
expect(screen.getByText("Count: 0")).toBeInTheDocument();
fireEvent.click(button);
expect(screen.getByText("Count: 1")).toBeInTheDocument();
});
```
### Testing with Context
```tsx
function renderWithContext(component: () => JSX.Element) {
return render(() => (
<ThemeProvider>
<AuthProvider>
{component()}
</AuthProvider>
</ThemeProvider>
));
}
test("Dashboard shows user", () => {
renderWithContext(() => <Dashboard />);
// ...
});
```
### Testing Async Components
```tsx
import { render, waitFor, screen } from "@solidjs/testing-library";
test("Loads user data", async () => {
render(() => <UserProfile userId="123" />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText("John Doe")).toBeInTheDocument();
});
});
```
## Error Handling Patterns
### Global Error Handler
```tsx
function App() {
return (
<ErrorBoundary
fallback={(err, reset) => (
<ErrorPage error={err} onRetry={reset} />
)}
>
<Suspense fallback={<AppLoader />}>
<Router>
{/* Routes */}
</Router>
</Suspense>
</ErrorBoundary>
);
}
```
### Async Error Handling
```tsx
function DataComponent() {
const [data] = createResource(fetchData);
return (
<Switch>
<Match when={data.loading}>
<Loading />
</Match>
<Match when={data.error}>
<Error error={data.error} onRetry={() => refetch()} />
</Match>
<Match when={data()}>
{(data) => <Content data={data()} />}
</Match>
</Switch>
);
}
```
## Accessibility Patterns
### Focus Management
```tsx
function Modal(props: ParentProps<{ isOpen: boolean; onClose: () => void }>) {
let dialogRef: HTMLDivElement;
let previousFocus: HTMLElement | null;
createEffect(() => {
if (props.isOpen) {
previousFocus = document.activeElement as HTMLElement;
dialogRef.focus();
} else if (previousFocus) {
previousFocus.focus();
}
});
return (
<Show when={props.isOpen}>
<Portal>
<div
ref={dialogRef!}
role="dialog"
aria-modal="true"
tabIndex={-1}
onKeyDown={(e) => e.key === "Escape" && props.onClose()}
>
{props.children}
</div>
</Portal>
</Show>
);
}
```
### Live Regions
```tsx
function Notifications() {
const [message, setMessage] = createSignal("");
return (
<div
role="status"
aria-live="polite"
aria-atomic="true"
class="sr-only"
>
{message()}
</div>
);
}
```

View File

@@ -1,223 +0,0 @@
---
name: spec-planner
description: Dialogue-driven spec development through skeptical questioning and iterative refinement. Triggers: "spec this out", feature planning, architecture decisions, "is this worth it?" questions, RFC/design doc creation, work scoping. Invoke Librarian for unfamiliar tech/frameworks/APIs.
---
# Spec Planner
Produce implementation-ready specs through rigorous dialogue and honest trade-off analysis.
## Core Philosophy
- **Dialogue over deliverables** - Plans emerge from discussion, not assumption
- **Skeptical by default** - Requirements are incomplete until proven otherwise
- **Second-order thinking** - Consider downstream effects and maintenance burden
## Workflow Phases
```
CLARIFY --[user responds]--> DISCOVER --[done]--> DRAFT --[complete]--> REFINE --[approved]--> DONE
| | | |
+--[still ambiguous]--<------+-------------------+----[gaps found]------+
```
**State phase at end of every response:**
```
---
Phase: CLARIFY | Waiting for: answers to questions 1-4
```
---
## Phase 1: CLARIFY (Mandatory)
**Hard rule:** No spec until user has responded to at least one round of questions.
1. **STOP.** Do not proceed to planning.
2. Identify gaps in: scope, motivation, constraints, edge cases, success criteria
3. Ask 3-5 pointed questions that would change the approach. USE YOUR QUESTION TOOL.
4. **Wait for responses**
**IMPORTANT: Always use the `question` tool to ask clarifying questions.** Do NOT output questions as freeform text. The question tool provides structured options and better UX. Example:
```
question({
questions: [{
header: "Scope",
question: "Which subsystems need detailed specs?",
options: [
{ label: "VCS layer", description: "jj-lib + gix unified interface" },
{ label: "Review workflow", description: "GitHub PR-style local review" },
{ label: "Event system", description: "pub/sub + persistence" }
],
multiple: true
}]
})
```
| Category | Example |
|----------|---------|
| Scope | "Share where? Social media? Direct link? Embed?" |
| Motivation | "What user problem are we actually solving?" |
| Constraints | "Does this need to work with existing privacy settings?" |
| Success | "How will we know this worked?" |
**Escape prevention:** Even if request seems complete, ask 2+ clarifying questions. Skip only for mechanical requests (e.g., "rename X to Y").
**Anti-patterns to resist:**
- "Just give me a rough plan" -> Still needs scope questions
- "I'll figure out the details" -> Those details ARE the spec
- Very long initial request -> Longer != clearer; probe assumptions
**Transition:** User answered AND no new ambiguities -> DISCOVER
---
## Phase 2: DISCOVER
**After clarification, before planning:** Understand existing system.
Launch explore subagents in parallel:
```
Task(
subagent_type="explore",
description="Explore [area name]",
prompt="Explore [area]. Return: key files, abstractions, patterns, integration points."
)
```
| Target | What to Find |
|--------|--------------|
| Affected area | Files, modules that will change |
| Existing patterns | How similar features are implemented |
| Integration points | APIs, events, data flows touched |
**If unfamiliar tech involved**, invoke Librarian:
```
Task(
subagent_type="librarian",
description="Research [tech name]",
prompt="Research [tech] for [use case]. Return: recommended approach, gotchas, production patterns."
)
```
**Output:** Brief architecture summary before proposing solutions.
**Transition:** System context understood -> DRAFT
---
## Phase 3: DRAFT
Apply planning framework from [decision-frameworks.md](./references/decision-frameworks.md):
1. **Problem Definition** - What are we solving? For whom? Cost of not solving?
2. **Constraints Inventory** - Time, system, knowledge, scope ceiling
3. **Solution Space** - Simplest -> Balanced -> Full engineering solution
4. **Trade-off Analysis** - See table format in references
5. **Recommendation** - One clear choice with reasoning
Use appropriate template from [templates.md](./references/templates.md):
- **Quick Decision** - Scoped technical choices
- **Feature Plan** - New feature development
- **ADR** - Architecture decisions
- **RFC** - Larger proposals
**Transition:** Spec produced -> REFINE
---
## Phase 4: REFINE
Run completeness check:
| Criterion | Check |
|-----------|-------|
| Scope bounded | Every deliverable listed; non-goals explicit |
| Ambiguity resolved | No "TBD" or "to be determined" |
| Acceptance testable | Each criterion pass/fail verifiable |
| Dependencies ordered | Clear what blocks what |
| Types defined | Data shapes specified (not "some object") |
| Effort estimated | Each deliverable has S/M/L/XL |
| Risks identified | At least 2 risks with mitigations |
| Open questions | Resolved OR assigned owner |
**If any criterion fails:** Return to dialogue. "To finalize, I need clarity on: [failing criteria]."
**Transition:** All criteria pass + user approval -> DONE
---
## Phase 5: DONE
### Final Output
```
=== Spec Complete ===
Phase: DONE
Type: <feature plan | architecture decision | refactoring | strategy>
Effort: <S/M/L/XL>
Status: Ready for task breakdown
Discovery:
- Explored: <areas investigated>
- Key findings: <relevant architecture/patterns>
Recommendation:
<brief summary>
Key Trade-offs:
- <what we're choosing vs alternatives>
Deliverables (Ordered):
1. [D1] (effort) - depends on: -
2. [D2] (effort) - depends on: D1
Open Questions:
- [ ] <if any remain> -> Owner: [who]
```
### Write Spec to File (MANDATORY)
1. Derive filename from feature/decision name (kebab-case)
2. Write spec to `specs/<filename>.md`
3. Confirm: `Spec written to: specs/<filename>.md`
---
## Effort Estimates
| Size | Time | Scope |
|------|------|-------|
| **S** | <1 hour | Single file, isolated change |
| **M** | 1-3 hours | Few files, contained feature |
| **L** | 1-2 days | Cross-cutting, multiple components |
| **XL** | >2 days | Major refactor, new system |
## Scope Control
When scope creeps:
1. **Name it:** "That's scope expansion. Let's finish X first."
2. **Park it:** "Added to Open Questions. Revisit after core spec stable."
3. **Cost it:** "Adding Y changes effort from M to XL. Worth it?"
**Hard rule:** If scope changes, re-estimate and flag explicitly.
## References
| File | When to Read |
|------|--------------|
| [templates.md](./references/templates.md) | Output formats for plans, ADRs, RFCs |
| [decision-frameworks.md](./references/decision-frameworks.md) | Complex multi-factor decisions |
| [estimation.md](./references/estimation.md) | Breaking down work, avoiding underestimation |
| [technical-debt.md](./references/technical-debt.md) | Evaluating refactoring ROI |
## Integration
| Agent | When to Invoke |
|-------|----------------|
| **Librarian** | Research unfamiliar tech, APIs, frameworks |
| **Oracle** | Deep architectural analysis, complex debugging |

View File

@@ -1,75 +0,0 @@
# Decision Frameworks
## Reversibility Matrix
| Decision Type | Approach |
|---------------|----------|
| **Two-way door** (easily reversed) | Decide fast, learn from outcome |
| **One-way door** (hard to reverse) | Invest time in analysis |
Most decisions are two-way doors. Don't over-analyze.
## Cost of Delay
```
Daily Cost = (Value Delivered / Time to Deliver) x Risk Factor
```
Use when prioritizing:
- High daily cost -> Do first
- Low daily cost -> Can wait
## RICE Scoring
| Factor | Question | Scale |
|--------|----------|-------|
| **R**each | How many users affected? | # users/period |
| **I**mpact | How much per user? | 0.25, 0.5, 1, 2, 3 |
| **C**onfidence | How sure are we? | 20%, 50%, 80%, 100% |
| **E**ffort | Person-weeks | 0.5, 1, 2, 4, 8+ |
```
RICE = (Reach x Impact x Confidence) / Effort
```
## Technical Decision Checklist
Before committing to a technical approach:
- [ ] Have we talked to someone who's done this before?
- [ ] What's the simplest version that teaches us something?
- [ ] What would make us reverse this decision?
- [ ] Who maintains this in 6 months?
- [ ] What's our rollback plan?
## When to Build vs Buy vs Adopt
| Signal | Build | Buy | Adopt (OSS) |
|--------|-------|-----|-------------|
| Core differentiator | Yes | No | Maybe |
| Commodity problem | No | Yes | Yes |
| Tight integration needed | Yes | Maybe | Maybe |
| Team has expertise | Yes | N/A | Yes |
| Time pressure | No | Yes | Maybe |
| Long-term control needed | Yes | No | Maybe |
## Decomposition Strategies
### Vertical Slicing
Cut features into thin end-to-end slices that deliver value:
```
Bad: "Build database layer" -> "Build API" -> "Build UI"
Good: "User can see their profile" -> "User can edit name" -> "User can upload avatar"
```
### Risk-First Ordering
1. Identify highest-risk unknowns
2. Build spike/proof-of-concept for those first
3. Then build around proven foundation
### Dependency Mapping
```
[Feature A] -depends on-> [Feature B] -depends on-> [Feature C]
^
Start here
```

View File

@@ -1,69 +0,0 @@
# Estimation
## Why Estimates Fail
| Cause | Mitigation |
|-------|------------|
| Optimism bias | Use historical data, not gut |
| Missing scope | List "obvious" tasks explicitly |
| Integration blindness | Add 20-30% for glue code |
| Unknown unknowns | Add buffer based on novelty |
| Interruptions | Assume 60% focused time |
## Estimation Techniques
### Three-Point Estimation
```
Expected = (Optimistic + 4xMostLikely + Pessimistic) / 6
```
### Relative Sizing
Compare to known references:
- "This is about twice as complex as Feature X"
- Use Fibonacci (1, 2, 3, 5, 8, 13) to reflect uncertainty
### Task Decomposition
1. Break into tasks <=4 hours
2. If can't decompose, spike first
3. Sum tasks + 20% integration buffer
## Effort Multipliers
| Factor | Multiplier |
|--------|------------|
| New technology | 1.5-2x |
| Unclear requirements | 1.3-1.5x |
| External dependencies (waiting on others) | 1.2-1.5x |
| Legacy/undocumented code | 1.3-2x |
| Production deployment | 1.2x |
| First time doing X | 2-3x |
| Context switching (other priorities) | 1.3x |
| Yak shaving risk (unknown unknowns) | 1.5x |
## Hidden Work Checklist
Always include time for:
- [ ] Code review (20% of dev time)
- [ ] Testing (30-50% of dev time)
- [ ] Documentation (10% of dev time)
- [ ] Deployment/config (varies)
- [ ] Bug fixes from testing (20% buffer)
- [ ] Interruptions / competing priorities
## When to Re-Estimate
Re-estimate when:
- Scope changes materially
- Major unknown becomes known
- Actual progress diverges >30% from estimate
## Communicating Estimates
**Good:** "1-2 weeks, confidence 70%-main risk is the third-party API integration"
**Bad:** "About 2 weeks"
Always include:
1. Range, not point estimate
2. Confidence level
3. Key assumptions/risks

View File

@@ -1,94 +0,0 @@
# Technical Debt
## Debt Categories
| Type | Example | Urgency |
|------|---------|---------|
| **Deliberate-Prudent** | "Ship now, refactor next sprint" | Planned paydown |
| **Deliberate-Reckless** | "We don't have time for tests" | Accumulating risk |
| **Inadvertent-Prudent** | "Now we know a better way" | Normal learning |
| **Inadvertent-Reckless** | "What's layering?" | Learning curve |
## When to Pay Down Debt
**Pay now when:**
- Debt is in path of upcoming work
- Cognitive load slowing every change
- Bugs recurring in same area
- Onboarding time increasing
**Defer when:**
- Area is stable, rarely touched
- Bigger refactor coming anyway
- Time constrained on priority work
- Code may be deprecated
## ROI Framework
```
Debt ROI = (Time Saved Per Touch x Touches/Month x Months) / Paydown Cost
```
| ROI | Action |
|-----|--------|
| >3x | Prioritize immediately |
| 1-3x | Plan into upcoming work |
| <1x | Accept or isolate |
## Refactoring Strategies
### Strangler Fig
1. Build new alongside old
2. Redirect traffic incrementally
3. Remove old when empty
Best for: Large system replacements
### Branch by Abstraction
1. Create abstraction over old code
2. Implement new behind abstraction
3. Switch implementations
4. Remove old
Best for: Library/dependency swaps
### Parallel Change (Expand-Contract)
1. Add new behavior alongside old
2. Migrate callers incrementally
3. Remove old behavior
Best for: API changes
### Mikado Method
1. Try the change
2. When it breaks, note prerequisites
3. Revert
4. Recursively fix prerequisites
5. Apply original change
Best for: Untangling dependencies
## Tracking Debt
Minimum viable debt tracking:
```markdown
## Tech Debt Log
| ID | Description | Impact | Area | Added |
|----|-------------|--------|------|-------|
| TD-1 | No caching layer | Slow queries | /api | 2024-01 |
```
Review monthly. Prune resolved items.
## Communicating Debt to Stakeholders
**Frame as investment, not cleanup:**
- "This will reduce bug rate by ~30%"
- "Deployment time goes from 2 hours to 20 minutes"
- "New features in this area take 2x longer than they should"
**Avoid:**
- "The code is messy"
- "We need to refactor"
- Technical jargon without business impact

View File

@@ -1,161 +0,0 @@
# Output Templates
## Quick Decision
For scoped technical choices with clear options.
```
## Decision: [choice]
**Why:** [1-2 sentences]
**Trade-off:** [what we're giving up]
**Revisit if:** [trigger conditions]
```
## Feature Plan (Implementation-Ready)
For new feature development. **Complete enough for task decomposition.**
```
## Feature: [name]
### Problem Statement
**Who:** [specific user/persona]
**What:** [the problem they face]
**Why it matters:** [business/user impact]
**Evidence:** [how we know this is real]
### Proposed Solution
[High-level approach in 2-3 paragraphs]
### Scope & Deliverables
| Deliverable | Effort | Depends On |
|-------------|--------|------------|
| [D1] | S/M/L | - |
| [D2] | S/M/L | D1 |
### Non-Goals (Explicit Exclusions)
- [Thing people might assume is in scope but isn't]
### Data Model
[Types, schemas, state shapes that will exist or change]
### API/Interface Contract
[Public interfaces between components-input/output/errors]
### Acceptance Criteria
- [ ] [Testable statement 1]
- [ ] [Testable statement 2]
### Test Strategy
| Layer | What | How |
|-------|------|-----|
| Unit | [specific logic] | [approach] |
| Integration | [boundaries] | [approach] |
### Risks & Mitigations
| Risk | Likelihood | Impact | Mitigation |
|------|------------|--------|------------|
### Trade-offs Made
| Chose | Over | Because |
|-------|------|---------|
### Open Questions
- [ ] [Question] -> Owner: [who decides]
### Success Metrics
- [Measurable outcome]
```
## Architecture Decision Record (ADR)
For significant architecture decisions that need documentation.
```
## ADR: [title]
**Status:** Proposed | Accepted | Deprecated | Superseded
**Date:** [date]
### Context
[What forces are at play]
### Decision
[What we're doing]
### Consequences
- [+] [Benefit]
- [-] [Drawback]
- [~] [Neutral observation]
```
## RFC (Request for Comments)
For larger proposals needing broader review.
```
## RFC: [title]
**Author:** [name]
**Status:** Draft | In Review | Accepted | Rejected
**Created:** [date]
### Summary
[1-2 paragraph overview]
### Motivation
[Why are we doing this?]
### Detailed Design
[Technical details]
### Alternatives Considered
| Option | Pros | Cons | Why Not |
|--------|------|------|---------|
### Migration/Rollout
[How we get from here to there]
### Open Questions
- [ ] [Question]
```
## Handoff Artifact
When spec is complete, produce final summary for task decomposition:
```
# [Feature Name] - Implementation Spec
**Status:** Ready for task breakdown
**Effort:** [total estimate]
**Approved by:** [human who approved]
**Date:** [date]
## Deliverables (Ordered)
1. **[D1]** (S) - [one-line description]
- Depends on: -
- Files likely touched: [paths]
2. **[D2]** (M) - [one-line description]
- Depends on: D1
- Files likely touched: [paths]
## Key Technical Decisions
- [Decision]: [choice] because [reason]
## Data Model
[Copy from spec]
## Acceptance Criteria
1. [Criterion 1]
2. [Criterion 2]
## Open Items (Non-Blocking)
- [Item] -> Owner: [who]
---
*Spec approved for task decomposition.*
```

File diff suppressed because it is too large Load Diff

View File

@@ -1,136 +0,0 @@
---
name: vercel-react-best-practices
description: React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.
license: MIT
metadata:
author: vercel
version: "1.0.0"
---
# Vercel React Best Practices
Comprehensive performance optimization guide for React and Next.js applications, maintained by Vercel. Contains 57 rules across 8 categories, prioritized by impact to guide automated refactoring and code generation.
## When to Apply
Reference these guidelines when:
- Writing new React components or Next.js pages
- Implementing data fetching (client or server-side)
- Reviewing code for performance issues
- Refactoring existing React/Next.js code
- Optimizing bundle size or load times
## Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|----------|----------|--------|--------|
| 1 | Eliminating Waterfalls | CRITICAL | `async-` |
| 2 | Bundle Size Optimization | CRITICAL | `bundle-` |
| 3 | Server-Side Performance | HIGH | `server-` |
| 4 | Client-Side Data Fetching | MEDIUM-HIGH | `client-` |
| 5 | Re-render Optimization | MEDIUM | `rerender-` |
| 6 | Rendering Performance | MEDIUM | `rendering-` |
| 7 | JavaScript Performance | LOW-MEDIUM | `js-` |
| 8 | Advanced Patterns | LOW | `advanced-` |
## Quick Reference
### 1. Eliminating Waterfalls (CRITICAL)
- `async-defer-await` - Move await into branches where actually used
- `async-parallel` - Use Promise.all() for independent operations
- `async-dependencies` - Use better-all for partial dependencies
- `async-api-routes` - Start promises early, await late in API routes
- `async-suspense-boundaries` - Use Suspense to stream content
### 2. Bundle Size Optimization (CRITICAL)
- `bundle-barrel-imports` - Import directly, avoid barrel files
- `bundle-dynamic-imports` - Use next/dynamic for heavy components
- `bundle-defer-third-party` - Load analytics/logging after hydration
- `bundle-conditional` - Load modules only when feature is activated
- `bundle-preload` - Preload on hover/focus for perceived speed
### 3. Server-Side Performance (HIGH)
- `server-auth-actions` - Authenticate server actions like API routes
- `server-cache-react` - Use React.cache() for per-request deduplication
- `server-cache-lru` - Use LRU cache for cross-request caching
- `server-dedup-props` - Avoid duplicate serialization in RSC props
- `server-serialization` - Minimize data passed to client components
- `server-parallel-fetching` - Restructure components to parallelize fetches
- `server-after-nonblocking` - Use after() for non-blocking operations
### 4. Client-Side Data Fetching (MEDIUM-HIGH)
- `client-swr-dedup` - Use SWR for automatic request deduplication
- `client-event-listeners` - Deduplicate global event listeners
- `client-passive-event-listeners` - Use passive listeners for scroll
- `client-localstorage-schema` - Version and minimize localStorage data
### 5. Re-render Optimization (MEDIUM)
- `rerender-defer-reads` - Don't subscribe to state only used in callbacks
- `rerender-memo` - Extract expensive work into memoized components
- `rerender-memo-with-default-value` - Hoist default non-primitive props
- `rerender-dependencies` - Use primitive dependencies in effects
- `rerender-derived-state` - Subscribe to derived booleans, not raw values
- `rerender-derived-state-no-effect` - Derive state during render, not effects
- `rerender-functional-setstate` - Use functional setState for stable callbacks
- `rerender-lazy-state-init` - Pass function to useState for expensive values
- `rerender-simple-expression-in-memo` - Avoid memo for simple primitives
- `rerender-move-effect-to-event` - Put interaction logic in event handlers
- `rerender-transitions` - Use startTransition for non-urgent updates
- `rerender-use-ref-transient-values` - Use refs for transient frequent values
### 6. Rendering Performance (MEDIUM)
- `rendering-animate-svg-wrapper` - Animate div wrapper, not SVG element
- `rendering-content-visibility` - Use content-visibility for long lists
- `rendering-hoist-jsx` - Extract static JSX outside components
- `rendering-svg-precision` - Reduce SVG coordinate precision
- `rendering-hydration-no-flicker` - Use inline script for client-only data
- `rendering-hydration-suppress-warning` - Suppress expected mismatches
- `rendering-activity` - Use Activity component for show/hide
- `rendering-conditional-render` - Use ternary, not && for conditionals
- `rendering-usetransition-loading` - Prefer useTransition for loading state
### 7. JavaScript Performance (LOW-MEDIUM)
- `js-batch-dom-css` - Group CSS changes via classes or cssText
- `js-index-maps` - Build Map for repeated lookups
- `js-cache-property-access` - Cache object properties in loops
- `js-cache-function-results` - Cache function results in module-level Map
- `js-cache-storage` - Cache localStorage/sessionStorage reads
- `js-combine-iterations` - Combine multiple filter/map into one loop
- `js-length-check-first` - Check array length before expensive comparison
- `js-early-exit` - Return early from functions
- `js-hoist-regexp` - Hoist RegExp creation outside loops
- `js-min-max-loop` - Use loop for min/max instead of sort
- `js-set-map-lookups` - Use Set/Map for O(1) lookups
- `js-tosorted-immutable` - Use toSorted() for immutability
### 8. Advanced Patterns (LOW)
- `advanced-event-handler-refs` - Store event handlers in refs
- `advanced-init-once` - Initialize app once per app load
- `advanced-use-latest` - useLatest for stable callback refs
## How to Use
Read individual rule files for detailed explanations and code examples:
```
rules/async-parallel.md
rules/bundle-barrel-imports.md
```
Each rule file contains:
- Brief explanation of why it matters
- Incorrect code example with explanation
- Correct code example with explanation
- Additional context and references
## Full Compiled Document
For the complete guide with all rules expanded: `AGENTS.md`

View File

@@ -1,46 +0,0 @@
# Sections
This file defines all sections, their ordering, impact levels, and descriptions.
The section ID (in parentheses) is the filename prefix used to group rules.
---
## 1. Eliminating Waterfalls (async)
**Impact:** CRITICAL
**Description:** Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains.
## 2. Bundle Size Optimization (bundle)
**Impact:** CRITICAL
**Description:** Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint.
## 3. Server-Side Performance (server)
**Impact:** HIGH
**Description:** Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times.
## 4. Client-Side Data Fetching (client)
**Impact:** MEDIUM-HIGH
**Description:** Automatic deduplication and efficient data fetching patterns reduce redundant network requests.
## 5. Re-render Optimization (rerender)
**Impact:** MEDIUM
**Description:** Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness.
## 6. Rendering Performance (rendering)
**Impact:** MEDIUM
**Description:** Optimizing the rendering process reduces the work the browser needs to do.
## 7. JavaScript Performance (js)
**Impact:** LOW-MEDIUM
**Description:** Micro-optimizations for hot paths can add up to meaningful improvements.
## 8. Advanced Patterns (advanced)
**Impact:** LOW
**Description:** Advanced patterns for specific cases that require careful implementation.

View File

@@ -1,28 +0,0 @@
---
title: Rule Title Here
impact: MEDIUM
impactDescription: Optional description of impact (e.g., "20-50% improvement")
tags: tag1, tag2
---
## Rule Title Here
**Impact: MEDIUM (optional impact description)**
Brief explanation of the rule and why it matters. This should be clear and concise, explaining the performance implications.
**Incorrect (description of what's wrong):**
```typescript
// Bad code example here
const bad = example()
```
**Correct (description of what's right):**
```typescript
// Good code example here
const good = example()
```
Reference: [Link to documentation or resource](https://example.com)

View File

@@ -1,55 +0,0 @@
---
title: Store Event Handlers in Refs
impact: LOW
impactDescription: stable subscriptions
tags: advanced, hooks, refs, event-handlers, optimization
---
## Store Event Handlers in Refs
Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.
**Incorrect (re-subscribes on every render):**
```tsx
function useWindowEvent(event: string, handler: (e) => void) {
useEffect(() => {
window.addEventListener(event, handler)
return () => window.removeEventListener(event, handler)
}, [event, handler])
}
```
**Correct (stable subscription):**
```tsx
function useWindowEvent(event: string, handler: (e) => void) {
const handlerRef = useRef(handler)
useEffect(() => {
handlerRef.current = handler
}, [handler])
useEffect(() => {
const listener = (e) => handlerRef.current(e)
window.addEventListener(event, listener)
return () => window.removeEventListener(event, listener)
}, [event])
}
```
**Alternative: use `useEffectEvent` if you're on latest React:**
```tsx
import { useEffectEvent } from 'react'
function useWindowEvent(event: string, handler: (e) => void) {
const onEvent = useEffectEvent(handler)
useEffect(() => {
window.addEventListener(event, onEvent)
return () => window.removeEventListener(event, onEvent)
}, [event])
}
```
`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.

View File

@@ -1,42 +0,0 @@
---
title: Initialize App Once, Not Per Mount
impact: LOW-MEDIUM
impactDescription: avoids duplicate init in development
tags: initialization, useEffect, app-startup, side-effects
---
## Initialize App Once, Not Per Mount
Do not put app-wide initialization that must run once per app load inside `useEffect([])` of a component. Components can remount and effects will re-run. Use a module-level guard or top-level init in the entry module instead.
**Incorrect (runs twice in dev, re-runs on remount):**
```tsx
function Comp() {
useEffect(() => {
loadFromStorage()
checkAuthToken()
}, [])
// ...
}
```
**Correct (once per app load):**
```tsx
let didInit = false
function Comp() {
useEffect(() => {
if (didInit) return
didInit = true
loadFromStorage()
checkAuthToken()
}, [])
// ...
}
```
Reference: [Initializing the application](https://react.dev/learn/you-might-not-need-an-effect#initializing-the-application)

View File

@@ -1,39 +0,0 @@
---
title: useEffectEvent for Stable Callback Refs
impact: LOW
impactDescription: prevents effect re-runs
tags: advanced, hooks, useEffectEvent, refs, optimization
---
## useEffectEvent for Stable Callback Refs
Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.
**Incorrect (effect re-runs on every callback change):**
```tsx
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
const [query, setQuery] = useState('')
useEffect(() => {
const timeout = setTimeout(() => onSearch(query), 300)
return () => clearTimeout(timeout)
}, [query, onSearch])
}
```
**Correct (using React's useEffectEvent):**
```tsx
import { useEffectEvent } from 'react';
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
const [query, setQuery] = useState('')
const onSearchEvent = useEffectEvent(onSearch)
useEffect(() => {
const timeout = setTimeout(() => onSearchEvent(query), 300)
return () => clearTimeout(timeout)
}, [query])
}
```

View File

@@ -1,38 +0,0 @@
---
title: Prevent Waterfall Chains in API Routes
impact: CRITICAL
impactDescription: 2-10× improvement
tags: api-routes, server-actions, waterfalls, parallelization
---
## Prevent Waterfall Chains in API Routes
In API routes and Server Actions, start independent operations immediately, even if you don't await them yet.
**Incorrect (config waits for auth, data waits for both):**
```typescript
export async function GET(request: Request) {
const session = await auth()
const config = await fetchConfig()
const data = await fetchData(session.user.id)
return Response.json({ data, config })
}
```
**Correct (auth and config start immediately):**
```typescript
export async function GET(request: Request) {
const sessionPromise = auth()
const configPromise = fetchConfig()
const session = await sessionPromise
const [config, data] = await Promise.all([
configPromise,
fetchData(session.user.id)
])
return Response.json({ data, config })
}
```
For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization).

View File

@@ -1,80 +0,0 @@
---
title: Defer Await Until Needed
impact: HIGH
impactDescription: avoids blocking unused code paths
tags: async, await, conditional, optimization
---
## Defer Await Until Needed
Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them.
**Incorrect (blocks both branches):**
```typescript
async function handleRequest(userId: string, skipProcessing: boolean) {
const userData = await fetchUserData(userId)
if (skipProcessing) {
// Returns immediately but still waited for userData
return { skipped: true }
}
// Only this branch uses userData
return processUserData(userData)
}
```
**Correct (only blocks when needed):**
```typescript
async function handleRequest(userId: string, skipProcessing: boolean) {
if (skipProcessing) {
// Returns immediately without waiting
return { skipped: true }
}
// Fetch only when needed
const userData = await fetchUserData(userId)
return processUserData(userData)
}
```
**Another example (early return optimization):**
```typescript
// Incorrect: always fetches permissions
async function updateResource(resourceId: string, userId: string) {
const permissions = await fetchPermissions(userId)
const resource = await getResource(resourceId)
if (!resource) {
return { error: 'Not found' }
}
if (!permissions.canEdit) {
return { error: 'Forbidden' }
}
return await updateResourceData(resource, permissions)
}
// Correct: fetches only when needed
async function updateResource(resourceId: string, userId: string) {
const resource = await getResource(resourceId)
if (!resource) {
return { error: 'Not found' }
}
const permissions = await fetchPermissions(userId)
if (!permissions.canEdit) {
return { error: 'Forbidden' }
}
return await updateResourceData(resource, permissions)
}
```
This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive.

View File

@@ -1,51 +0,0 @@
---
title: Dependency-Based Parallelization
impact: CRITICAL
impactDescription: 2-10× improvement
tags: async, parallelization, dependencies, better-all
---
## Dependency-Based Parallelization
For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment.
**Incorrect (profile waits for config unnecessarily):**
```typescript
const [user, config] = await Promise.all([
fetchUser(),
fetchConfig()
])
const profile = await fetchProfile(user.id)
```
**Correct (config and profile run in parallel):**
```typescript
import { all } from 'better-all'
const { user, config, profile } = await all({
async user() { return fetchUser() },
async config() { return fetchConfig() },
async profile() {
return fetchProfile((await this.$.user).id)
}
})
```
**Alternative without extra dependencies:**
We can also create all the promises first, and do `Promise.all()` at the end.
```typescript
const userPromise = fetchUser()
const profilePromise = userPromise.then(user => fetchProfile(user.id))
const [user, config, profile] = await Promise.all([
userPromise,
fetchConfig(),
profilePromise
])
```
Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)

View File

@@ -1,28 +0,0 @@
---
title: Promise.all() for Independent Operations
impact: CRITICAL
impactDescription: 2-10× improvement
tags: async, parallelization, promises, waterfalls
---
## Promise.all() for Independent Operations
When async operations have no interdependencies, execute them concurrently using `Promise.all()`.
**Incorrect (sequential execution, 3 round trips):**
```typescript
const user = await fetchUser()
const posts = await fetchPosts()
const comments = await fetchComments()
```
**Correct (parallel execution, 1 round trip):**
```typescript
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments()
])
```

View File

@@ -1,99 +0,0 @@
---
title: Strategic Suspense Boundaries
impact: HIGH
impactDescription: faster initial paint
tags: async, suspense, streaming, layout-shift
---
## Strategic Suspense Boundaries
Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads.
**Incorrect (wrapper blocked by data fetching):**
```tsx
async function Page() {
const data = await fetchData() // Blocks entire page
return (
<div>
<div>Sidebar</div>
<div>Header</div>
<div>
<DataDisplay data={data} />
</div>
<div>Footer</div>
</div>
)
}
```
The entire layout waits for data even though only the middle section needs it.
**Correct (wrapper shows immediately, data streams in):**
```tsx
function Page() {
return (
<div>
<div>Sidebar</div>
<div>Header</div>
<div>
<Suspense fallback={<Skeleton />}>
<DataDisplay />
</Suspense>
</div>
<div>Footer</div>
</div>
)
}
async function DataDisplay() {
const data = await fetchData() // Only blocks this component
return <div>{data.content}</div>
}
```
Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data.
**Alternative (share promise across components):**
```tsx
function Page() {
// Start fetch immediately, but don't await
const dataPromise = fetchData()
return (
<div>
<div>Sidebar</div>
<div>Header</div>
<Suspense fallback={<Skeleton />}>
<DataDisplay dataPromise={dataPromise} />
<DataSummary dataPromise={dataPromise} />
</Suspense>
<div>Footer</div>
</div>
)
}
function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {
const data = use(dataPromise) // Unwraps the promise
return <div>{data.content}</div>
}
function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {
const data = use(dataPromise) // Reuses the same promise
return <div>{data.summary}</div>
}
```
Both components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together.
**When NOT to use this pattern:**
- Critical data needed for layout decisions (affects positioning)
- SEO-critical content above the fold
- Small, fast queries where suspense overhead isn't worth it
- When you want to avoid layout shift (loading → content jump)
**Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities.

View File

@@ -1,59 +0,0 @@
---
title: Avoid Barrel File Imports
impact: CRITICAL
impactDescription: 200-800ms import cost, slow builds
tags: bundle, imports, tree-shaking, barrel-files, performance
---
## Avoid Barrel File Imports
Import directly from source files instead of barrel files to avoid loading thousands of unused modules. **Barrel files** are entry points that re-export multiple modules (e.g., `index.js` that does `export * from './module'`).
Popular icon and component libraries can have **up to 10,000 re-exports** in their entry file. For many React packages, **it takes 200-800ms just to import them**, affecting both development speed and production cold starts.
**Why tree-shaking doesn't help:** When a library is marked as external (not bundled), the bundler can't optimize it. If you bundle it to enable tree-shaking, builds become substantially slower analyzing the entire module graph.
**Incorrect (imports entire library):**
```tsx
import { Check, X, Menu } from 'lucide-react'
// Loads 1,583 modules, takes ~2.8s extra in dev
// Runtime cost: 200-800ms on every cold start
import { Button, TextField } from '@mui/material'
// Loads 2,225 modules, takes ~4.2s extra in dev
```
**Correct (imports only what you need):**
```tsx
import Check from 'lucide-react/dist/esm/icons/check'
import X from 'lucide-react/dist/esm/icons/x'
import Menu from 'lucide-react/dist/esm/icons/menu'
// Loads only 3 modules (~2KB vs ~1MB)
import Button from '@mui/material/Button'
import TextField from '@mui/material/TextField'
// Loads only what you use
```
**Alternative (Next.js 13.5+):**
```js
// next.config.js - use optimizePackageImports
module.exports = {
experimental: {
optimizePackageImports: ['lucide-react', '@mui/material']
}
}
// Then you can keep the ergonomic barrel imports:
import { Check, X, Menu } from 'lucide-react'
// Automatically transformed to direct imports at build time
```
Direct imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR.
Libraries commonly affected: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@headlessui/react`, `@radix-ui/react-*`, `lodash`, `ramda`, `date-fns`, `rxjs`, `react-use`.
Reference: [How we optimized package imports in Next.js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)

View File

@@ -1,31 +0,0 @@
---
title: Conditional Module Loading
impact: HIGH
impactDescription: loads large data only when needed
tags: bundle, conditional-loading, lazy-loading
---
## Conditional Module Loading
Load large data or modules only when a feature is activated.
**Example (lazy-load animation frames):**
```tsx
function AnimationPlayer({ enabled, setEnabled }: { enabled: boolean; setEnabled: React.Dispatch<React.SetStateAction<boolean>> }) {
const [frames, setFrames] = useState<Frame[] | null>(null)
useEffect(() => {
if (enabled && !frames && typeof window !== 'undefined') {
import('./animation-frames.js')
.then(mod => setFrames(mod.frames))
.catch(() => setEnabled(false))
}
}, [enabled, frames, setEnabled])
if (!frames) return <Skeleton />
return <Canvas frames={frames} />
}
```
The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed.

View File

@@ -1,49 +0,0 @@
---
title: Defer Non-Critical Third-Party Libraries
impact: MEDIUM
impactDescription: loads after hydration
tags: bundle, third-party, analytics, defer
---
## Defer Non-Critical Third-Party Libraries
Analytics, logging, and error tracking don't block user interaction. Load them after hydration.
**Incorrect (blocks initial bundle):**
```tsx
import { Analytics } from '@vercel/analytics/react'
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Analytics />
</body>
</html>
)
}
```
**Correct (loads after hydration):**
```tsx
import dynamic from 'next/dynamic'
const Analytics = dynamic(
() => import('@vercel/analytics/react').then(m => m.Analytics),
{ ssr: false }
)
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Analytics />
</body>
</html>
)
}
```

View File

@@ -1,35 +0,0 @@
---
title: Dynamic Imports for Heavy Components
impact: CRITICAL
impactDescription: directly affects TTI and LCP
tags: bundle, dynamic-import, code-splitting, next-dynamic
---
## Dynamic Imports for Heavy Components
Use `next/dynamic` to lazy-load large components not needed on initial render.
**Incorrect (Monaco bundles with main chunk ~300KB):**
```tsx
import { MonacoEditor } from './monaco-editor'
function CodePanel({ code }: { code: string }) {
return <MonacoEditor value={code} />
}
```
**Correct (Monaco loads on demand):**
```tsx
import dynamic from 'next/dynamic'
const MonacoEditor = dynamic(
() => import('./monaco-editor').then(m => m.MonacoEditor),
{ ssr: false }
)
function CodePanel({ code }: { code: string }) {
return <MonacoEditor value={code} />
}
```

View File

@@ -1,50 +0,0 @@
---
title: Preload Based on User Intent
impact: MEDIUM
impactDescription: reduces perceived latency
tags: bundle, preload, user-intent, hover
---
## Preload Based on User Intent
Preload heavy bundles before they're needed to reduce perceived latency.
**Example (preload on hover/focus):**
```tsx
function EditorButton({ onClick }: { onClick: () => void }) {
const preload = () => {
if (typeof window !== 'undefined') {
void import('./monaco-editor')
}
}
return (
<button
onMouseEnter={preload}
onFocus={preload}
onClick={onClick}
>
Open Editor
</button>
)
}
```
**Example (preload when feature flag is enabled):**
```tsx
function FlagsProvider({ children, flags }: Props) {
useEffect(() => {
if (flags.editorEnabled && typeof window !== 'undefined') {
void import('./monaco-editor').then(mod => mod.init())
}
}, [flags.editorEnabled])
return <FlagsContext.Provider value={flags}>
{children}
</FlagsContext.Provider>
}
```
The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed.

View File

@@ -1,74 +0,0 @@
---
title: Deduplicate Global Event Listeners
impact: LOW
impactDescription: single listener for N components
tags: client, swr, event-listeners, subscription
---
## Deduplicate Global Event Listeners
Use `useSWRSubscription()` to share global event listeners across component instances.
**Incorrect (N instances = N listeners):**
```tsx
function useKeyboardShortcut(key: string, callback: () => void) {
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.metaKey && e.key === key) {
callback()
}
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
}, [key, callback])
}
```
When using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener.
**Correct (N instances = 1 listener):**
```tsx
import useSWRSubscription from 'swr/subscription'
// Module-level Map to track callbacks per key
const keyCallbacks = new Map<string, Set<() => void>>()
function useKeyboardShortcut(key: string, callback: () => void) {
// Register this callback in the Map
useEffect(() => {
if (!keyCallbacks.has(key)) {
keyCallbacks.set(key, new Set())
}
keyCallbacks.get(key)!.add(callback)
return () => {
const set = keyCallbacks.get(key)
if (set) {
set.delete(callback)
if (set.size === 0) {
keyCallbacks.delete(key)
}
}
}
}, [key, callback])
useSWRSubscription('global-keydown', () => {
const handler = (e: KeyboardEvent) => {
if (e.metaKey && keyCallbacks.has(e.key)) {
keyCallbacks.get(e.key)!.forEach(cb => cb())
}
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
})
}
function Profile() {
// Multiple shortcuts will share the same listener
useKeyboardShortcut('p', () => { /* ... */ })
useKeyboardShortcut('k', () => { /* ... */ })
// ...
}
```

View File

@@ -1,71 +0,0 @@
---
title: Version and Minimize localStorage Data
impact: MEDIUM
impactDescription: prevents schema conflicts, reduces storage size
tags: client, localStorage, storage, versioning, data-minimization
---
## Version and Minimize localStorage Data
Add version prefix to keys and store only needed fields. Prevents schema conflicts and accidental storage of sensitive data.
**Incorrect:**
```typescript
// No version, stores everything, no error handling
localStorage.setItem('userConfig', JSON.stringify(fullUserObject))
const data = localStorage.getItem('userConfig')
```
**Correct:**
```typescript
const VERSION = 'v2'
function saveConfig(config: { theme: string; language: string }) {
try {
localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config))
} catch {
// Throws in incognito/private browsing, quota exceeded, or disabled
}
}
function loadConfig() {
try {
const data = localStorage.getItem(`userConfig:${VERSION}`)
return data ? JSON.parse(data) : null
} catch {
return null
}
}
// Migration from v1 to v2
function migrate() {
try {
const v1 = localStorage.getItem('userConfig:v1')
if (v1) {
const old = JSON.parse(v1)
saveConfig({ theme: old.darkMode ? 'dark' : 'light', language: old.lang })
localStorage.removeItem('userConfig:v1')
}
} catch {}
}
```
**Store minimal fields from server responses:**
```typescript
// User object has 20+ fields, only store what UI needs
function cachePrefs(user: FullUser) {
try {
localStorage.setItem('prefs:v1', JSON.stringify({
theme: user.preferences.theme,
notifications: user.preferences.notifications
}))
} catch {}
}
```
**Always wrap in try-catch:** `getItem()` and `setItem()` throw in incognito/private browsing (Safari, Firefox), when quota exceeded, or when disabled.
**Benefits:** Schema evolution via versioning, reduced storage size, prevents storing tokens/PII/internal flags.

View File

@@ -1,48 +0,0 @@
---
title: Use Passive Event Listeners for Scrolling Performance
impact: MEDIUM
impactDescription: eliminates scroll delay caused by event listeners
tags: client, event-listeners, scrolling, performance, touch, wheel
---
## Use Passive Event Listeners for Scrolling Performance
Add `{ passive: true }` to touch and wheel event listeners to enable immediate scrolling. Browsers normally wait for listeners to finish to check if `preventDefault()` is called, causing scroll delay.
**Incorrect:**
```typescript
useEffect(() => {
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)
const handleWheel = (e: WheelEvent) => console.log(e.deltaY)
document.addEventListener('touchstart', handleTouch)
document.addEventListener('wheel', handleWheel)
return () => {
document.removeEventListener('touchstart', handleTouch)
document.removeEventListener('wheel', handleWheel)
}
}, [])
```
**Correct:**
```typescript
useEffect(() => {
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)
const handleWheel = (e: WheelEvent) => console.log(e.deltaY)
document.addEventListener('touchstart', handleTouch, { passive: true })
document.addEventListener('wheel', handleWheel, { passive: true })
return () => {
document.removeEventListener('touchstart', handleTouch)
document.removeEventListener('wheel', handleWheel)
}
}, [])
```
**Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`.
**Don't use passive when:** implementing custom swipe gestures, custom zoom controls, or any listener that needs `preventDefault()`.

View File

@@ -1,56 +0,0 @@
---
title: Use SWR for Automatic Deduplication
impact: MEDIUM-HIGH
impactDescription: automatic deduplication
tags: client, swr, deduplication, data-fetching
---
## Use SWR for Automatic Deduplication
SWR enables request deduplication, caching, and revalidation across component instances.
**Incorrect (no deduplication, each instance fetches):**
```tsx
function UserList() {
const [users, setUsers] = useState([])
useEffect(() => {
fetch('/api/users')
.then(r => r.json())
.then(setUsers)
}, [])
}
```
**Correct (multiple instances share one request):**
```tsx
import useSWR from 'swr'
function UserList() {
const { data: users } = useSWR('/api/users', fetcher)
}
```
**For immutable data:**
```tsx
import { useImmutableSWR } from '@/lib/swr'
function StaticContent() {
const { data } = useImmutableSWR('/api/config', fetcher)
}
```
**For mutations:**
```tsx
import { useSWRMutation } from 'swr/mutation'
function UpdateButton() {
const { trigger } = useSWRMutation('/api/user', updateUser)
return <button onClick={() => trigger()}>Update</button>
}
```
Reference: [https://swr.vercel.app](https://swr.vercel.app)

View File

@@ -1,107 +0,0 @@
---
title: Avoid Layout Thrashing
impact: MEDIUM
impactDescription: prevents forced synchronous layouts and reduces performance bottlenecks
tags: javascript, dom, css, performance, reflow, layout-thrashing
---
## Avoid Layout Thrashing
Avoid interleaving style writes with layout reads. When you read a layout property (like `offsetWidth`, `getBoundingClientRect()`, or `getComputedStyle()`) between style changes, the browser is forced to trigger a synchronous reflow.
**This is OK (browser batches style changes):**
```typescript
function updateElementStyles(element: HTMLElement) {
// Each line invalidates style, but browser batches the recalculation
element.style.width = '100px'
element.style.height = '200px'
element.style.backgroundColor = 'blue'
element.style.border = '1px solid black'
}
```
**Incorrect (interleaved reads and writes force reflows):**
```typescript
function layoutThrashing(element: HTMLElement) {
element.style.width = '100px'
const width = element.offsetWidth // Forces reflow
element.style.height = '200px'
const height = element.offsetHeight // Forces another reflow
}
```
**Correct (batch writes, then read once):**
```typescript
function updateElementStyles(element: HTMLElement) {
// Batch all writes together
element.style.width = '100px'
element.style.height = '200px'
element.style.backgroundColor = 'blue'
element.style.border = '1px solid black'
// Read after all writes are done (single reflow)
const { width, height } = element.getBoundingClientRect()
}
```
**Correct (batch reads, then writes):**
```typescript
function avoidThrashing(element: HTMLElement) {
// Read phase - all layout queries first
const rect1 = element.getBoundingClientRect()
const offsetWidth = element.offsetWidth
const offsetHeight = element.offsetHeight
// Write phase - all style changes after
element.style.width = '100px'
element.style.height = '200px'
}
```
**Better: use CSS classes**
```css
.highlighted-box {
width: 100px;
height: 200px;
background-color: blue;
border: 1px solid black;
}
```
```typescript
function updateElementStyles(element: HTMLElement) {
element.classList.add('highlighted-box')
const { width, height } = element.getBoundingClientRect()
}
```
**React example:**
```tsx
// Incorrect: interleaving style changes with layout queries
function Box({ isHighlighted }: { isHighlighted: boolean }) {
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
if (ref.current && isHighlighted) {
ref.current.style.width = '100px'
const width = ref.current.offsetWidth // Forces layout
ref.current.style.height = '200px'
}
}, [isHighlighted])
return <div ref={ref}>Content</div>
}
// Correct: toggle class
function Box({ isHighlighted }: { isHighlighted: boolean }) {
return (
<div className={isHighlighted ? 'highlighted-box' : ''}>
Content
</div>
)
}
```
Prefer CSS classes over inline styles when possible. CSS files are cached by the browser, and classes provide better separation of concerns and are easier to maintain.
See [this gist](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) and [CSS Triggers](https://csstriggers.com/) for more information on layout-forcing operations.

View File

@@ -1,80 +0,0 @@
---
title: Cache Repeated Function Calls
impact: MEDIUM
impactDescription: avoid redundant computation
tags: javascript, cache, memoization, performance
---
## Cache Repeated Function Calls
Use a module-level Map to cache function results when the same function is called repeatedly with the same inputs during render.
**Incorrect (redundant computation):**
```typescript
function ProjectList({ projects }: { projects: Project[] }) {
return (
<div>
{projects.map(project => {
// slugify() called 100+ times for same project names
const slug = slugify(project.name)
return <ProjectCard key={project.id} slug={slug} />
})}
</div>
)
}
```
**Correct (cached results):**
```typescript
// Module-level cache
const slugifyCache = new Map<string, string>()
function cachedSlugify(text: string): string {
if (slugifyCache.has(text)) {
return slugifyCache.get(text)!
}
const result = slugify(text)
slugifyCache.set(text, result)
return result
}
function ProjectList({ projects }: { projects: Project[] }) {
return (
<div>
{projects.map(project => {
// Computed only once per unique project name
const slug = cachedSlugify(project.name)
return <ProjectCard key={project.id} slug={slug} />
})}
</div>
)
}
```
**Simpler pattern for single-value functions:**
```typescript
let isLoggedInCache: boolean | null = null
function isLoggedIn(): boolean {
if (isLoggedInCache !== null) {
return isLoggedInCache
}
isLoggedInCache = document.cookie.includes('auth=')
return isLoggedInCache
}
// Clear cache when auth changes
function onAuthChange() {
isLoggedInCache = null
}
```
Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.
Reference: [How we made the Vercel Dashboard twice as fast](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast)

View File

@@ -1,28 +0,0 @@
---
title: Cache Property Access in Loops
impact: LOW-MEDIUM
impactDescription: reduces lookups
tags: javascript, loops, optimization, caching
---
## Cache Property Access in Loops
Cache object property lookups in hot paths.
**Incorrect (3 lookups × N iterations):**
```typescript
for (let i = 0; i < arr.length; i++) {
process(obj.config.settings.value)
}
```
**Correct (1 lookup total):**
```typescript
const value = obj.config.settings.value
const len = arr.length
for (let i = 0; i < len; i++) {
process(value)
}
```

View File

@@ -1,70 +0,0 @@
---
title: Cache Storage API Calls
impact: LOW-MEDIUM
impactDescription: reduces expensive I/O
tags: javascript, localStorage, storage, caching, performance
---
## Cache Storage API Calls
`localStorage`, `sessionStorage`, and `document.cookie` are synchronous and expensive. Cache reads in memory.
**Incorrect (reads storage on every call):**
```typescript
function getTheme() {
return localStorage.getItem('theme') ?? 'light'
}
// Called 10 times = 10 storage reads
```
**Correct (Map cache):**
```typescript
const storageCache = new Map<string, string | null>()
function getLocalStorage(key: string) {
if (!storageCache.has(key)) {
storageCache.set(key, localStorage.getItem(key))
}
return storageCache.get(key)
}
function setLocalStorage(key: string, value: string) {
localStorage.setItem(key, value)
storageCache.set(key, value) // keep cache in sync
}
```
Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.
**Cookie caching:**
```typescript
let cookieCache: Record<string, string> | null = null
function getCookie(name: string) {
if (!cookieCache) {
cookieCache = Object.fromEntries(
document.cookie.split('; ').map(c => c.split('='))
)
}
return cookieCache[name]
}
```
**Important (invalidate on external changes):**
If storage can change externally (another tab, server-set cookies), invalidate cache:
```typescript
window.addEventListener('storage', (e) => {
if (e.key) storageCache.delete(e.key)
})
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
storageCache.clear()
}
})
```

View File

@@ -1,32 +0,0 @@
---
title: Combine Multiple Array Iterations
impact: LOW-MEDIUM
impactDescription: reduces iterations
tags: javascript, arrays, loops, performance
---
## Combine Multiple Array Iterations
Multiple `.filter()` or `.map()` calls iterate the array multiple times. Combine into one loop.
**Incorrect (3 iterations):**
```typescript
const admins = users.filter(u => u.isAdmin)
const testers = users.filter(u => u.isTester)
const inactive = users.filter(u => !u.isActive)
```
**Correct (1 iteration):**
```typescript
const admins: User[] = []
const testers: User[] = []
const inactive: User[] = []
for (const user of users) {
if (user.isAdmin) admins.push(user)
if (user.isTester) testers.push(user)
if (!user.isActive) inactive.push(user)
}
```

View File

@@ -1,50 +0,0 @@
---
title: Early Return from Functions
impact: LOW-MEDIUM
impactDescription: avoids unnecessary computation
tags: javascript, functions, optimization, early-return
---
## Early Return from Functions
Return early when result is determined to skip unnecessary processing.
**Incorrect (processes all items even after finding answer):**
```typescript
function validateUsers(users: User[]) {
let hasError = false
let errorMessage = ''
for (const user of users) {
if (!user.email) {
hasError = true
errorMessage = 'Email required'
}
if (!user.name) {
hasError = true
errorMessage = 'Name required'
}
// Continues checking all users even after error found
}
return hasError ? { valid: false, error: errorMessage } : { valid: true }
}
```
**Correct (returns immediately on first error):**
```typescript
function validateUsers(users: User[]) {
for (const user of users) {
if (!user.email) {
return { valid: false, error: 'Email required' }
}
if (!user.name) {
return { valid: false, error: 'Name required' }
}
}
return { valid: true }
}
```

View File

@@ -1,45 +0,0 @@
---
title: Hoist RegExp Creation
impact: LOW-MEDIUM
impactDescription: avoids recreation
tags: javascript, regexp, optimization, memoization
---
## Hoist RegExp Creation
Don't create RegExp inside render. Hoist to module scope or memoize with `useMemo()`.
**Incorrect (new RegExp every render):**
```tsx
function Highlighter({ text, query }: Props) {
const regex = new RegExp(`(${query})`, 'gi')
const parts = text.split(regex)
return <>{parts.map((part, i) => ...)}</>
}
```
**Correct (memoize or hoist):**
```tsx
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
function Highlighter({ text, query }: Props) {
const regex = useMemo(
() => new RegExp(`(${escapeRegex(query)})`, 'gi'),
[query]
)
const parts = text.split(regex)
return <>{parts.map((part, i) => ...)}</>
}
```
**Warning (global regex has mutable state):**
Global regex (`/g`) has mutable `lastIndex` state:
```typescript
const regex = /foo/g
regex.test('foo') // true, lastIndex = 3
regex.test('foo') // false, lastIndex = 0
```

View File

@@ -1,37 +0,0 @@
---
title: Build Index Maps for Repeated Lookups
impact: LOW-MEDIUM
impactDescription: 1M ops to 2K ops
tags: javascript, map, indexing, optimization, performance
---
## Build Index Maps for Repeated Lookups
Multiple `.find()` calls by the same key should use a Map.
**Incorrect (O(n) per lookup):**
```typescript
function processOrders(orders: Order[], users: User[]) {
return orders.map(order => ({
...order,
user: users.find(u => u.id === order.userId)
}))
}
```
**Correct (O(1) per lookup):**
```typescript
function processOrders(orders: Order[], users: User[]) {
const userById = new Map(users.map(u => [u.id, u]))
return orders.map(order => ({
...order,
user: userById.get(order.userId)
}))
}
```
Build map once (O(n)), then all lookups are O(1).
For 1000 orders × 1000 users: 1M ops → 2K ops.

View File

@@ -1,49 +0,0 @@
---
title: Early Length Check for Array Comparisons
impact: MEDIUM-HIGH
impactDescription: avoids expensive operations when lengths differ
tags: javascript, arrays, performance, optimization, comparison
---
## Early Length Check for Array Comparisons
When comparing arrays with expensive operations (sorting, deep equality, serialization), check lengths first. If lengths differ, the arrays cannot be equal.
In real-world applications, this optimization is especially valuable when the comparison runs in hot paths (event handlers, render loops).
**Incorrect (always runs expensive comparison):**
```typescript
function hasChanges(current: string[], original: string[]) {
// Always sorts and joins, even when lengths differ
return current.sort().join() !== original.sort().join()
}
```
Two O(n log n) sorts run even when `current.length` is 5 and `original.length` is 100. There is also overhead of joining the arrays and comparing the strings.
**Correct (O(1) length check first):**
```typescript
function hasChanges(current: string[], original: string[]) {
// Early return if lengths differ
if (current.length !== original.length) {
return true
}
// Only sort when lengths match
const currentSorted = current.toSorted()
const originalSorted = original.toSorted()
for (let i = 0; i < currentSorted.length; i++) {
if (currentSorted[i] !== originalSorted[i]) {
return true
}
}
return false
}
```
This new approach is more efficient because:
- It avoids the overhead of sorting and joining the arrays when lengths differ
- It avoids consuming memory for the joined strings (especially important for large arrays)
- It avoids mutating the original arrays
- It returns early when a difference is found

View File

@@ -1,82 +0,0 @@
---
title: Use Loop for Min/Max Instead of Sort
impact: LOW
impactDescription: O(n) instead of O(n log n)
tags: javascript, arrays, performance, sorting, algorithms
---
## Use Loop for Min/Max Instead of Sort
Finding the smallest or largest element only requires a single pass through the array. Sorting is wasteful and slower.
**Incorrect (O(n log n) - sort to find latest):**
```typescript
interface Project {
id: string
name: string
updatedAt: number
}
function getLatestProject(projects: Project[]) {
const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt)
return sorted[0]
}
```
Sorts the entire array just to find the maximum value.
**Incorrect (O(n log n) - sort for oldest and newest):**
```typescript
function getOldestAndNewest(projects: Project[]) {
const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt)
return { oldest: sorted[0], newest: sorted[sorted.length - 1] }
}
```
Still sorts unnecessarily when only min/max are needed.
**Correct (O(n) - single loop):**
```typescript
function getLatestProject(projects: Project[]) {
if (projects.length === 0) return null
let latest = projects[0]
for (let i = 1; i < projects.length; i++) {
if (projects[i].updatedAt > latest.updatedAt) {
latest = projects[i]
}
}
return latest
}
function getOldestAndNewest(projects: Project[]) {
if (projects.length === 0) return { oldest: null, newest: null }
let oldest = projects[0]
let newest = projects[0]
for (let i = 1; i < projects.length; i++) {
if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i]
if (projects[i].updatedAt > newest.updatedAt) newest = projects[i]
}
return { oldest, newest }
}
```
Single pass through the array, no copying, no sorting.
**Alternative (Math.min/Math.max for small arrays):**
```typescript
const numbers = [5, 2, 8, 1, 9]
const min = Math.min(...numbers)
const max = Math.max(...numbers)
```
This works for small arrays, but can be slower or just throw an error for very large arrays due to spread operator limitations. Maximal array length is approximately 124000 in Chrome 143 and 638000 in Safari 18; exact numbers may vary - see [the fiddle](https://jsfiddle.net/qw1jabsx/4/). Use the loop approach for reliability.

View File

@@ -1,24 +0,0 @@
---
title: Use Set/Map for O(1) Lookups
impact: LOW-MEDIUM
impactDescription: O(n) to O(1)
tags: javascript, set, map, data-structures, performance
---
## Use Set/Map for O(1) Lookups
Convert arrays to Set/Map for repeated membership checks.
**Incorrect (O(n) per check):**
```typescript
const allowedIds = ['a', 'b', 'c', ...]
items.filter(item => allowedIds.includes(item.id))
```
**Correct (O(1) per check):**
```typescript
const allowedIds = new Set(['a', 'b', 'c', ...])
items.filter(item => allowedIds.has(item.id))
```

View File

@@ -1,57 +0,0 @@
---
title: Use toSorted() Instead of sort() for Immutability
impact: MEDIUM-HIGH
impactDescription: prevents mutation bugs in React state
tags: javascript, arrays, immutability, react, state, mutation
---
## Use toSorted() Instead of sort() for Immutability
`.sort()` mutates the array in place, which can cause bugs with React state and props. Use `.toSorted()` to create a new sorted array without mutation.
**Incorrect (mutates original array):**
```typescript
function UserList({ users }: { users: User[] }) {
// Mutates the users prop array!
const sorted = useMemo(
() => users.sort((a, b) => a.name.localeCompare(b.name)),
[users]
)
return <div>{sorted.map(renderUser)}</div>
}
```
**Correct (creates new array):**
```typescript
function UserList({ users }: { users: User[] }) {
// Creates new sorted array, original unchanged
const sorted = useMemo(
() => users.toSorted((a, b) => a.name.localeCompare(b.name)),
[users]
)
return <div>{sorted.map(renderUser)}</div>
}
```
**Why this matters in React:**
1. Props/state mutations break React's immutability model - React expects props and state to be treated as read-only
2. Causes stale closure bugs - Mutating arrays inside closures (callbacks, effects) can lead to unexpected behavior
**Browser support (fallback for older browsers):**
`.toSorted()` is available in all modern browsers (Chrome 110+, Safari 16+, Firefox 115+, Node.js 20+). For older environments, use spread operator:
```typescript
// Fallback for older browsers
const sorted = [...items].sort((a, b) => a.value - b.value)
```
**Other immutable array methods:**
- `.toSorted()` - immutable sort
- `.toReversed()` - immutable reverse
- `.toSpliced()` - immutable splice
- `.with()` - immutable element replacement

View File

@@ -1,26 +0,0 @@
---
title: Use Activity Component for Show/Hide
impact: MEDIUM
impactDescription: preserves state/DOM
tags: rendering, activity, visibility, state-preservation
---
## Use Activity Component for Show/Hide
Use React's `<Activity>` to preserve state/DOM for expensive components that frequently toggle visibility.
**Usage:**
```tsx
import { Activity } from 'react'
function Dropdown({ isOpen }: Props) {
return (
<Activity mode={isOpen ? 'visible' : 'hidden'}>
<ExpensiveMenu />
</Activity>
)
}
```
Avoids expensive re-renders and state loss.

View File

@@ -1,47 +0,0 @@
---
title: Animate SVG Wrapper Instead of SVG Element
impact: LOW
impactDescription: enables hardware acceleration
tags: rendering, svg, css, animation, performance
---
## Animate SVG Wrapper Instead of SVG Element
Many browsers don't have hardware acceleration for CSS3 animations on SVG elements. Wrap SVG in a `<div>` and animate the wrapper instead.
**Incorrect (animating SVG directly - no hardware acceleration):**
```tsx
function LoadingSpinner() {
return (
<svg
className="animate-spin"
width="24"
height="24"
viewBox="0 0 24 24"
>
<circle cx="12" cy="12" r="10" stroke="currentColor" />
</svg>
)
}
```
**Correct (animating wrapper div - hardware accelerated):**
```tsx
function LoadingSpinner() {
return (
<div className="animate-spin">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
>
<circle cx="12" cy="12" r="10" stroke="currentColor" />
</svg>
</div>
)
}
```
This applies to all CSS transforms and transitions (`transform`, `opacity`, `translate`, `scale`, `rotate`). The wrapper div allows browsers to use GPU acceleration for smoother animations.

View File

@@ -1,40 +0,0 @@
---
title: Use Explicit Conditional Rendering
impact: LOW
impactDescription: prevents rendering 0 or NaN
tags: rendering, conditional, jsx, falsy-values
---
## Use Explicit Conditional Rendering
Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render.
**Incorrect (renders "0" when count is 0):**
```tsx
function Badge({ count }: { count: number }) {
return (
<div>
{count && <span className="badge">{count}</span>}
</div>
)
}
// When count = 0, renders: <div>0</div>
// When count = 5, renders: <div><span class="badge">5</span></div>
```
**Correct (renders nothing when count is 0):**
```tsx
function Badge({ count }: { count: number }) {
return (
<div>
{count > 0 ? <span className="badge">{count}</span> : null}
</div>
)
}
// When count = 0, renders: <div></div>
// When count = 5, renders: <div><span class="badge">5</span></div>
```

View File

@@ -1,38 +0,0 @@
---
title: CSS content-visibility for Long Lists
impact: HIGH
impactDescription: faster initial render
tags: rendering, css, content-visibility, long-lists
---
## CSS content-visibility for Long Lists
Apply `content-visibility: auto` to defer off-screen rendering.
**CSS:**
```css
.message-item {
content-visibility: auto;
contain-intrinsic-size: 0 80px;
}
```
**Example:**
```tsx
function MessageList({ messages }: { messages: Message[] }) {
return (
<div className="overflow-y-auto h-screen">
{messages.map(msg => (
<div key={msg.id} className="message-item">
<Avatar user={msg.author} />
<div>{msg.content}</div>
</div>
))}
</div>
)
}
```
For 1000 messages, browser skips layout/paint for ~990 off-screen items (10× faster initial render).

View File

@@ -1,46 +0,0 @@
---
title: Hoist Static JSX Elements
impact: LOW
impactDescription: avoids re-creation
tags: rendering, jsx, static, optimization
---
## Hoist Static JSX Elements
Extract static JSX outside components to avoid re-creation.
**Incorrect (recreates element every render):**
```tsx
function LoadingSkeleton() {
return <div className="animate-pulse h-20 bg-gray-200" />
}
function Container() {
return (
<div>
{loading && <LoadingSkeleton />}
</div>
)
}
```
**Correct (reuses same element):**
```tsx
const loadingSkeleton = (
<div className="animate-pulse h-20 bg-gray-200" />
)
function Container() {
return (
<div>
{loading && loadingSkeleton}
</div>
)
}
```
This is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render.
**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler automatically hoists static JSX elements and optimizes component re-renders, making manual hoisting unnecessary.

View File

@@ -1,82 +0,0 @@
---
title: Prevent Hydration Mismatch Without Flickering
impact: MEDIUM
impactDescription: avoids visual flicker and hydration errors
tags: rendering, ssr, hydration, localStorage, flicker
---
## Prevent Hydration Mismatch Without Flickering
When rendering content that depends on client-side storage (localStorage, cookies), avoid both SSR breakage and post-hydration flickering by injecting a synchronous script that updates the DOM before React hydrates.
**Incorrect (breaks SSR):**
```tsx
function ThemeWrapper({ children }: { children: ReactNode }) {
// localStorage is not available on server - throws error
const theme = localStorage.getItem('theme') || 'light'
return (
<div className={theme}>
{children}
</div>
)
}
```
Server-side rendering will fail because `localStorage` is undefined.
**Incorrect (visual flickering):**
```tsx
function ThemeWrapper({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState('light')
useEffect(() => {
// Runs after hydration - causes visible flash
const stored = localStorage.getItem('theme')
if (stored) {
setTheme(stored)
}
}, [])
return (
<div className={theme}>
{children}
</div>
)
}
```
Component first renders with default value (`light`), then updates after hydration, causing a visible flash of incorrect content.
**Correct (no flicker, no hydration mismatch):**
```tsx
function ThemeWrapper({ children }: { children: ReactNode }) {
return (
<>
<div id="theme-wrapper">
{children}
</div>
<script
dangerouslySetInnerHTML={{
__html: `
(function() {
try {
var theme = localStorage.getItem('theme') || 'light';
var el = document.getElementById('theme-wrapper');
if (el) el.className = theme;
} catch (e) {}
})();
`,
}}
/>
</>
)
}
```
The inline script executes synchronously before showing the element, ensuring the DOM already has the correct value. No flickering, no hydration mismatch.
This pattern is especially useful for theme toggles, user preferences, authentication states, and any client-only data that should render immediately without flashing default values.

View File

@@ -1,30 +0,0 @@
---
title: Suppress Expected Hydration Mismatches
impact: LOW-MEDIUM
impactDescription: avoids noisy hydration warnings for known differences
tags: rendering, hydration, ssr, nextjs
---
## Suppress Expected Hydration Mismatches
In SSR frameworks (e.g., Next.js), some values are intentionally different on server vs client (random IDs, dates, locale/timezone formatting). For these *expected* mismatches, wrap the dynamic text in an element with `suppressHydrationWarning` to prevent noisy warnings. Do not use this to hide real bugs. Dont overuse it.
**Incorrect (known mismatch warnings):**
```tsx
function Timestamp() {
return <span>{new Date().toLocaleString()}</span>
}
```
**Correct (suppress expected mismatch only):**
```tsx
function Timestamp() {
return (
<span suppressHydrationWarning>
{new Date().toLocaleString()}
</span>
)
}
```

View File

@@ -1,28 +0,0 @@
---
title: Optimize SVG Precision
impact: LOW
impactDescription: reduces file size
tags: rendering, svg, optimization, svgo
---
## Optimize SVG Precision
Reduce SVG coordinate precision to decrease file size. The optimal precision depends on the viewBox size, but in general reducing precision should be considered.
**Incorrect (excessive precision):**
```svg
<path d="M 10.293847 20.847362 L 30.938472 40.192837" />
```
**Correct (1 decimal place):**
```svg
<path d="M 10.3 20.8 L 30.9 40.2" />
```
**Automate with SVGO:**
```bash
npx svgo --precision=1 --multipass icon.svg
```

View File

@@ -1,75 +0,0 @@
---
title: Use useTransition Over Manual Loading States
impact: LOW
impactDescription: reduces re-renders and improves code clarity
tags: rendering, transitions, useTransition, loading, state
---
## Use useTransition Over Manual Loading States
Use `useTransition` instead of manual `useState` for loading states. This provides built-in `isPending` state and automatically manages transitions.
**Incorrect (manual loading state):**
```tsx
function SearchResults() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [isLoading, setIsLoading] = useState(false)
const handleSearch = async (value: string) => {
setIsLoading(true)
setQuery(value)
const data = await fetchResults(value)
setResults(data)
setIsLoading(false)
}
return (
<>
<input onChange={(e) => handleSearch(e.target.value)} />
{isLoading && <Spinner />}
<ResultsList results={results} />
</>
)
}
```
**Correct (useTransition with built-in pending state):**
```tsx
import { useTransition, useState } from 'react'
function SearchResults() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [isPending, startTransition] = useTransition()
const handleSearch = (value: string) => {
setQuery(value) // Update input immediately
startTransition(async () => {
// Fetch and update results
const data = await fetchResults(value)
setResults(data)
})
}
return (
<>
<input onChange={(e) => handleSearch(e.target.value)} />
{isPending && <Spinner />}
<ResultsList results={results} />
</>
)
}
```
**Benefits:**
- **Automatic pending state**: No need to manually manage `setIsLoading(true/false)`
- **Error resilience**: Pending state correctly resets even if the transition throws
- **Better responsiveness**: Keeps the UI responsive during updates
- **Interrupt handling**: New transitions automatically cancel pending ones
Reference: [useTransition](https://react.dev/reference/react/useTransition)

View File

@@ -1,39 +0,0 @@
---
title: Defer State Reads to Usage Point
impact: MEDIUM
impactDescription: avoids unnecessary subscriptions
tags: rerender, searchParams, localStorage, optimization
---
## Defer State Reads to Usage Point
Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks.
**Incorrect (subscribes to all searchParams changes):**
```tsx
function ShareButton({ chatId }: { chatId: string }) {
const searchParams = useSearchParams()
const handleShare = () => {
const ref = searchParams.get('ref')
shareChat(chatId, { ref })
}
return <button onClick={handleShare}>Share</button>
}
```
**Correct (reads on demand, no subscription):**
```tsx
function ShareButton({ chatId }: { chatId: string }) {
const handleShare = () => {
const params = new URLSearchParams(window.location.search)
const ref = params.get('ref')
shareChat(chatId, { ref })
}
return <button onClick={handleShare}>Share</button>
}
```

View File

@@ -1,45 +0,0 @@
---
title: Narrow Effect Dependencies
impact: LOW
impactDescription: minimizes effect re-runs
tags: rerender, useEffect, dependencies, optimization
---
## Narrow Effect Dependencies
Specify primitive dependencies instead of objects to minimize effect re-runs.
**Incorrect (re-runs on any user field change):**
```tsx
useEffect(() => {
console.log(user.id)
}, [user])
```
**Correct (re-runs only when id changes):**
```tsx
useEffect(() => {
console.log(user.id)
}, [user.id])
```
**For derived state, compute outside effect:**
```tsx
// Incorrect: runs on width=767, 766, 765...
useEffect(() => {
if (width < 768) {
enableMobileMode()
}
}, [width])
// Correct: runs only on boolean transition
const isMobile = width < 768
useEffect(() => {
if (isMobile) {
enableMobileMode()
}
}, [isMobile])
```

View File

@@ -1,40 +0,0 @@
---
title: Calculate Derived State During Rendering
impact: MEDIUM
impactDescription: avoids redundant renders and state drift
tags: rerender, derived-state, useEffect, state
---
## Calculate Derived State During Rendering
If a value can be computed from current props/state, do not store it in state or update it in an effect. Derive it during render to avoid extra renders and state drift. Do not set state in effects solely in response to prop changes; prefer derived values or keyed resets instead.
**Incorrect (redundant state and effect):**
```tsx
function Form() {
const [firstName, setFirstName] = useState('First')
const [lastName, setLastName] = useState('Last')
const [fullName, setFullName] = useState('')
useEffect(() => {
setFullName(firstName + ' ' + lastName)
}, [firstName, lastName])
return <p>{fullName}</p>
}
```
**Correct (derive during render):**
```tsx
function Form() {
const [firstName, setFirstName] = useState('First')
const [lastName, setLastName] = useState('Last')
const fullName = firstName + ' ' + lastName
return <p>{fullName}</p>
}
```
References: [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect)

View File

@@ -1,29 +0,0 @@
---
title: Subscribe to Derived State
impact: MEDIUM
impactDescription: reduces re-render frequency
tags: rerender, derived-state, media-query, optimization
---
## Subscribe to Derived State
Subscribe to derived boolean state instead of continuous values to reduce re-render frequency.
**Incorrect (re-renders on every pixel change):**
```tsx
function Sidebar() {
const width = useWindowWidth() // updates continuously
const isMobile = width < 768
return <nav className={isMobile ? 'mobile' : 'desktop'} />
}
```
**Correct (re-renders only when boolean changes):**
```tsx
function Sidebar() {
const isMobile = useMediaQuery('(max-width: 767px)')
return <nav className={isMobile ? 'mobile' : 'desktop'} />
}
```

View File

@@ -1,74 +0,0 @@
---
title: Use Functional setState Updates
impact: MEDIUM
impactDescription: prevents stale closures and unnecessary callback recreations
tags: react, hooks, useState, useCallback, callbacks, closures
---
## Use Functional setState Updates
When updating state based on the current state value, use the functional update form of setState instead of directly referencing the state variable. This prevents stale closures, eliminates unnecessary dependencies, and creates stable callback references.
**Incorrect (requires state as dependency):**
```tsx
function TodoList() {
const [items, setItems] = useState(initialItems)
// Callback must depend on items, recreated on every items change
const addItems = useCallback((newItems: Item[]) => {
setItems([...items, ...newItems])
}, [items]) // ❌ items dependency causes recreations
// Risk of stale closure if dependency is forgotten
const removeItem = useCallback((id: string) => {
setItems(items.filter(item => item.id !== id))
}, []) // ❌ Missing items dependency - will use stale items!
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
}
```
The first callback is recreated every time `items` changes, which can cause child components to re-render unnecessarily. The second callback has a stale closure bug—it will always reference the initial `items` value.
**Correct (stable callbacks, no stale closures):**
```tsx
function TodoList() {
const [items, setItems] = useState(initialItems)
// Stable callback, never recreated
const addItems = useCallback((newItems: Item[]) => {
setItems(curr => [...curr, ...newItems])
}, []) // ✅ No dependencies needed
// Always uses latest state, no stale closure risk
const removeItem = useCallback((id: string) => {
setItems(curr => curr.filter(item => item.id !== id))
}, []) // ✅ Safe and stable
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
}
```
**Benefits:**
1. **Stable callback references** - Callbacks don't need to be recreated when state changes
2. **No stale closures** - Always operates on the latest state value
3. **Fewer dependencies** - Simplifies dependency arrays and reduces memory leaks
4. **Prevents bugs** - Eliminates the most common source of React closure bugs
**When to use functional updates:**
- Any setState that depends on the current state value
- Inside useCallback/useMemo when state is needed
- Event handlers that reference state
- Async operations that update state
**When direct updates are fine:**
- Setting state to a static value: `setCount(0)`
- Setting state from props/arguments only: `setName(newName)`
- State doesn't depend on previous value
**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler can automatically optimize some cases, but functional updates are still recommended for correctness and to prevent stale closure bugs.

View File

@@ -1,58 +0,0 @@
---
title: Use Lazy State Initialization
impact: MEDIUM
impactDescription: wasted computation on every render
tags: react, hooks, useState, performance, initialization
---
## Use Lazy State Initialization
Pass a function to `useState` for expensive initial values. Without the function form, the initializer runs on every render even though the value is only used once.
**Incorrect (runs on every render):**
```tsx
function FilteredList({ items }: { items: Item[] }) {
// buildSearchIndex() runs on EVERY render, even after initialization
const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))
const [query, setQuery] = useState('')
// When query changes, buildSearchIndex runs again unnecessarily
return <SearchResults index={searchIndex} query={query} />
}
function UserProfile() {
// JSON.parse runs on every render
const [settings, setSettings] = useState(
JSON.parse(localStorage.getItem('settings') || '{}')
)
return <SettingsForm settings={settings} onChange={setSettings} />
}
```
**Correct (runs only once):**
```tsx
function FilteredList({ items }: { items: Item[] }) {
// buildSearchIndex() runs ONLY on initial render
const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))
const [query, setQuery] = useState('')
return <SearchResults index={searchIndex} query={query} />
}
function UserProfile() {
// JSON.parse runs only on initial render
const [settings, setSettings] = useState(() => {
const stored = localStorage.getItem('settings')
return stored ? JSON.parse(stored) : {}
})
return <SettingsForm settings={settings} onChange={setSettings} />
}
```
Use lazy initialization when computing initial values from localStorage/sessionStorage, building data structures (indexes, maps), reading from the DOM, or performing heavy transformations.
For simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.

View File

@@ -1,38 +0,0 @@
---
title: Extract Default Non-primitive Parameter Value from Memoized Component to Constant
impact: MEDIUM
impactDescription: restores memoization by using a constant for default value
tags: rerender, memo, optimization
---
## Extract Default Non-primitive Parameter Value from Memoized Component to Constant
When memoized component has a default value for some non-primitive optional parameter, such as an array, function, or object, calling the component without that parameter results in broken memoization. This is because new value instances are created on every rerender, and they do not pass strict equality comparison in `memo()`.
To address this issue, extract the default value into a constant.
**Incorrect (`onClick` has different values on every rerender):**
```tsx
const UserAvatar = memo(function UserAvatar({ onClick = () => {} }: { onClick?: () => void }) {
// ...
})
// Used without optional onClick
<UserAvatar />
```
**Correct (stable default value):**
```tsx
const NOOP = () => {};
const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) {
// ...
})
// Used without optional onClick
<UserAvatar />
```

View File

@@ -1,44 +0,0 @@
---
title: Extract to Memoized Components
impact: MEDIUM
impactDescription: enables early returns
tags: rerender, memo, useMemo, optimization
---
## Extract to Memoized Components
Extract expensive work into memoized components to enable early returns before computation.
**Incorrect (computes avatar even when loading):**
```tsx
function Profile({ user, loading }: Props) {
const avatar = useMemo(() => {
const id = computeAvatarId(user)
return <Avatar id={id} />
}, [user])
if (loading) return <Skeleton />
return <div>{avatar}</div>
}
```
**Correct (skips computation when loading):**
```tsx
const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
const id = useMemo(() => computeAvatarId(user), [user])
return <Avatar id={id} />
})
function Profile({ user, loading }: Props) {
if (loading) return <Skeleton />
return (
<div>
<UserAvatar user={user} />
</div>
)
}
```
**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, manual memoization with `memo()` and `useMemo()` is not necessary. The compiler automatically optimizes re-renders.

View File

@@ -1,45 +0,0 @@
---
title: Put Interaction Logic in Event Handlers
impact: MEDIUM
impactDescription: avoids effect re-runs and duplicate side effects
tags: rerender, useEffect, events, side-effects, dependencies
---
## Put Interaction Logic in Event Handlers
If a side effect is triggered by a specific user action (submit, click, drag), run it in that event handler. Do not model the action as state + effect; it makes effects re-run on unrelated changes and can duplicate the action.
**Incorrect (event modeled as state + effect):**
```tsx
function Form() {
const [submitted, setSubmitted] = useState(false)
const theme = useContext(ThemeContext)
useEffect(() => {
if (submitted) {
post('/api/register')
showToast('Registered', theme)
}
}, [submitted, theme])
return <button onClick={() => setSubmitted(true)}>Submit</button>
}
```
**Correct (do it in the handler):**
```tsx
function Form() {
const theme = useContext(ThemeContext)
function handleSubmit() {
post('/api/register')
showToast('Registered', theme)
}
return <button onClick={handleSubmit}>Submit</button>
}
```
Reference: [Should this code move to an event handler?](https://react.dev/learn/removing-effect-dependencies#should-this-code-move-to-an-event-handler)

View File

@@ -1,35 +0,0 @@
---
title: Do not wrap a simple expression with a primitive result type in useMemo
impact: LOW-MEDIUM
impactDescription: wasted computation on every render
tags: rerender, useMemo, optimization
---
## Do not wrap a simple expression with a primitive result type in useMemo
When an expression is simple (few logical or arithmetical operators) and has a primitive result type (boolean, number, string), do not wrap it in `useMemo`.
Calling `useMemo` and comparing hook dependencies may consume more resources than the expression itself.
**Incorrect:**
```tsx
function Header({ user, notifications }: Props) {
const isLoading = useMemo(() => {
return user.isLoading || notifications.isLoading
}, [user.isLoading, notifications.isLoading])
if (isLoading) return <Skeleton />
// return some markup
}
```
**Correct:**
```tsx
function Header({ user, notifications }: Props) {
const isLoading = user.isLoading || notifications.isLoading
if (isLoading) return <Skeleton />
// return some markup
}
```

View File

@@ -1,40 +0,0 @@
---
title: Use Transitions for Non-Urgent Updates
impact: MEDIUM
impactDescription: maintains UI responsiveness
tags: rerender, transitions, startTransition, performance
---
## Use Transitions for Non-Urgent Updates
Mark frequent, non-urgent state updates as transitions to maintain UI responsiveness.
**Incorrect (blocks UI on every scroll):**
```tsx
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0)
useEffect(() => {
const handler = () => setScrollY(window.scrollY)
window.addEventListener('scroll', handler, { passive: true })
return () => window.removeEventListener('scroll', handler)
}, [])
}
```
**Correct (non-blocking updates):**
```tsx
import { startTransition } from 'react'
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0)
useEffect(() => {
const handler = () => {
startTransition(() => setScrollY(window.scrollY))
}
window.addEventListener('scroll', handler, { passive: true })
return () => window.removeEventListener('scroll', handler)
}, [])
}
```

View File

@@ -1,73 +0,0 @@
---
title: Use useRef for Transient Values
impact: MEDIUM
impactDescription: avoids unnecessary re-renders on frequent updates
tags: rerender, useref, state, performance
---
## Use useRef for Transient Values
When a value changes frequently and you don't want a re-render on every update (e.g., mouse trackers, intervals, transient flags), store it in `useRef` instead of `useState`. Keep component state for UI; use refs for temporary DOM-adjacent values. Updating a ref does not trigger a re-render.
**Incorrect (renders every update):**
```tsx
function Tracker() {
const [lastX, setLastX] = useState(0)
useEffect(() => {
const onMove = (e: MouseEvent) => setLastX(e.clientX)
window.addEventListener('mousemove', onMove)
return () => window.removeEventListener('mousemove', onMove)
}, [])
return (
<div
style={{
position: 'fixed',
top: 0,
left: lastX,
width: 8,
height: 8,
background: 'black',
}}
/>
)
}
```
**Correct (no re-render for tracking):**
```tsx
function Tracker() {
const lastXRef = useRef(0)
const dotRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const onMove = (e: MouseEvent) => {
lastXRef.current = e.clientX
const node = dotRef.current
if (node) {
node.style.transform = `translateX(${e.clientX}px)`
}
}
window.addEventListener('mousemove', onMove)
return () => window.removeEventListener('mousemove', onMove)
}, [])
return (
<div
ref={dotRef}
style={{
position: 'fixed',
top: 0,
left: 0,
width: 8,
height: 8,
background: 'black',
transform: 'translateX(0px)',
}}
/>
)
}
```

View File

@@ -1,73 +0,0 @@
---
title: Use after() for Non-Blocking Operations
impact: MEDIUM
impactDescription: faster response times
tags: server, async, logging, analytics, side-effects
---
## Use after() for Non-Blocking Operations
Use Next.js's `after()` to schedule work that should execute after a response is sent. This prevents logging, analytics, and other side effects from blocking the response.
**Incorrect (blocks response):**
```tsx
import { logUserAction } from '@/app/utils'
export async function POST(request: Request) {
// Perform mutation
await updateDatabase(request)
// Logging blocks the response
const userAgent = request.headers.get('user-agent') || 'unknown'
await logUserAction({ userAgent })
return new Response(JSON.stringify({ status: 'success' }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
}
```
**Correct (non-blocking):**
```tsx
import { after } from 'next/server'
import { headers, cookies } from 'next/headers'
import { logUserAction } from '@/app/utils'
export async function POST(request: Request) {
// Perform mutation
await updateDatabase(request)
// Log after response is sent
after(async () => {
const userAgent = (await headers()).get('user-agent') || 'unknown'
const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous'
logUserAction({ sessionCookie, userAgent })
})
return new Response(JSON.stringify({ status: 'success' }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
}
```
The response is sent immediately while logging happens in the background.
**Common use cases:**
- Analytics tracking
- Audit logging
- Sending notifications
- Cache invalidation
- Cleanup tasks
**Important notes:**
- `after()` runs even if the response fails or redirects
- Works in Server Actions, Route Handlers, and Server Components
Reference: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after)

View File

@@ -1,96 +0,0 @@
---
title: Authenticate Server Actions Like API Routes
impact: CRITICAL
impactDescription: prevents unauthorized access to server mutations
tags: server, server-actions, authentication, security, authorization
---
## Authenticate Server Actions Like API Routes
**Impact: CRITICAL (prevents unauthorized access to server mutations)**
Server Actions (functions with `"use server"`) are exposed as public endpoints, just like API routes. Always verify authentication and authorization **inside** each Server Action—do not rely solely on middleware, layout guards, or page-level checks, as Server Actions can be invoked directly.
Next.js documentation explicitly states: "Treat Server Actions with the same security considerations as public-facing API endpoints, and verify if the user is allowed to perform a mutation."
**Incorrect (no authentication check):**
```typescript
'use server'
export async function deleteUser(userId: string) {
// Anyone can call this! No auth check
await db.user.delete({ where: { id: userId } })
return { success: true }
}
```
**Correct (authentication inside the action):**
```typescript
'use server'
import { verifySession } from '@/lib/auth'
import { unauthorized } from '@/lib/errors'
export async function deleteUser(userId: string) {
// Always check auth inside the action
const session = await verifySession()
if (!session) {
throw unauthorized('Must be logged in')
}
// Check authorization too
if (session.user.role !== 'admin' && session.user.id !== userId) {
throw unauthorized('Cannot delete other users')
}
await db.user.delete({ where: { id: userId } })
return { success: true }
}
```
**With input validation:**
```typescript
'use server'
import { verifySession } from '@/lib/auth'
import { z } from 'zod'
const updateProfileSchema = z.object({
userId: z.string().uuid(),
name: z.string().min(1).max(100),
email: z.string().email()
})
export async function updateProfile(data: unknown) {
// Validate input first
const validated = updateProfileSchema.parse(data)
// Then authenticate
const session = await verifySession()
if (!session) {
throw new Error('Unauthorized')
}
// Then authorize
if (session.user.id !== validated.userId) {
throw new Error('Can only update own profile')
}
// Finally perform the mutation
await db.user.update({
where: { id: validated.userId },
data: {
name: validated.name,
email: validated.email
}
})
return { success: true }
}
```
Reference: [https://nextjs.org/docs/app/guides/authentication](https://nextjs.org/docs/app/guides/authentication)

View File

@@ -1,41 +0,0 @@
---
title: Cross-Request LRU Caching
impact: HIGH
impactDescription: caches across requests
tags: server, cache, lru, cross-request
---
## Cross-Request LRU Caching
`React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache.
**Implementation:**
```typescript
import { LRUCache } from 'lru-cache'
const cache = new LRUCache<string, any>({
max: 1000,
ttl: 5 * 60 * 1000 // 5 minutes
})
export async function getUser(id: string) {
const cached = cache.get(id)
if (cached) return cached
const user = await db.user.findUnique({ where: { id } })
cache.set(id, user)
return user
}
// Request 1: DB query, result cached
// Request 2: cache hit, no DB query
```
Use when sequential user actions hit multiple endpoints needing the same data within seconds.
**With Vercel's [Fluid Compute](https://vercel.com/docs/fluid-compute):** LRU caching is especially effective because multiple concurrent requests can share the same function instance and cache. This means the cache persists across requests without needing external storage like Redis.
**In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching.
Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)

View File

@@ -1,76 +0,0 @@
---
title: Per-Request Deduplication with React.cache()
impact: MEDIUM
impactDescription: deduplicates within request
tags: server, cache, react-cache, deduplication
---
## Per-Request Deduplication with React.cache()
Use `React.cache()` for server-side request deduplication. Authentication and database queries benefit most.
**Usage:**
```typescript
import { cache } from 'react'
export const getCurrentUser = cache(async () => {
const session = await auth()
if (!session?.user?.id) return null
return await db.user.findUnique({
where: { id: session.user.id }
})
})
```
Within a single request, multiple calls to `getCurrentUser()` execute the query only once.
**Avoid inline objects as arguments:**
`React.cache()` uses shallow equality (`Object.is`) to determine cache hits. Inline objects create new references each call, preventing cache hits.
**Incorrect (always cache miss):**
```typescript
const getUser = cache(async (params: { uid: number }) => {
return await db.user.findUnique({ where: { id: params.uid } })
})
// Each call creates new object, never hits cache
getUser({ uid: 1 })
getUser({ uid: 1 }) // Cache miss, runs query again
```
**Correct (cache hit):**
```typescript
const getUser = cache(async (uid: number) => {
return await db.user.findUnique({ where: { id: uid } })
})
// Primitive args use value equality
getUser(1)
getUser(1) // Cache hit, returns cached result
```
If you must pass objects, pass the same reference:
```typescript
const params = { uid: 1 }
getUser(params) // Query runs
getUser(params) // Cache hit (same reference)
```
**Next.js-Specific Note:**
In Next.js, the `fetch` API is automatically extended with request memoization. Requests with the same URL and options are automatically deduplicated within a single request, so you don't need `React.cache()` for `fetch` calls. However, `React.cache()` is still essential for other async tasks:
- Database queries (Prisma, Drizzle, etc.)
- Heavy computations
- Authentication checks
- File system operations
- Any non-fetch async work
Use `React.cache()` to deduplicate these operations across your component tree.
Reference: [React.cache documentation](https://react.dev/reference/react/cache)

View File

@@ -1,65 +0,0 @@
---
title: Avoid Duplicate Serialization in RSC Props
impact: LOW
impactDescription: reduces network payload by avoiding duplicate serialization
tags: server, rsc, serialization, props, client-components
---
## Avoid Duplicate Serialization in RSC Props
**Impact: LOW (reduces network payload by avoiding duplicate serialization)**
RSC→client serialization deduplicates by object reference, not value. Same reference = serialized once; new reference = serialized again. Do transformations (`.toSorted()`, `.filter()`, `.map()`) in client, not server.
**Incorrect (duplicates array):**
```tsx
// RSC: sends 6 strings (2 arrays × 3 items)
<ClientList usernames={usernames} usernamesOrdered={usernames.toSorted()} />
```
**Correct (sends 3 strings):**
```tsx
// RSC: send once
<ClientList usernames={usernames} />
// Client: transform there
'use client'
const sorted = useMemo(() => [...usernames].sort(), [usernames])
```
**Nested deduplication behavior:**
Deduplication works recursively. Impact varies by data type:
- `string[]`, `number[]`, `boolean[]`: **HIGH impact** - array + all primitives fully duplicated
- `object[]`: **LOW impact** - array duplicated, but nested objects deduplicated by reference
```tsx
// string[] - duplicates everything
usernames={['a','b']} sorted={usernames.toSorted()} // sends 4 strings
// object[] - duplicates array structure only
users={[{id:1},{id:2}]} sorted={users.toSorted()} // sends 2 arrays + 2 unique objects (not 4)
```
**Operations breaking deduplication (create new references):**
- Arrays: `.toSorted()`, `.filter()`, `.map()`, `.slice()`, `[...arr]`
- Objects: `{...obj}`, `Object.assign()`, `structuredClone()`, `JSON.parse(JSON.stringify())`
**More examples:**
```tsx
// ❌ Bad
<C users={users} active={users.filter(u => u.active)} />
<C product={product} productName={product.name} />
// ✅ Good
<C users={users} />
<C product={product} />
// Do filtering/destructuring in client
```
**Exception:** Pass derived data when transformation is expensive or client doesn't need original.

Some files were not shown because too many files have changed in this diff Show More