Development Guide
This guide covers common development tasks and workflows for working with FVN.li's React + Inertia.js frontend.
Getting Started
Prerequisites
Ensure you have the following installed:
Initial Setup
Start DDEV environment:
ddev start
Install dependencies:
ddev composer install
ddev pnpm install
Set up environment:
cp .env.example .env
ddev artisan key:generate
ddev artisan migrate
Start development server:
ddev pnpm dev
Access the application at https://fvn-li.ddev.site
Development Workflow
Running the Dev Server
The Vite development server provides hot module replacement (HMR) for instant feedback:
# Start Vite dev server
ddev pnpm dev
Features:
Hot Module Replacement: Changes appear instantly without page reload
Fast Refresh: React state is preserved during updates
Error Overlay: Build errors appear in the browser
Source Maps: Debug with original TypeScript source
Type Checking
Run TypeScript compiler to check for type errors:
ddev pnpm types
This runs tsc --noEmit
to check types without generating output files.
Linting
Check and fix code style issues:
# Check for issues
ddev pnpm lint
# Auto-fix issues
ddev pnpm lint --fix
ESLint configuration includes:
Format code with Prettier:
# Format all files
ddev pnpm format
# Check formatting without changes
ddev pnpm format:check
Prettier is configured to:
Building for Production
Client-Side Only
ddev pnpm build
This creates optimized production assets in public/build/
.
With SSR Support
ddev pnpm build:ssr
This builds both client and server bundles:
Creating Components
Basic Component
Create a new component in resources/js/components/
:
// resources/js/components/MyComponent.tsx
import React from 'react';
interface MyComponentProps {
title: string;
description?: string;
}
export default function MyComponent({ title, description }: MyComponentProps) {
return (
<div className="rounded-lg border border-gray-200 p-4 dark:border-gray-700">
<h3 className="text-lg font-semibold">{title}</h3>
{description && (
<p className="mt-2 text-gray-600 dark:text-gray-400">
{description}
</p>
)}
</div>
);
}
Page Component
Create a new page in resources/js/pages/
:
// resources/js/pages/example/index.tsx
import React from 'react';
import { Head } from '@inertiajs/react';
interface ExamplePageProps {
items: Array<{ id: number; name: string }>;
}
export default function ExamplePage({ items }: ExamplePageProps) {
return (
<>
<Head title="Example Page" />
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold">Example Page</h1>
<div className="mt-6 grid gap-4">
{items.map((item) => (
<div key={item.id} className="rounded-lg border p-4">
{item.name}
</div>
))}
</div>
</div>
</>
);
}
Custom Hook
Create reusable logic in resources/js/hooks/
:
// resources/js/hooks/useLocalStorage.ts
import { useState, useEffect } from 'react';
export function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error('Error saving to localStorage:', error);
}
}, [key, value]);
return [value, setValue] as const;
}
Working with Inertia
Navigation
Use Inertia's Link
component for navigation:
import { Link } from '@inertiajs/react';
<Link
href={route('games.show', { game: gameId })}
className="text-blue-600 hover:underline"
>
View Game
</Link>
Use Inertia's form helpers for form handling:
import { useForm } from '@inertiajs/react';
export default function CreateForm() {
const { data, setData, post, processing, errors } = useForm({
name: '',
description: '',
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
post(route('items.store'));
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="name">Name</label>
<input
id="name"
type="text"
value={data.name}
onChange={(e) => setData('name', e.target.value)}
/>
{errors.name && <div className="text-red-600">{errors.name}</div>}
</div>
<button type="submit" disabled={processing}>
{processing ? 'Saving...' : 'Save'}
</button>
</form>
);
}
Shared Data
Access shared data from the page props:
import { usePage } from '@inertiajs/react';
import type { PageProps } from '@/types';
export default function MyComponent() {
const { auth, flash } = usePage<PageProps>().props;
return (
<div>
{auth.user && <p>Welcome, {auth.user.name}!</p>}
{flash.message && <div className="alert">{flash.message}</div>}
</div>
);
}
Styling with Tailwind
Using Tailwind Classes
<div className="flex items-center justify-between rounded-lg bg-white p-4 shadow-sm dark:bg-gray-800">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
Title
</h2>
<button className="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700">
Action
</button>
</div>
Dark Mode
Use Tailwind's dark mode classes:
<div className="bg-white text-gray-900 dark:bg-gray-900 dark:text-white">
Content adapts to dark mode
</div>
The application automatically detects system preference and allows user override.
Responsive Design
Use Tailwind's responsive prefixes:
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{/* Responsive grid */}
</div>
Testing
E2E Tests with Playwright
Create tests in tests/e2e/specs/
:
// tests/e2e/specs/example.spec.ts
import { test, expect } from '@playwright/test';
test('homepage loads correctly', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle(/FVN.li/);
await expect(page.locator('h1')).toBeVisible();
});
test('search functionality works', async ({ page }) => {
await page.goto('/games');
await page.fill('[placeholder="Search games..."]', 'visual novel');
await page.keyboard.press('Enter');
await expect(page).toHaveURL(/search=visual\+novel/);
});
Run tests:
# Run all tests
ddev pnpm test:e2e
# Run in UI mode
ddev pnpm test:e2e:ui
# Run specific test file
ddev playwright test tests/e2e/specs/example.spec.ts
Accessibility Testing
# Run accessibility tests
ddev pnpm test:a11y
# View accessibility report
ddev pnpm test:a11y:report
Common Tasks
Adding a New Route
Create the page component:
// resources/js/pages/my-feature/index.tsx
export default function MyFeature() {
return <div>My Feature</div>;
}
Add the route in Laravel:
// routes/web.php
Route::get('/my-feature', [MyFeatureController::class, 'index'])
->name('my-feature.index');
Create the controller:
// app/Http/Controllers/MyFeatureController.php
public function index()
{
return inertia('my-feature/index', [
'data' => MyModel::all(),
]);
}
Adding TypeScript Types
Define types in resources/js/types/index.ts
:
export interface Game {
id: number;
title: string;
description: string;
rating: number;
created_at: string;
}
export interface PageProps {
auth: {
user: User | null;
};
flash: {
message?: string;
error?: string;
};
}
Using Environment Variables
Access Vite environment variables:
const appName = import.meta.env.VITE_APP_NAME;
const apiUrl = import.meta.env.VITE_API_URL;
Define in .env
:
VITE_APP_NAME="FVN.li"
VITE_API_URL="https://api.fvn.li"
Debugging
React DevTools: Install the React DevTools browser extension
Inertia DevTools: Available in development mode
Vue DevTools: Can also inspect Inertia state
Console Logging
// Development only logging
if (import.meta.env.DEV) {
console.log('Debug info:', data);
}
Source Maps
Source maps are enabled in development, allowing you to debug TypeScript source directly in the browser.
Code Splitting
Lazy load heavy components:
import { lazy, Suspense } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
export default function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
);
}
Memoization
Use React.memo for expensive components:
import { memo } from 'react';
const ExpensiveComponent = memo(function ExpensiveComponent({ data }) {
// Expensive rendering logic
return <div>{/* ... */}</div>;
});
Image Optimization
Use appropriate image formats and sizes:
<img
src={game.thumbnail}
srcSet={`${game.thumbnail_2x} 2x`}
alt={game.title}
loading="lazy"
className="h-48 w-full object-cover"
/>
Troubleshooting
HMR Not Working
Check that Vite dev server is running
Verify DDEV configuration in vite.config.ts
Clear browser cache
Restart Vite dev server
Type Errors
Run ddev pnpm types
to see all type errors
Check that types are properly imported
Ensure tsconfig.json
paths are correct
Restart TypeScript server in your IDE
Build Failures
Check for TypeScript errors: ddev pnpm types
Check for linting errors: ddev pnpm lint
Clear build cache: rm -rf public/build
Reinstall dependencies: ddev pnpm install
Last modified: 13 October 2025