React Setup Guide
View SourceThis guide covers setting up a full-stack Phoenix + React + TypeScript application with AshTypescript.
Quick Setup
Use the React framework installer for automated setup:
mix igniter.install ash_typescript --framework react
This command automatically sets up:
- 📦 Package.json with React 19 & TypeScript
- ⚛️ React components with welcome page and documentation
- 🎨 Tailwind CSS integration with modern styling
- 🔧 Build configuration with esbuild and TypeScript compilation
- 📄 Templates with proper script loading and syntax highlighting
- 🌐 Getting started guide accessible at
/ash-typescriptin your Phoenix app
What Gets Created
Frontend Structure
assets/
├── js/
│ ├── app.tsx # React entry point
│ ├── ash_rpc.ts # Generated TypeScript types
│ └── components/
│ └── Welcome.tsx # Example component
├── css/
│ └── app.css # Tailwind styles
└── package.json # DependenciesWelcome Page
After running your Phoenix server, visit:
http://localhost:4000/ash-typescriptThe welcome page includes:
- Step-by-step setup instructions
- Code examples with syntax highlighting
- Links to documentation and demo projects
- Type-safe RPC function examples
Manual React Setup
If you prefer manual setup or need to customize:
1. Install Dependencies
cd assets
npm install --save react react-dom
npm install --save-dev @types/react @types/react-dom typescript
2. Configure TypeScript
Create assets/tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true
},
"include": ["js/**/*"],
"exclude": ["node_modules"]
}3. Create React Entry Point
Create assets/js/app.tsx:
import React from 'react';
import ReactDOM from 'react-dom/client';
import { App } from './components/App';
const root = document.getElementById('root');
if (root) {
ReactDOM.createRoot(root).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
}4. Update Phoenix Template
In your lib/my_app_web/components/layouts/root.html.heex:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}></script>
</head>
<body>
<div id="root"></div>
</body>
</html>5. Configure esbuild
Update config/config.exs:
config :esbuild,
version: "0.17.11",
default: [
args: ~w(
js/app.tsx
--bundle
--target=es2020
--outdir=../priv/static/assets
--external:/fonts/*
--external:/images/*
),
cd: Path.expand("../assets", __DIR__),
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
]Using AshTypescript with React
Basic Component Example
import React, { useEffect, useState } from 'react';
import { listTodos, createTodo, type Todo } from '../ash_rpc';
export function TodoList() {
const [todos, setTodos] = useState<Todo[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
loadTodos();
}, []);
async function loadTodos() {
setLoading(true);
const result = await listTodos({
fields: ["id", "title", "completed"]
});
if (result.success) {
setTodos(result.data.results);
setError(null);
} else {
setError(result.errors.map(e => e.message).join(', '));
}
setLoading(false);
}
async function handleCreate(title: string) {
const result = await createTodo({
fields: ["id", "title", "completed"],
input: { title }
});
if (result.success) {
setTodos([...todos, result.data]);
} else {
setError(result.errors.map(e => e.message).join(', '));
}
}
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<h1>Todos</h1>
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.title} - {todo.completed ? '✓' : '○'}
</li>
))}
</ul>
<button onClick={() => handleCreate('New Todo')}>
Add Todo
</button>
</div>
);
}With TanStack Query
For better data fetching, use TanStack Query (React Query):
npm install @tanstack/react-query
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { listTodos, createTodo } from '../ash_rpc';
export function TodoListWithQuery() {
const queryClient = useQueryClient();
const { data, isLoading, error } = useQuery({
queryKey: ['todos'],
queryFn: async () => {
const result = await listTodos({
fields: ["id", "title", "completed"]
});
if (!result.success) {
throw new Error(result.errors.map(e => e.message).join(', '));
}
return result.data.results;
}
});
const createMutation = useMutation({
mutationFn: async (title: string) => {
const result = await createTodo({
fields: ["id", "title", "completed"],
input: { title }
});
if (!result.success) {
throw new Error(result.errors.map(e => e.message).join(', '));
}
return result.data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
}
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>Todos</h1>
<ul>
{data?.map(todo => (
<li key={todo.id}>
{todo.title} - {todo.completed ? '✓' : '○'}
</li>
))}
</ul>
<button onClick={() => createMutation.mutate('New Todo')}>
Add Todo
</button>
</div>
);
}Example Repository
Check out the AshTypescript Demo by Christian Alexander for a complete example featuring:
- Complete Phoenix + React + TypeScript integration
- TanStack Query for data fetching
- TanStack Table for data display
- Best practices and patterns
Adding Tailwind CSS
1. Install Tailwind
cd assets
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init
2. Configure Tailwind
Update assets/tailwind.config.js:
module.exports = {
content: [
'./js/**/*.{js,jsx,ts,tsx}',
'../lib/*_web/**/*.*ex'
],
theme: {
extend: {},
},
plugins: [],
}3. Add Tailwind Directives
In assets/css/app.css:
@tailwind base;
@tailwind components;
@tailwind utilities;4. Configure PostCSS
Create assets/postcss.config.js:
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
}
}Development Workflow
1. Start Phoenix Server
mix phx.server
This automatically:
- Compiles TypeScript
- Watches for file changes
- Hot-reloads the browser
2. Generate Types
Whenever you change resources or actions:
mix ash.codegen
3. Type Check
Add a script to package.json:
{
"scripts": {
"typecheck": "tsc --noEmit"
}
}Run type checking:
npm run typecheck
CSRF Protection
When using session-based authentication, use CSRF headers:
import { listTodos, buildCSRFHeaders } from '../ash_rpc';
const result = await listTodos({
fields: ["id", "title"],
headers: buildCSRFHeaders()
});The buildCSRFHeaders() function automatically reads the CSRF token from the meta tag in your layout.
Next Steps
- Basic CRUD Operations - Common patterns
- Field Selection - Advanced queries
- Error Handling - Handling errors
- Form Validation - Client-side validation
- Lifecycle Hooks - Auth, logging, telemetry