migrating

This commit is contained in:
Michael Freno
2025-12-17 00:23:13 -05:00
parent b3df3eedd2
commit 81969ae907
79 changed files with 4187 additions and 172 deletions

441
src/lib/SOLID-PATTERNS.md Normal file
View File

@@ -0,0 +1,441 @@
# React to SolidJS Conversion Patterns
This guide documents common patterns for converting React code to SolidJS for this migration.
## Table of Contents
- [State Management](#state-management)
- [Effects](#effects)
- [Refs](#refs)
- [Routing](#routing)
- [Conditional Rendering](#conditional-rendering)
- [Lists](#lists)
- [Forms](#forms)
- [Event Handlers](#event-handlers)
## State Management
### React (useState)
```tsx
import { useState } from "react";
const [count, setCount] = useState(0);
const [user, setUser] = useState<User | null>(null);
// Update
setCount(count + 1);
setCount(prev => prev + 1);
setUser({ ...user, name: "John" });
```
### Solid (createSignal)
```tsx
import { createSignal } from "solid-js";
const [count, setCount] = createSignal(0);
const [user, setUser] = createSignal<User | null>(null);
// Update - note the function call to read value
setCount(count() + 1);
setCount(prev => prev + 1);
setUser({ ...user(), name: "John" });
// ⚠️ Important: Always call the signal to read its value
console.log(count()); // ✅ Correct
console.log(count); // ❌ Wrong - this is the function itself
```
## Effects
### React (useEffect)
```tsx
import { useEffect } from "react";
// Run once on mount
useEffect(() => {
console.log("Mounted");
return () => {
console.log("Cleanup");
};
}, []);
// Run when dependency changes
useEffect(() => {
console.log(count);
}, [count]);
```
### Solid (createEffect / onMount / onCleanup)
```tsx
import { createEffect, onMount, onCleanup } from "solid-js";
// Run once on mount
onMount(() => {
console.log("Mounted");
});
// Cleanup
onCleanup(() => {
console.log("Cleanup");
});
// Run when dependency changes (automatic tracking)
createEffect(() => {
console.log(count()); // Automatically tracks count signal
});
// ⚠️ Important: Effects automatically track any signal reads
// No dependency array needed!
```
## Refs
### React (useRef)
```tsx
import { useRef } from "react";
const inputRef = useRef<HTMLInputElement>(null);
// Access
inputRef.current?.focus();
// In JSX
<input ref={inputRef} />
```
### Solid (let binding or signal)
```tsx
// Method 1: Direct binding (preferred for simple cases)
let inputRef: HTMLInputElement | undefined;
// Access
inputRef?.focus();
// In JSX
<input ref={inputRef} />
// Method 2: Using a signal (for reactive refs)
import { createSignal } from "solid-js";
const [inputRef, setInputRef] = createSignal<HTMLInputElement>();
// Access
inputRef()?.focus();
// In JSX
<input ref={setInputRef} />
```
## Routing
### React (Next.js)
```tsx
import Link from "next/link";
import { useRouter } from "next/navigation";
// Link component
<Link href="/about">About</Link>
// Programmatic navigation
const router = useRouter();
router.push("/dashboard");
router.back();
router.refresh();
```
### Solid (SolidStart)
```tsx
import { A, useNavigate } from "@solidjs/router";
// Link component
<A href="/about">About</A>
// Programmatic navigation
const navigate = useNavigate();
navigate("/dashboard");
navigate(-1); // Go back
// Note: No refresh() - Solid is reactive by default
```
## Conditional Rendering
### React
```tsx
// Using && operator
{isLoggedIn && <Dashboard />}
// Using ternary
{isLoggedIn ? <Dashboard /> : <Login />}
// Using if statement
if (loading) return <Spinner />;
return <Content />;
```
### Solid
```tsx
import { Show } from "solid-js";
// Using Show component (recommended)
<Show when={isLoggedIn()}>
<Dashboard />
</Show>
// With fallback
<Show when={isLoggedIn()} fallback={<Login />}>
<Dashboard />
</Show>
// ⚠️ Important: Can still use && and ternary, but Show is more efficient
// because it doesn't recreate the DOM on every change
// Early return still works
if (loading()) return <Spinner />;
return <Content />;
```
## Lists
### React
```tsx
// Using map
{users.map(user => (
<div key={user.id}>{user.name}</div>
))}
// With index
{users.map((user, index) => (
<div key={index}>{user.name}</div>
))}
```
### Solid
```tsx
import { For, Index } from "solid-js";
// Using For (when items have stable keys)
<For each={users()}>
{(user) => <div>{user.name}</div>}
</For>
// With index
<For each={users()}>
{(user, index) => <div>{index()} - {user.name}</div>}
</For>
// Using Index (when items have no stable identity, keyed by index)
<Index each={users()}>
{(user, index) => <div>{index} - {user().name}</div>}
</Index>
// ⚠️ Key differences:
// - For: Better when items have stable identity (keyed by reference)
// - Index: Better when items change frequently (keyed by index)
// - Note the () on user in Index component
```
## Forms
### React
```tsx
import { useState } from "react";
const [email, setEmail] = useState("");
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log(email);
};
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</form>
```
### Solid
```tsx
import { createSignal } from "solid-js";
const [email, setEmail] = createSignal("");
const handleSubmit = (e: Event) => {
e.preventDefault();
console.log(email());
};
<form onSubmit={handleSubmit}>
<input
type="email"
value={email()}
onInput={(e) => setEmail(e.currentTarget.value)}
/>
</form>
// ⚠️ Important differences:
// - Use onInput instead of onChange for real-time updates
// - onChange fires on blur in Solid
// - Use e.currentTarget instead of e.target for type safety
```
## Event Handlers
### React
```tsx
// Inline
<button onClick={() => setCount(count + 1)}>
// Function reference
<button onClick={handleClick}>
// With parameters
<button onClick={(e) => handleClick(e, id)}>
```
### Solid
```tsx
// Inline - same as React
<button onClick={() => setCount(count() + 1)}>
// Function reference - same as React
<button onClick={handleClick}>
// With parameters - same as React
<button onClick={(e) => handleClick(e, id)}>
// Alternative syntax with array (for optimization)
<button onClick={[handleClick, id]}>
// ⚠️ Important: Remember to call signals with ()
<button onClick={() => setCount(count() + 1)}> // ✅
<button onClick={() => setCount(count + 1)}> // ❌
```
## Server Actions to tRPC
### React (Next.js Server Actions)
```tsx
"use server";
export async function updateProfile(displayName: string) {
const userId = cookies().get("userIDToken");
await db.execute("UPDATE User SET display_name = ? WHERE id = ?",
[displayName, userId]);
return { success: true };
}
// Client usage
import { updateProfile } from "./actions";
const result = await updateProfile("John");
```
### Solid (tRPC)
```tsx
// Server (in src/server/api/routers/user.ts)
updateDisplayName: publicProcedure
.input(z.object({ displayName: z.string() }))
.mutation(async ({ input, ctx }) => {
const userId = await getUserID(ctx.event);
const conn = ConnectionFactory();
await conn.execute({
sql: "UPDATE User SET display_name = ? WHERE id = ?",
args: [input.displayName, userId]
});
return { success: true };
})
// Client usage
import { api } from "~/lib/api";
const result = await api.user.updateDisplayName.mutate({
displayName: "John"
});
```
## Common Gotchas
### 1. Reading Signal Values
```tsx
// ❌ WRONG
const value = mySignal; // This is the function, not the value!
// ✅ CORRECT
const value = mySignal(); // Call it to get the value
```
### 2. Updating Objects in Signals
```tsx
// ❌ WRONG - Mutating directly
const [user, setUser] = createSignal({ name: "John" });
user().name = "Jane"; // This won't trigger reactivity!
// ✅ CORRECT - Create new object
setUser({ ...user(), name: "Jane" });
// ✅ ALSO CORRECT - Using produce (from solid-js/store)
import { produce } from "solid-js/store";
setUser(produce(u => { u.name = "Jane"; }));
```
### 3. Conditional Effects
```tsx
// ❌ WRONG - Effect won't re-run when condition changes
if (someCondition()) {
createEffect(() => {
// This only creates the effect if condition is true initially
});
}
// ✅ CORRECT - Effect tracks condition reactively
createEffect(() => {
if (someCondition()) {
// This runs whenever condition or dependencies change
}
});
```
### 4. Cleanup in Effects
```tsx
// ❌ WRONG
createEffect(() => {
const timer = setInterval(() => {}, 1000);
// No cleanup!
});
// ✅ CORRECT
createEffect(() => {
const timer = setInterval(() => {}, 1000);
onCleanup(() => {
clearInterval(timer);
});
});
```
## Quick Reference Card
| React | Solid | Notes |
|-------|-------|-------|
| `useState` | `createSignal` | Call signal to read: `count()` |
| `useEffect` | `createEffect` | Auto-tracks dependencies |
| `useRef` | `let` binding | Or use signal for reactive refs |
| `useRouter()` | `useNavigate()` | Different API |
| `Link` | `A` | Different import |
| `{cond && <A />}` | `<Show when={cond()}><A /></Show>` | Show is more efficient |
| `{arr.map()}` | `<For each={arr()}></For>` | For is more efficient |
| `onChange` | `onInput` | onChange fires on blur |
| `e.target` | `e.currentTarget` | Better types |
| "use server" | tRPC router | Different architecture |
## Tips for Success
1. **Always call signals to read their values**: `count()` not `count`
2. **Use Show and For components**: More efficient than && and map
3. **Effects auto-track**: No dependency arrays needed
4. **Immutable updates**: Always create new objects when updating signals
5. **Use onCleanup**: Clean up timers, subscriptions, etc.
6. **Type safety**: SolidJS has excellent TypeScript support - use it!

72
src/lib/cookies.client.ts Normal file
View File

@@ -0,0 +1,72 @@
/**
* Client-side cookie utilities
* For use in browser/client components only
*/
/**
* Get cookie value on the client (browser)
*/
export function getClientCookie(name: string): string | undefined {
if (typeof document === "undefined") return undefined;
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) {
return parts.pop()?.split(";").shift();
}
return undefined;
}
/**
* Set cookie on the client (browser)
*/
export function setClientCookie(
name: string,
value: string,
options?: {
maxAge?: number;
expires?: Date;
path?: string;
secure?: boolean;
sameSite?: "strict" | "lax" | "none";
}
) {
if (typeof document === "undefined") return;
let cookieString = `${name}=${value}`;
if (options?.maxAge) {
cookieString += `; max-age=${options.maxAge}`;
}
if (options?.expires) {
cookieString += `; expires=${options.expires.toUTCString()}`;
}
if (options?.path) {
cookieString += `; path=${options.path}`;
} else {
cookieString += "; path=/";
}
if (options?.secure) {
cookieString += "; secure";
}
if (options?.sameSite) {
cookieString += `; samesite=${options.sameSite}`;
}
document.cookie = cookieString;
}
/**
* Delete cookie on the client (browser)
*/
export function deleteClientCookie(name: string) {
if (typeof document === "undefined") return;
document.cookie = `${name}=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
}

111
src/lib/cookies.ts Normal file
View File

@@ -0,0 +1,111 @@
/**
* Cookie utilities for SolidStart
* Provides client and server-side cookie management
*/
import { getCookie as getServerCookie, setCookie as setServerCookie } from "vinxi/http";
import type { H3Event } from "vinxi/http";
/**
* Get cookie value on the server
*/
export function getCookie(event: H3Event, name: string): string | undefined {
return getServerCookie(event, name);
}
/**
* Set cookie on the server
*/
export function setCookie(
event: H3Event,
name: string,
value: string,
options?: {
maxAge?: number;
expires?: Date;
httpOnly?: boolean;
secure?: boolean;
sameSite?: "strict" | "lax" | "none";
path?: string;
}
) {
setServerCookie(event, name, value, options);
}
/**
* Delete cookie on the server
*/
export function deleteCookie(event: H3Event, name: string) {
setServerCookie(event, name, "", {
maxAge: 0,
expires: new Date("2016-10-05"),
});
}
/**
* Get cookie value on the client (browser)
*/
export function getClientCookie(name: string): string | undefined {
if (typeof document === "undefined") return undefined;
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) {
return parts.pop()?.split(";").shift();
}
return undefined;
}
/**
* Set cookie on the client (browser)
*/
export function setClientCookie(
name: string,
value: string,
options?: {
maxAge?: number;
expires?: Date;
path?: string;
secure?: boolean;
sameSite?: "strict" | "lax" | "none";
}
) {
if (typeof document === "undefined") return;
let cookieString = `${name}=${value}`;
if (options?.maxAge) {
cookieString += `; max-age=${options.maxAge}`;
}
if (options?.expires) {
cookieString += `; expires=${options.expires.toUTCString()}`;
}
if (options?.path) {
cookieString += `; path=${options.path}`;
} else {
cookieString += "; path=/";
}
if (options?.secure) {
cookieString += "; secure";
}
if (options?.sameSite) {
cookieString += `; samesite=${options.sameSite}`;
}
document.cookie = cookieString;
}
/**
* Delete cookie on the client (browser)
*/
export function deleteClientCookie(name: string) {
if (typeof document === "undefined") return;
document.cookie = `${name}=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
}

95
src/lib/validation.ts Normal file
View File

@@ -0,0 +1,95 @@
/**
* Form validation utilities
*/
/**
* Validate email format
*/
export function isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
/**
* Validate password strength
*/
export function validatePassword(password: string): {
isValid: boolean;
errors: string[];
} {
const errors: string[] = [];
if (password.length < 8) {
errors.push("Password must be at least 8 characters long");
}
// Optional: Add more password requirements
// if (!/[A-Z]/.test(password)) {
// errors.push("Password must contain at least one uppercase letter");
// }
// if (!/[a-z]/.test(password)) {
// errors.push("Password must contain at least one lowercase letter");
// }
// if (!/[0-9]/.test(password)) {
// errors.push("Password must contain at least one number");
// }
return {
isValid: errors.length === 0,
errors,
};
}
/**
* Check if two passwords match
*/
export function passwordsMatch(password: string, confirmation: string): boolean {
return password === confirmation && password.length > 0;
}
/**
* Validate display name
*/
export function isValidDisplayName(name: string): boolean {
return name.trim().length >= 1 && name.trim().length <= 50;
}
/**
* Sanitize user input (basic XSS prevention)
*/
export function sanitizeInput(input: string): string {
return input
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#x27;")
.replace(/\//g, "&#x2F;");
}
/**
* Check if string is a valid URL
*/
export function isValidUrl(url: string): boolean {
try {
new URL(url);
return true;
} catch {
return false;
}
}
/**
* Validate file type for uploads
*/
export function isValidImageType(file: File): boolean {
const validTypes = ["image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp"];
return validTypes.includes(file.type);
}
/**
* Validate file size (in bytes)
*/
export function isValidFileSize(file: File, maxSizeMB: number = 5): boolean {
const maxBytes = maxSizeMB * 1024 * 1024;
return file.size <= maxBytes;
}