@coralai/sps-cli 0.42.0 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -3
- package/dist/commands/projectInit.d.ts.map +1 -1
- package/dist/commands/projectInit.js +40 -53
- package/dist/commands/projectInit.js.map +1 -1
- package/dist/commands/skillCommand.d.ts +2 -0
- package/dist/commands/skillCommand.d.ts.map +1 -0
- package/dist/commands/skillCommand.js +235 -0
- package/dist/commands/skillCommand.js.map +1 -0
- package/dist/core/skillStore.d.ts +46 -0
- package/dist/core/skillStore.d.ts.map +1 -0
- package/dist/core/skillStore.js +197 -0
- package/dist/core/skillStore.js.map +1 -0
- package/dist/core/skillStore.test.d.ts +2 -0
- package/dist/core/skillStore.test.d.ts.map +1 -0
- package/dist/core/skillStore.test.js +190 -0
- package/dist/core/skillStore.test.js.map +1 -0
- package/dist/main.js +19 -17
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
- package/skills/architecture-decision-records/SKILL.md +207 -0
- package/skills/backend/SKILL.md +62 -0
- package/skills/backend/references/api-design.md +168 -0
- package/skills/backend/references/caching.md +181 -0
- package/skills/backend/references/data-access.md +173 -0
- package/skills/backend/references/layering.md +181 -0
- package/skills/backend/references/observability.md +190 -0
- package/skills/backend/references/resilience.md +201 -0
- package/skills/backend/references/security.md +186 -0
- package/skills/backend-architect/SKILL.md +119 -0
- package/skills/code-reviewer/SKILL.md +143 -0
- package/skills/coding-standards/SKILL.md +60 -0
- package/skills/coding-standards/references/clean-code.md +258 -0
- package/skills/coding-standards/references/code-review.md +192 -0
- package/skills/coding-standards/references/commits-and-prs.md +226 -0
- package/skills/coding-standards/references/error-strategy.md +193 -0
- package/skills/coding-standards/references/naming.md +185 -0
- package/skills/coding-standards/references/tdd.md +171 -0
- package/skills/database/SKILL.md +53 -0
- package/skills/database/references/indexing.md +190 -0
- package/skills/database/references/migrations.md +199 -0
- package/skills/database/references/nosql.md +185 -0
- package/skills/database/references/queries.md +295 -0
- package/skills/database/references/scaling.md +203 -0
- package/skills/database/references/schema.md +191 -0
- package/skills/database-optimizer/SKILL.md +168 -0
- package/skills/debugging-workflow/SKILL.md +244 -0
- package/skills/devops/SKILL.md +55 -0
- package/skills/devops/references/ci-cd.md +204 -0
- package/skills/devops/references/containers.md +272 -0
- package/skills/devops/references/deploy.md +201 -0
- package/skills/devops/references/iac.md +252 -0
- package/skills/devops/references/observability.md +228 -0
- package/skills/devops/references/secrets.md +178 -0
- package/skills/devops-automator/SKILL.md +164 -0
- package/skills/frontend/SKILL.md +52 -0
- package/skills/frontend/references/accessibility.md +222 -0
- package/skills/frontend/references/components.md +206 -0
- package/skills/frontend/references/performance.md +219 -0
- package/skills/frontend/references/routing.md +209 -0
- package/skills/frontend/references/state.md +190 -0
- package/skills/frontend/references/testing.md +216 -0
- package/skills/frontend-developer/SKILL.md +115 -0
- package/skills/git-workflow/SKILL.md +355 -0
- package/skills/golang/SKILL.md +49 -0
- package/skills/golang/references/concurrency.md +284 -0
- package/skills/golang/references/errors.md +241 -0
- package/skills/golang/references/idioms.md +285 -0
- package/skills/golang/references/testing.md +238 -0
- package/skills/java/SKILL.md +50 -0
- package/skills/java/references/concurrency.md +194 -0
- package/skills/java/references/idioms.md +283 -0
- package/skills/java/references/testing.md +228 -0
- package/skills/kotlin/SKILL.md +47 -0
- package/skills/kotlin/references/coroutines.md +240 -0
- package/skills/kotlin/references/idioms.md +268 -0
- package/skills/kotlin/references/testing.md +219 -0
- package/skills/mobile/SKILL.md +50 -0
- package/skills/mobile/references/architecture.md +204 -0
- package/skills/mobile/references/navigation.md +158 -0
- package/skills/mobile/references/performance.md +152 -0
- package/skills/mobile/references/platform.md +166 -0
- package/skills/mobile/references/state-and-data.md +174 -0
- package/skills/python/SKILL.md +51 -0
- package/skills/python/THIRD_PARTY.md +14 -0
- package/skills/python/references/async.md +218 -0
- package/skills/python/references/error-handling.md +254 -0
- package/skills/python/references/idioms.md +279 -0
- package/skills/python/references/packaging.md +233 -0
- package/skills/python/references/testing.md +269 -0
- package/skills/python/references/typing.md +292 -0
- package/skills/qa-tester/SKILL.md +186 -0
- package/skills/rust/SKILL.md +50 -0
- package/skills/rust/references/async.md +224 -0
- package/skills/rust/references/errors.md +240 -0
- package/skills/rust/references/ownership.md +263 -0
- package/skills/rust/references/testing.md +274 -0
- package/skills/rust/references/traits.md +250 -0
- package/skills/security-engineer/SKILL.md +157 -0
- package/skills/swift/SKILL.md +48 -0
- package/skills/swift/references/concurrency.md +280 -0
- package/skills/swift/references/idioms.md +334 -0
- package/skills/swift/references/testing.md +229 -0
- package/skills/typescript/SKILL.md +51 -0
- package/skills/typescript/references/async.md +241 -0
- package/skills/typescript/references/errors.md +208 -0
- package/skills/typescript/references/idioms.md +246 -0
- package/skills/typescript/references/testing.md +225 -0
- package/skills/typescript/references/tooling.md +208 -0
- package/skills/typescript/references/types.md +259 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# Performance
|
|
2
|
+
|
|
3
|
+
Rendering cost, bundle size, code-splitting, images, fonts.
|
|
4
|
+
|
|
5
|
+
## Measure first
|
|
6
|
+
|
|
7
|
+
No optimization without a number. Three cheap measures:
|
|
8
|
+
|
|
9
|
+
- **Lighthouse / PageSpeed Insights** — first-load & Core Web Vitals.
|
|
10
|
+
- **Browser DevTools Performance tab** — where time goes during interaction.
|
|
11
|
+
- **Framework-specific profiler** — React DevTools Profiler, Vue DevTools, Svelte's built-in.
|
|
12
|
+
|
|
13
|
+
Target Core Web Vitals (real-user, 75th percentile):
|
|
14
|
+
- **LCP** (Largest Contentful Paint) < 2.5 s
|
|
15
|
+
- **INP** (Interaction to Next Paint) < 200 ms
|
|
16
|
+
- **CLS** (Cumulative Layout Shift) < 0.1
|
|
17
|
+
|
|
18
|
+
If one is green, stop. Optimizing green metrics is churn.
|
|
19
|
+
|
|
20
|
+
## Initial load
|
|
21
|
+
|
|
22
|
+
### Bundle size
|
|
23
|
+
|
|
24
|
+
Use the bundler's analyzer (`rollup-plugin-visualizer`, `webpack-bundle-analyzer`, Next's built-in). Look for:
|
|
25
|
+
|
|
26
|
+
- **Duplicate libraries** — two copies of React / lodash usually mean a version mismatch.
|
|
27
|
+
- **Locales / polyfills** — `moment` ships every locale by default; `date-fns-tz` / `luxon` are leaner.
|
|
28
|
+
- **Barrel re-exports** defeating tree-shaking — named imports work only if the dependency's `package.json` says `"sideEffects": false`.
|
|
29
|
+
|
|
30
|
+
### Code splitting
|
|
31
|
+
|
|
32
|
+
Load only what the current route needs.
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
const Settings = lazy(() => import('./features/settings/Settings'));
|
|
36
|
+
|
|
37
|
+
<Suspense fallback={<Skeleton />}>
|
|
38
|
+
<Settings />
|
|
39
|
+
</Suspense>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Route-level splitting is free with modern routers. Component-level splitting is worth it for:
|
|
43
|
+
- Heavy third-party libs (rich text editor, chart lib, map lib) loaded only when the feature opens.
|
|
44
|
+
- Large modals / drawers.
|
|
45
|
+
|
|
46
|
+
### Preloading
|
|
47
|
+
|
|
48
|
+
Tell the browser what's coming:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
<link rel="preload" as="font" href="/fonts/inter.woff2" crossorigin>
|
|
52
|
+
<link rel="preconnect" href="https://api.example.com">
|
|
53
|
+
<link rel="dns-prefetch" href="https://cdn.example.com">
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Or via framework API (`next/link` prefetch, `<Link>` prefetch). Pair with hover / viewport prefetching (see `routing.md`).
|
|
57
|
+
|
|
58
|
+
## Render cost
|
|
59
|
+
|
|
60
|
+
### Keep updates local
|
|
61
|
+
|
|
62
|
+
One component updating should not re-render the whole tree. Signals (Solid, Vue refs, Svelte reactivity) are fine-grained by default. React re-renders by reference — containment is on you:
|
|
63
|
+
|
|
64
|
+
- Split components so state lives near where it changes.
|
|
65
|
+
- Memoize the boundary with `React.memo`.
|
|
66
|
+
- Stable callback / object references (`useCallback`, `useMemo`).
|
|
67
|
+
|
|
68
|
+
Don't spray memoization everywhere. Measure; memoize where it matters.
|
|
69
|
+
|
|
70
|
+
### Expensive children
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
function List({ items }) {
|
|
74
|
+
return items.map(i => <Row key={i.id} item={i} />);
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
If `Row` is expensive and `List` gets a new `items` array reference (but same data), every row re-renders. Either:
|
|
79
|
+
- Use the cache lib to return stable references from queries.
|
|
80
|
+
- Memoize `Row` with equality on `item.id` + `item.version`.
|
|
81
|
+
|
|
82
|
+
### Lists — virtualize if big
|
|
83
|
+
|
|
84
|
+
Rendering 10 000 DOM nodes is slow at any framework. Use `react-window`, `@tanstack/virtual`, or `IntersectionObserver`-based lazy rendering.
|
|
85
|
+
|
|
86
|
+
Rule of thumb: virtualize any list that might show more than a couple hundred items.
|
|
87
|
+
|
|
88
|
+
### Avoid layout thrashing
|
|
89
|
+
|
|
90
|
+
Writing to the DOM (style, class) then reading (offsetHeight, getBoundingClientRect) in the same frame forces re-layout. Batch reads, then writes.
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
# ❌
|
|
94
|
+
for (const el of items) {
|
|
95
|
+
el.style.height = 'auto';
|
|
96
|
+
const h = el.offsetHeight; // forces layout
|
|
97
|
+
el.dataset.h = h;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# ✅
|
|
101
|
+
const heights = items.map(el => el.offsetHeight); // read
|
|
102
|
+
items.forEach((el, i) => el.dataset.h = heights[i]); // write
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Use `IntersectionObserver`, `ResizeObserver` instead of `getBoundingClientRect` in scroll / resize handlers.
|
|
106
|
+
|
|
107
|
+
## Images
|
|
108
|
+
|
|
109
|
+
Biggest easy win. Order of effect:
|
|
110
|
+
|
|
111
|
+
1. **Right size.** Don't serve a 2000 × 2000 JPEG into a 400 × 400 avatar slot.
|
|
112
|
+
2. **Right format.** AVIF > WebP > JPEG for photos; PNG / SVG for flat.
|
|
113
|
+
3. **Responsive `srcset` / `<picture>`** so mobile doesn't fetch desktop-size images.
|
|
114
|
+
4. **Lazy-load below-the-fold** with `loading="lazy"` or `IntersectionObserver`.
|
|
115
|
+
5. **Modern image component** (`next/image`, `nuxt/image`) auto-serves the right size/format with `priority` hints for the LCP image.
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
<img src="hero-400.avif"
|
|
119
|
+
srcset="hero-400.avif 1x, hero-800.avif 2x"
|
|
120
|
+
sizes="(max-width: 600px) 400px, 800px"
|
|
121
|
+
loading="lazy"
|
|
122
|
+
width="400" height="300"
|
|
123
|
+
alt="...">
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Always set `width` and `height` (or aspect-ratio). Reserves layout space; kills CLS.
|
|
127
|
+
|
|
128
|
+
## Fonts
|
|
129
|
+
|
|
130
|
+
Web fonts are costly — big, render-blocking without hints.
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
```css
|
|
137
|
+
@font-face {
|
|
138
|
+
font-family: 'Inter';
|
|
139
|
+
src: url('/fonts/inter.woff2') format('woff2');
|
|
140
|
+
font-display: swap; /* show fallback text while loading; swap in when ready */
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
- **Subset** to characters you use (`glyphhanger`, `pyftsubset`).
|
|
145
|
+
- **`font-display: swap`** avoids invisible text during load (FOIT).
|
|
146
|
+
- **System font stacks** are free and fast if your brand allows.
|
|
147
|
+
|
|
148
|
+
## JavaScript execution
|
|
149
|
+
|
|
150
|
+
Third-party scripts (analytics, chat widgets, A/B tools) are the leading cause of poor INP.
|
|
151
|
+
|
|
152
|
+
- Load non-critical scripts with `async` / `defer`.
|
|
153
|
+
- Consider `requestIdleCallback` for non-urgent work.
|
|
154
|
+
- Audit what's on the critical path; "one more tag" slows every user.
|
|
155
|
+
|
|
156
|
+
## Caching
|
|
157
|
+
|
|
158
|
+
Set `Cache-Control` headers generously on static assets. Combine with content-hashed filenames (`main.f3a7c.js`) so long TTLs don't prevent deploys.
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
Cache-Control: public, max-age=31536000, immutable # hashed assets
|
|
162
|
+
Cache-Control: no-cache # HTML
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Service workers (PWA) add offline caching; use a tested recipe (Workbox) rather than hand-rolling.
|
|
166
|
+
|
|
167
|
+
## Reducing main-thread work
|
|
168
|
+
|
|
169
|
+
Identify long tasks in DevTools (purple blocks). Common offenders:
|
|
170
|
+
|
|
171
|
+
- JSON parsing large payloads — use streaming (NDJSON), smaller pages.
|
|
172
|
+
- Expensive computations during render — move to a Web Worker.
|
|
173
|
+
- Hydration in SSR apps — use selective / streaming hydration (React Server Components, Qwik's resumability, Astro Islands).
|
|
174
|
+
|
|
175
|
+
## Web Workers — for CPU-bound work
|
|
176
|
+
|
|
177
|
+
For image processing, parsing, crypto, heavy transforms.
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
const worker = new Worker(new URL('./heavy.worker.js', import.meta.url), { type: 'module' });
|
|
181
|
+
worker.postMessage({ data });
|
|
182
|
+
worker.onmessage = (e) => setResult(e.data);
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Libraries: `comlink` makes postMessage feel like async function calls. `workerize-loader` auto-splits.
|
|
186
|
+
|
|
187
|
+
## Avoid Layout Shift (CLS)
|
|
188
|
+
|
|
189
|
+
- Reserve space for images (`width` / `height` or `aspect-ratio`).
|
|
190
|
+
- Avoid late-loaded content above existing content (banners, cookies) — overlay or reserve.
|
|
191
|
+
- Use CSS `font-size-adjust` and fallback metrics to minimize FOUT jumps.
|
|
192
|
+
|
|
193
|
+
## Performance budgets
|
|
194
|
+
|
|
195
|
+
Set numbers in CI:
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
# lighthouse-ci or equivalent
|
|
199
|
+
"assertions": {
|
|
200
|
+
"categories:performance": ["error", {"minScore": 0.9}],
|
|
201
|
+
"largest-contentful-paint": ["warn", {"maxNumericValue": 2500}]
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Budget violations block merge. Without numbers, perf erodes silently.
|
|
206
|
+
|
|
207
|
+
## Anti-patterns
|
|
208
|
+
|
|
209
|
+
| Anti-pattern | Fix |
|
|
210
|
+
|---|---|
|
|
211
|
+
| Importing a huge lib for one function | Tree-shake (`lodash-es`), or copy the function |
|
|
212
|
+
| Sending 5 MB JS to render 50 KB of content | Code-split; audit bundles |
|
|
213
|
+
| No lazy-loading for below-the-fold images | `loading="lazy"` / image component |
|
|
214
|
+
| Missing width/height on `<img>` | Causes CLS |
|
|
215
|
+
| `@import` in CSS | Blocks; bundle with build tool |
|
|
216
|
+
| 20 third-party scripts on the critical path | Load deferred; remove what doesn't earn its weight |
|
|
217
|
+
| Shipping unminified dev builds | Build step + server compression |
|
|
218
|
+
| SSR hydrating the entire page on mount | Selective / islands / RSC |
|
|
219
|
+
| Animation with `top`/`left` / `width`/`height` (triggers layout) | Use `transform` / `opacity` |
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# Routing
|
|
2
|
+
|
|
3
|
+
Client-side routing, deep linking, guards, transitions.
|
|
4
|
+
|
|
5
|
+
## Route = URL + data + component
|
|
6
|
+
|
|
7
|
+
Every route declaration answers three questions:
|
|
8
|
+
1. What URL pattern matches?
|
|
9
|
+
2. What data does it need?
|
|
10
|
+
3. What renders?
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
{
|
|
14
|
+
path: '/orders/:id',
|
|
15
|
+
loader: ({ params }) => fetchOrder(params.id),
|
|
16
|
+
element: <OrderDetail />,
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Modern routers (React Router 6+, TanStack Router, Nuxt, SvelteKit, Remix) embed data loading. Prefer route-level loaders over fetching in components — it parallelizes data with rendering and enables streaming / Suspense.
|
|
21
|
+
|
|
22
|
+
## Nested routes
|
|
23
|
+
|
|
24
|
+
A nested route inherits layout from its parent.
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
/app
|
|
28
|
+
/app/profile <Settings tabs /> + ProfilePanel
|
|
29
|
+
/app/billing <Settings tabs /> + BillingPanel
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Parent route stays mounted; only the outlet changes. Cheap tab switches, persistent side nav, zero re-fetch for shared data.
|
|
33
|
+
|
|
34
|
+
## Dynamic segments
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
/users/:userId/orders/:orderId
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Treat params as strings; parse and validate at the boundary. Never trust the URL — a bad one shouldn't crash your app.
|
|
41
|
+
|
|
42
|
+
## Query params
|
|
43
|
+
|
|
44
|
+
For sort, filter, pagination, selected tab. Don't shoehorn into path params.
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
/users?role=admin&sort=-createdAt&page=2
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Client routers give you typed access (`useSearchParams` / similar). Always treat query params as optional; have sensible defaults.
|
|
51
|
+
|
|
52
|
+
## Route guards
|
|
53
|
+
|
|
54
|
+
Auth, role, feature flag checks happen at the route boundary, not in the component.
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
{
|
|
58
|
+
path: '/admin',
|
|
59
|
+
loader: async () => {
|
|
60
|
+
const user = await requireAuth();
|
|
61
|
+
if (!user.roles.includes('admin')) throw redirect('/');
|
|
62
|
+
return null;
|
|
63
|
+
},
|
|
64
|
+
element: <AdminShell />,
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Return a redirect or throw; don't render and then unmount. Unauthorized users should never see the page flash.
|
|
69
|
+
|
|
70
|
+
## Navigation
|
|
71
|
+
|
|
72
|
+
Imperative vs. declarative:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
// Declarative — preferred for actions triggered by markup
|
|
76
|
+
<Link to={`/orders/${id}`}>View</Link>
|
|
77
|
+
|
|
78
|
+
// Imperative — for after-effect navigation (post-submit, post-auth)
|
|
79
|
+
await submit();
|
|
80
|
+
navigate('/orders');
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`<Link>` / `<NuxtLink>` / `<a>` with client-side handling gets you:
|
|
84
|
+
- Prefetching on hover / visible.
|
|
85
|
+
- Correct ctrl/cmd/middle-click behaviour.
|
|
86
|
+
- Accessibility (focus ring, screen-reader announcement).
|
|
87
|
+
|
|
88
|
+
Avoid `onClick={() => navigate(...)}` on a `<div>` — it breaks all of that.
|
|
89
|
+
|
|
90
|
+
## Prefetching
|
|
91
|
+
|
|
92
|
+
The right-click / hover preload is the cheapest perf optimization available.
|
|
93
|
+
|
|
94
|
+
- **Hover**: preload route data on mouse over (~100ms before click).
|
|
95
|
+
- **Viewport**: preload links that appear on screen.
|
|
96
|
+
- **Intent**: preload the next step in a known flow (after login → dashboard).
|
|
97
|
+
|
|
98
|
+
Most modern routers support all three declaratively. Enable them.
|
|
99
|
+
|
|
100
|
+
## Transitions
|
|
101
|
+
|
|
102
|
+
Two patterns:
|
|
103
|
+
|
|
104
|
+
### Block-until-ready
|
|
105
|
+
|
|
106
|
+
Show the old page until the new route's data is loaded. Good for same-shell navigations.
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
{navigation.state === 'loading' && <TopProgressBar />}
|
|
110
|
+
<Outlet />
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Render-and-stream
|
|
114
|
+
|
|
115
|
+
Render the new layout immediately with `<Suspense>` boundaries for not-yet-loaded data. Better for deep nested data.
|
|
116
|
+
|
|
117
|
+
Pick per-route. Don't universalize one policy.
|
|
118
|
+
|
|
119
|
+
## 404s and fallbacks
|
|
120
|
+
|
|
121
|
+
Route declarations should include:
|
|
122
|
+
- An explicit 404 route at the end.
|
|
123
|
+
- An error boundary per route level.
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
{ path: '*', element: <NotFound /> }
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Otherwise a mistyped URL renders a blank screen.
|
|
130
|
+
|
|
131
|
+
## Redirects
|
|
132
|
+
|
|
133
|
+
Put redirects at the router level, not in an effect.
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
# ❌ effect-based
|
|
137
|
+
useEffect(() => { if (!user) navigate('/login'); }, [user]);
|
|
138
|
+
// flash of protected content before effect runs
|
|
139
|
+
|
|
140
|
+
# ✅ route-level
|
|
141
|
+
loader: async () => {
|
|
142
|
+
const user = await getUser();
|
|
143
|
+
if (!user) throw redirect('/login');
|
|
144
|
+
return user;
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Route parameters vs. path
|
|
149
|
+
|
|
150
|
+
- Path: `/orders/:id` — `id` is essential to identify the resource.
|
|
151
|
+
- Query: `/orders?filter=...&sort=...` — UI controls, non-essential to URL identity.
|
|
152
|
+
|
|
153
|
+
Rule: if two people share the URL, path params are "what am I looking at", query params are "how do I want to see it".
|
|
154
|
+
|
|
155
|
+
## Scroll restoration
|
|
156
|
+
|
|
157
|
+
On back/forward, restore the previous scroll. On forward navigation, scroll to top (or anchor).
|
|
158
|
+
|
|
159
|
+
Most routers offer `<ScrollRestoration />` / built-in behaviour. Use it. Manual scroll logic becomes buggy fast.
|
|
160
|
+
|
|
161
|
+
## Loading / pending UI per route
|
|
162
|
+
|
|
163
|
+
Each route knows what "loading" looks like — a skeleton, a progress bar, or nothing (for fast enough loads).
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
{
|
|
167
|
+
path: '/dashboard',
|
|
168
|
+
loader: fetchDashboard,
|
|
169
|
+
element: <Dashboard />,
|
|
170
|
+
pendingElement: <DashboardSkeleton />, // or via Suspense boundary
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Don't use a global spinner that blocks the whole app on every navigation. It makes the UI feel slow.
|
|
175
|
+
|
|
176
|
+
## Deep linking
|
|
177
|
+
|
|
178
|
+
Every URL should be shareable / bookmarkable / refreshable:
|
|
179
|
+
|
|
180
|
+
- Modals that represent significant state → put in URL (`/inbox/message/42`).
|
|
181
|
+
- Selected tab → query param.
|
|
182
|
+
- Filter pills → query param.
|
|
183
|
+
- Ephemeral UI (tooltip open, hover state) → not in URL.
|
|
184
|
+
|
|
185
|
+
If users screenshot and paste your URL, the page should load exactly what they saw.
|
|
186
|
+
|
|
187
|
+
## SSR / SSG / hybrid
|
|
188
|
+
|
|
189
|
+
For SEO-sensitive or fast-first-paint pages, server-render the initial route:
|
|
190
|
+
|
|
191
|
+
- **SSR** (Next.js, Nuxt, SvelteKit, Remix) — render per-request on the server.
|
|
192
|
+
- **SSG** (static generation) — render at build time.
|
|
193
|
+
- **ISR** (on-demand revalidation) — SSG + background refresh.
|
|
194
|
+
- **CSR** (traditional SPA) — render entirely in the browser.
|
|
195
|
+
|
|
196
|
+
Choice is architectural; covered more in the framework docs. From a routing perspective: route-level data loaders work identically across all four, which is the whole point of these routers.
|
|
197
|
+
|
|
198
|
+
## Anti-patterns
|
|
199
|
+
|
|
200
|
+
| Anti-pattern | Fix |
|
|
201
|
+
|---|---|
|
|
202
|
+
| `useEffect(() => fetch(...), [params])` in a component | Route loader |
|
|
203
|
+
| Mutating URL via `window.history.pushState` | Use the router API |
|
|
204
|
+
| Non-semantic "links" (`onClick` on `<div>`) | Use `<Link>` / `<a>` |
|
|
205
|
+
| Redirect via `useEffect` + `navigate` for unauthenticated pages | Route guard / loader redirect |
|
|
206
|
+
| Passing current route info via Context | Read from router — `useLocation` / `useRoute` |
|
|
207
|
+
| Nesting routes too deeply for the layout to match | Flatten; layouts and routes are separate axes |
|
|
208
|
+
| Skipping `<NotFound />` | Always have a 404 route |
|
|
209
|
+
| Treating the query string as throwaway | It's user state; design it |
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# State
|
|
2
|
+
|
|
3
|
+
Local, shared, server, derived. The taxonomy matters more than the library.
|
|
4
|
+
|
|
5
|
+
## Taxonomy
|
|
6
|
+
|
|
7
|
+
| Kind | Lives | Examples | Tool |
|
|
8
|
+
|---|---|---|---|
|
|
9
|
+
| **Local** | In one component | Input text, dropdown-open boolean | `useState` / `ref` / component state |
|
|
10
|
+
| **Shared** | Across siblings or deep tree | Theme, current user, feature flag | Context / provider / store |
|
|
11
|
+
| **Server** | Comes from an API | User list, order details, prices | Server-cache lib (TanStack Query, SWR, Apollo) |
|
|
12
|
+
| **URL** | In the address bar | Current page, filters, selected item | Router query / path params |
|
|
13
|
+
| **Derived** | Computed from other state | `totalCents = sum(items.map(i => i.cents))` | Memo / computed |
|
|
14
|
+
|
|
15
|
+
Knowing which bucket each piece belongs in saves 80% of state-management pain.
|
|
16
|
+
|
|
17
|
+
## Server state is special
|
|
18
|
+
|
|
19
|
+
Server state is not your state — it's a cache of someone else's state. Treat it that way.
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
// ❌ putting server data into a global store
|
|
23
|
+
dispatch({ type: 'users/loaded', users });
|
|
24
|
+
|
|
25
|
+
// ✅ using a server-cache library
|
|
26
|
+
const { data: users, isLoading } = useQuery(['users'], fetchUsers);
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Server-cache libraries give you:
|
|
30
|
+
- Per-key caching + reuse across components.
|
|
31
|
+
- Stale-while-revalidate, auto-refetch on window focus / network.
|
|
32
|
+
- Optimistic updates with rollback.
|
|
33
|
+
- Request deduplication.
|
|
34
|
+
|
|
35
|
+
Don't reinvent these. They're not a minor convenience — they handle edge cases that take weeks to replicate.
|
|
36
|
+
|
|
37
|
+
## Derive, don't store
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
// ❌ stored
|
|
41
|
+
const [items, setItems] = useState([]);
|
|
42
|
+
const [total, setTotal] = useState(0);
|
|
43
|
+
// now every setItems must also update total — easy to forget
|
|
44
|
+
|
|
45
|
+
// ✅ derived
|
|
46
|
+
const [items, setItems] = useState([]);
|
|
47
|
+
const total = useMemo(() => items.reduce((s, i) => s + i.cents, 0), [items]);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Two pieces of state that must stay in sync = one piece of state.
|
|
51
|
+
|
|
52
|
+
## Single source of truth
|
|
53
|
+
|
|
54
|
+
When two components both "know" the same data, either:
|
|
55
|
+
- Lift the state to their common parent, OR
|
|
56
|
+
- Put it in a shared store.
|
|
57
|
+
|
|
58
|
+
Never duplicate.
|
|
59
|
+
|
|
60
|
+
## Local first, global last
|
|
61
|
+
|
|
62
|
+
Reach for global state only when you genuinely have cross-cutting concerns (auth user, theme, i18n). Otherwise, state that starts local can be lifted when needed. The opposite move (pull-back from global to local) is painful.
|
|
63
|
+
|
|
64
|
+
## Forms
|
|
65
|
+
|
|
66
|
+
Forms are half server state, half local. Libraries:
|
|
67
|
+
|
|
68
|
+
| Lib | Feel |
|
|
69
|
+
|---|---|
|
|
70
|
+
| React Hook Form | Uncontrolled-first, minimal re-renders |
|
|
71
|
+
| Formik | Controlled, rich ecosystem |
|
|
72
|
+
| TanStack Form | Typed, framework-agnostic, modern |
|
|
73
|
+
| Native `<form>` + validation | Underrated for simple cases |
|
|
74
|
+
|
|
75
|
+
For schema validation, reach for `zod` / `valibot` / `yup` and share the schema with the backend if possible.
|
|
76
|
+
|
|
77
|
+
Rules:
|
|
78
|
+
- Validate on submit (mandatory) + on blur (optional) + on change (for high-visibility fields like email uniqueness).
|
|
79
|
+
- Disable submit while submitting; prevent double-submit.
|
|
80
|
+
- Clear form fields only when the user expects it (e.g., after successful create; not after error).
|
|
81
|
+
|
|
82
|
+
## Optimistic UI
|
|
83
|
+
|
|
84
|
+
Show the expected result immediately. Reconcile on server response.
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
async function like(postId) {
|
|
88
|
+
// Optimistic
|
|
89
|
+
setPosts(posts => posts.map(p => p.id === postId ? { ...p, liked: true, likes: p.likes + 1 } : p));
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const updated = await api.like(postId);
|
|
93
|
+
setPosts(posts => posts.map(p => p.id === postId ? updated : p));
|
|
94
|
+
} catch (e) {
|
|
95
|
+
// Rollback
|
|
96
|
+
setPosts(posts => posts.map(p => p.id === postId ? { ...p, liked: false, likes: p.likes - 1 } : p));
|
|
97
|
+
toast.error('Failed to like');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Server-cache libraries make this pattern a one-liner. Use it for any user-initiated action where latency > ~50ms.
|
|
103
|
+
|
|
104
|
+
## Undo / stack
|
|
105
|
+
|
|
106
|
+
For multi-step flows or sensitive actions, keep a history:
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
const [history, setHistory] = useState([initial]);
|
|
110
|
+
const [index, setIndex] = useState(0);
|
|
111
|
+
const current = history[index];
|
|
112
|
+
|
|
113
|
+
function apply(action) {
|
|
114
|
+
const next = reducer(current, action);
|
|
115
|
+
setHistory(h => [...h.slice(0, index + 1), next]);
|
|
116
|
+
setIndex(i => i + 1);
|
|
117
|
+
}
|
|
118
|
+
function undo() { setIndex(i => Math.max(0, i - 1)); }
|
|
119
|
+
function redo() { setIndex(i => Math.min(history.length - 1, i + 1)); }
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Stores — when you need one
|
|
123
|
+
|
|
124
|
+
Pick based on complexity:
|
|
125
|
+
|
|
126
|
+
| Library | Feel |
|
|
127
|
+
|---|---|
|
|
128
|
+
| Zustand | Minimal, hook-based, great for small-to-medium |
|
|
129
|
+
| Jotai | Atoms; fine-grained reactivity |
|
|
130
|
+
| Redux Toolkit | Large apps with strict flow; heavy ceremony |
|
|
131
|
+
| MobX | Observable objects; reactive by mutation |
|
|
132
|
+
| Svelte stores | Built-in; simple |
|
|
133
|
+
| Pinia | Vue default; ergonomic |
|
|
134
|
+
|
|
135
|
+
Rule of thumb: start without a store. Add one when you pass the same piece of state through 4+ component levels of drilling.
|
|
136
|
+
|
|
137
|
+
Keep store slices small and feature-aligned (`authStore`, `cartStore`). One giant store is the 2015 Redux antipattern.
|
|
138
|
+
|
|
139
|
+
## Context — use for slow-changing values
|
|
140
|
+
|
|
141
|
+
Context rerenders every consumer whenever the value changes. Fine for theme, i18n, auth user. Bad for frequently-changing values (cursor position, scroll position).
|
|
142
|
+
|
|
143
|
+
Mitigations:
|
|
144
|
+
- Split contexts: one per slow-changing value.
|
|
145
|
+
- Use `useSyncExternalStore` / subscribe pattern for frequently-changing data.
|
|
146
|
+
|
|
147
|
+
## URL state
|
|
148
|
+
|
|
149
|
+
Filters, selected tab, pagination — put in the URL.
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
?status=active&sort=-createdAt&page=3
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Benefits:
|
|
156
|
+
- Bookmarkable, shareable, back/forward works.
|
|
157
|
+
- Reload restores state.
|
|
158
|
+
- Observable in analytics.
|
|
159
|
+
|
|
160
|
+
Frameworks provide query-param hooks (`useSearchParams`, `useRoute`). Treat the URL as part of your state system, not an afterthought.
|
|
161
|
+
|
|
162
|
+
## Persistence
|
|
163
|
+
|
|
164
|
+
Some state needs to survive reload: draft form content, selected theme, auth tokens.
|
|
165
|
+
|
|
166
|
+
| Where | For |
|
|
167
|
+
|---|---|
|
|
168
|
+
| `localStorage` | Preferences, form drafts (size limit ~5MB) |
|
|
169
|
+
| `sessionStorage` | Per-tab, cleared on close |
|
|
170
|
+
| `indexedDB` | Larger data, structured (offline caches) |
|
|
171
|
+
| Cookie | Auth session (HttpOnly, Secure; see `backend/security.md`) |
|
|
172
|
+
|
|
173
|
+
Rules:
|
|
174
|
+
- Never store tokens in `localStorage` (XSS-readable).
|
|
175
|
+
- Serialize deliberately — full store dumps change often and break older clients.
|
|
176
|
+
- Version the stored shape; migrate on read when structure evolves.
|
|
177
|
+
|
|
178
|
+
## Anti-patterns
|
|
179
|
+
|
|
180
|
+
| Anti-pattern | Fix |
|
|
181
|
+
|---|---|
|
|
182
|
+
| Global store holding fetched server data | Server-cache library |
|
|
183
|
+
| Deriving state in `useEffect` | Derive with `useMemo` / computed |
|
|
184
|
+
| Controlled forms with tons of re-renders | Uncontrolled + `react-hook-form` / equivalent |
|
|
185
|
+
| Multiple sources of truth for the same value | Pick one; others derive |
|
|
186
|
+
| Prop-drilling 5 levels deep | Context, composition, or store |
|
|
187
|
+
| Redux for 3-component apps | `useState` is enough |
|
|
188
|
+
| State updates inside render | Move to events / effects |
|
|
189
|
+
| Forgetting to reset state on "new" contexts (e.g., user logout) | Clear or keyed remount |
|
|
190
|
+
| Using `useEffect` to sync URL → state → URL | Derive from URL directly |
|