@raftlabs/raftstack 1.0.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.
@@ -0,0 +1,418 @@
1
+ ---
2
+ name: react
3
+ description: Use when writing React components, using hooks, handling state, working with Next.js/Remix/Vite/Astro, or when components feel bloated, have unnecessary re-renders, or violate single responsibility
4
+ ---
5
+
6
+ # React Development
7
+
8
+ ## Overview
9
+
10
+ Write components that are small, focused, and follow framework conventions. Detect the framework first - patterns differ significantly between Next.js App Router, Remix, Vite SPA, and Astro.
11
+
12
+ ## When to Use
13
+
14
+ - Creating any React component
15
+ - Choosing state management approach
16
+ - Deciding client vs server rendering
17
+ - Optimizing re-renders
18
+ - Setting up data fetching
19
+
20
+ ## Framework Detection
21
+
22
+ **Always check before writing React code:**
23
+
24
+ ```typescript
25
+ // Check package.json for framework
26
+ import pkg from './package.json';
27
+
28
+ const framework = detectFramework(pkg.dependencies);
29
+ // 'next' → Next.js (check for app/ vs pages/)
30
+ // '@remix-run/react' → Remix
31
+ // 'astro' → Astro
32
+ // 'vite' only → Vite SPA
33
+ // none → Create React App or custom
34
+ ```
35
+
36
+ | File/Config | Framework | Router Type |
37
+ |------------|-----------|-------------|
38
+ | `app/` directory + `next.config` | Next.js App Router | File-based, RSC |
39
+ | `pages/` directory + `next.config` | Next.js Pages Router | File-based, CSR/SSR |
40
+ | `remix.config.js` or `@remix-run/*` | Remix | Nested, loader-based |
41
+ | `astro.config.mjs` | Astro | Islands, mostly static |
42
+ | `vite.config.ts` only | Vite SPA | Client-side only |
43
+
44
+ ## The Iron Rules
45
+
46
+ ### 1. 'use client' Goes at File Top - NEVER Middle
47
+
48
+ ```typescript
49
+ // ❌ FATAL: This breaks compilation
50
+ function ServerComponent() { ... }
51
+
52
+ 'use client'; // WRONG - can't be after other code
53
+
54
+ function ClientComponent() { ... }
55
+
56
+ // ✅ CORRECT: Separate files
57
+ // -- server-component.tsx --
58
+ export function ServerComponent() { ... }
59
+
60
+ // -- client-component.tsx --
61
+ 'use client';
62
+ export function ClientComponent() { ... }
63
+ ```
64
+
65
+ **Never write "for demonstration" code that violates this.** Broken code teaches nothing.
66
+
67
+ ### 2. Server Components by Default (Next.js App Router)
68
+
69
+ Data fetching belongs on the server. Only add 'use client' when you need:
70
+ - Event handlers (onClick, onChange)
71
+ - useState, useEffect, useReducer
72
+ - Browser APIs (localStorage, window)
73
+
74
+ **Keep 'use client' boundaries deep** to minimize JavaScript bundle size.
75
+
76
+ ```typescript
77
+ // ✅ GOOD: Server Component fetches data
78
+ // app/products/page.tsx
79
+ async function ProductsPage() {
80
+ const products = await db.product.findMany(); // Direct DB access
81
+ return <ProductList products={products} />;
82
+ }
83
+
84
+ // ❌ BAD: Client Component fetches in useEffect
85
+ 'use client';
86
+ function ProductsPage() {
87
+ const [products, setProducts] = useState([]);
88
+ useEffect(() => {
89
+ fetch('/api/products').then(...) // Extra round-trip
90
+ }, []);
91
+ }
92
+
93
+ // ✅ GOOD: Mixed - only Search is client
94
+ function Layout({ children }) {
95
+ return (
96
+ <nav>
97
+ <Logo /> {/* Server Component */}
98
+ <Search /> {/* 'use client' - only this interactive */}
99
+ </nav>
100
+ );
101
+ }
102
+ ```
103
+
104
+ ### 3. Component Responsibilities: Max 3 Concerns
105
+
106
+ A component should handle at most 3 concerns:
107
+ 1. Data → Layout → Interaction (orchestrator)
108
+ 2. Fetch → Transform → Display (data component)
109
+ 3. State → Render → Events (interactive component)
110
+
111
+ If you have more, split.
112
+
113
+ ```typescript
114
+ // ❌ BAD: Too many concerns
115
+ function ProductPage() {
116
+ // Fetching
117
+ // Validation
118
+ // Cart logic
119
+ // Analytics
120
+ // Rendering
121
+ // Error handling
122
+ }
123
+
124
+ // ✅ GOOD: Split by concern
125
+ function ProductPage() {
126
+ return (
127
+ <ProductDataProvider>
128
+ <ProductDisplay />
129
+ <AddToCartButton />
130
+ </ProductDataProvider>
131
+ );
132
+ }
133
+ ```
134
+
135
+ ### 4. State Placement Decision Tree
136
+
137
+ ```
138
+ Need state?
139
+ ├── Used by 1 component → useState in that component
140
+ ├── Used by siblings → lift to parent
141
+ ├── Used across routes → URL params or context
142
+ ├── Complex/async → useReducer or server state (TanStack Query)
143
+ └── Global → Zustand or Jotai (NOT Redux unless massive app)
144
+ ```
145
+
146
+ ### 5. Hooks Rules
147
+
148
+ ```typescript
149
+ // ❌ BAD: Conditional hook
150
+ if (condition) {
151
+ useEffect(() => { ... }); // Breaks rules of hooks
152
+ }
153
+
154
+ // ✅ GOOD: Condition inside hook
155
+ useEffect(() => {
156
+ if (!condition) return;
157
+ // ... effect logic
158
+ }, [condition]);
159
+
160
+ // ❌ BAD: Too many useState
161
+ const [name, setName] = useState('');
162
+ const [email, setEmail] = useState('');
163
+ const [error, setError] = useState(null);
164
+ const [loading, setLoading] = useState(false);
165
+ const [submitted, setSubmitted] = useState(false);
166
+
167
+ // ✅ GOOD: Related state in reducer
168
+ const [formState, dispatch] = useReducer(formReducer, initialState);
169
+ ```
170
+
171
+ ### 6. Server Actions for Forms (Next.js App Router)
172
+
173
+ Server Actions handle mutations without API routes. Use `useActionState` (React 19+) for form state.
174
+
175
+ ```typescript
176
+ // ✅ GOOD: Server Action with validation
177
+ 'use server';
178
+ import { z } from 'zod';
179
+
180
+ const schema = z.object({
181
+ email: z.string().email(),
182
+ name: z.string().min(2)
183
+ });
184
+
185
+ export async function createUser(prevState: any, formData: FormData) {
186
+ const validatedFields = schema.safeParse({
187
+ email: formData.get('email'),
188
+ name: formData.get('name')
189
+ });
190
+
191
+ if (!validatedFields.success) {
192
+ return { errors: validatedFields.error.flatten().fieldErrors };
193
+ }
194
+
195
+ // Return error object, DON'T throw
196
+ const result = await db.user.create(validatedFields.data);
197
+ if (!result.ok) return { message: 'Failed to create user' };
198
+
199
+ redirect('/users');
200
+ }
201
+
202
+ // Client Component using the action
203
+ 'use client';
204
+ import { useActionState } from 'react';
205
+
206
+ export function SignupForm() {
207
+ const [state, formAction, isPending] = useActionState(createUser, { message: '' });
208
+
209
+ return (
210
+ <form action={formAction}>
211
+ <input type="email" name="email" required />
212
+ {state?.errors?.email && <p>{state.errors.email}</p>}
213
+
214
+ <input type="text" name="name" required />
215
+ {state?.errors?.name && <p>{state.errors.name}</p>}
216
+
217
+ <button disabled={isPending}>Sign up</button>
218
+ {state?.message && <p aria-live="polite">{state.message}</p>}
219
+ </form>
220
+ );
221
+ }
222
+ ```
223
+
224
+ **Key patterns:**
225
+ - Server Actions return error objects, never throw
226
+ - Use `useActionState` for form state + pending + errors
227
+ - Progressive enhancement: works without JS
228
+ - Validate with Zod in Server Action
229
+
230
+ ## React 19 Features
231
+
232
+ ### use() Hook - Read Promises/Context
233
+
234
+ ```typescript
235
+ import { use } from 'react';
236
+
237
+ // ✅ Read promise in component
238
+ function Message({ messagePromise }: { messagePromise: Promise<string> }) {
239
+ const message = use(messagePromise); // Suspends until resolved
240
+ return <p>{message}</p>;
241
+ }
242
+
243
+ // ✅ Read context without Consumer
244
+ function Button() {
245
+ const theme = use(ThemeContext);
246
+ return <button className={theme}>Click</button>;
247
+ }
248
+ ```
249
+
250
+ ### useOptimistic - Instant UI Updates
251
+
252
+ ```typescript
253
+ 'use client';
254
+ import { useOptimistic } from 'react';
255
+
256
+ function TodoList({ todos }: { todos: Todo[] }) {
257
+ const [optimisticTodos, addOptimistic] = useOptimistic(
258
+ todos,
259
+ (state, newTodo: Todo) => [...state, newTodo]
260
+ );
261
+
262
+ async function handleAdd(formData: FormData) {
263
+ const newTodo = { id: Date.now(), text: formData.get('todo') };
264
+ addOptimistic(newTodo); // Instant update
265
+ await addTodo(formData); // Server mutation
266
+ }
267
+
268
+ return (
269
+ <form action={handleAdd}>
270
+ <input name="todo" />
271
+ <button type="submit">Add</button>
272
+ <ul>
273
+ {optimisticTodos.map(todo => <li key={todo.id}>{todo.text}</li>)}
274
+ </ul>
275
+ </form>
276
+ );
277
+ }
278
+ ```
279
+
280
+ **Auto-reverts on error** - no manual rollback needed.
281
+
282
+ ## Streaming & Suspense
283
+
284
+ ### Full Page Streaming with loading.tsx
285
+
286
+ ```typescript
287
+ // app/posts/loading.tsx
288
+ export default function Loading() {
289
+ return <PostsSkeleton />;
290
+ }
291
+
292
+ // app/posts/page.tsx (automatically wrapped in Suspense)
293
+ export default async function PostsPage() {
294
+ const posts = await fetchPosts(); // Streams when ready
295
+ return <PostsList posts={posts} />;
296
+ }
297
+ ```
298
+
299
+ ### Granular Streaming with Suspense
300
+
301
+ ```typescript
302
+ import { Suspense } from 'react';
303
+
304
+ export default function Dashboard() {
305
+ return (
306
+ <div>
307
+ <h1>Dashboard</h1> {/* Sent immediately */}
308
+
309
+ <Suspense fallback={<StatsSkeleton />}>
310
+ <Stats /> {/* Async, streams later */}
311
+ </Suspense>
312
+
313
+ <Suspense fallback={<ChartSkeleton />}>
314
+ <RevenueChart /> {/* Async, independent stream */}
315
+ </Suspense>
316
+ </div>
317
+ );
318
+ }
319
+ ```
320
+
321
+ **Benefits:**
322
+ - TTFB improvement - show shell instantly
323
+ - Independent loading states
324
+ - Partial prefetching for faster navigation
325
+
326
+ ## Quick Reference: Framework Patterns
327
+
328
+ | Task | Next.js App Router | Remix | Vite SPA |
329
+ |------|-------------------|-------|----------|
330
+ | Data fetching | async component | loader function | useQuery |
331
+ | Mutations | Server Action | action function | useMutation |
332
+ | Loading UI | loading.tsx | useNavigation | suspense |
333
+ | Error UI | error.tsx | ErrorBoundary | ErrorBoundary |
334
+ | Metadata | metadata export | meta function | react-helmet |
335
+ | Navigation | Link + router | Link + navigate | Link + navigate |
336
+
337
+ ## Testing Strategy
338
+
339
+ | What to Test | How |
340
+ |--------------|-----|
341
+ | Server Components | Import directly, verify data fetching |
342
+ | Client Components | Render with Testing Library, verify interactions |
343
+ | Server Actions | Call with FormData, assert return values |
344
+ | Suspense fallbacks | Wait for async resolution, verify both states |
345
+ | Forms with useActionState | Submit form, verify pending/error/success states |
346
+
347
+ ```typescript
348
+ // Test Server Component
349
+ import ProductsPage from '@/app/products/page';
350
+
351
+ test('fetches and displays products', async () => {
352
+ const result = await ProductsPage(); // It's an async function
353
+ expect(result.props.children).toContain('Product 1');
354
+ });
355
+
356
+ // Test Server Action
357
+ import { createUser } from '@/app/actions';
358
+
359
+ test('validates email format', async () => {
360
+ const formData = new FormData();
361
+ formData.set('email', 'invalid');
362
+
363
+ const result = await createUser(null, formData);
364
+ expect(result.errors.email).toBeDefined();
365
+ });
366
+
367
+ // Test Client Component with Suspense
368
+ import { render, waitFor } from '@testing-library/react';
369
+
370
+ test('shows fallback then content', async () => {
371
+ const { getByText, queryByText } = render(
372
+ <Suspense fallback={<div>Loading...</div>}>
373
+ <AsyncComponent />
374
+ </Suspense>
375
+ );
376
+
377
+ expect(getByText('Loading...')).toBeInTheDocument();
378
+ await waitFor(() => expect(queryByText('Loading...')).not.toBeInTheDocument());
379
+ expect(getByText('Content')).toBeInTheDocument();
380
+ });
381
+ ```
382
+
383
+ ## References
384
+
385
+ - [Next.js Documentation](https://nextjs.org/docs) - App Router, Server Components, Server Actions
386
+ - [React 19 Release](https://react.dev/blog/2024/12/05/react-19) - use(), useActionState, useOptimistic
387
+ - [React Server Components](https://react.dev/reference/rsc/server-components) - Official RSC guide
388
+
389
+ **Version Notes:**
390
+ - Next.js 14+: App Router stable, Server Actions stable
391
+ - Next.js 15: Enhanced caching, Turbopack, React 19 support
392
+ - React 19: use(), Actions, useOptimistic, useActionState
393
+
394
+ ## Red Flags - STOP and Restructure
395
+
396
+ | Thought | Reality |
397
+ |---------|---------|
398
+ | "I'll put 'use client' here just for demo" | Broken code teaches nothing. Write it correctly. |
399
+ | "This component does a lot but it's fine" | Max 3 concerns. Split now. |
400
+ | "I'll fetch client-side, it's simpler" | Server fetching is simpler AND faster. |
401
+ | "Let me add useState for this" | Check: can it be URL params, server state, or derived? |
402
+ | "Redux for this small app" | Zustand/Jotai unless you have 50+ reducers. |
403
+ | "I need useEffect for this fetch" | In Next.js/Remix: no. Use server components or loaders. |
404
+ | "I'll create an API route for this form" | Use Server Actions - no API route needed. |
405
+ | "Let me throw an error in Server Action" | Return error object for useActionState. Don't throw. |
406
+ | "I'll add 'use client' to the whole layout" | Keep boundaries deep. Only interactive parts need it. |
407
+ | "Forms need JavaScript to work" | Server Actions work with progressive enhancement. |
408
+
409
+ ## Common Mistakes
410
+
411
+ | Mistake | Fix |
412
+ |---------|-----|
413
+ | 'use client' not at file top | Move to separate file |
414
+ | useEffect for data fetching in Next.js | Server component or loader |
415
+ | Prop drilling 3+ levels | Context or composition |
416
+ | Re-render on every keystroke | Debounce or uncontrolled input |
417
+ | window/localStorage in server component | 'use client' or dynamic import |
418
+ | Inline function in JSX causing re-renders | useCallback or extract |