Frontend Development Guide
Overview
Weber's frontend is built with Preact, a lightweight React alternative that provides the same modern component-based architecture with a much smaller bundle size (3KB vs 40KB).
Key Technologies:
- Preact 10+ - Component framework
- TypeScript - Type-safe JavaScript
- Webpack 5 - Module bundler
- Babel - JSX transformation
- preact-render-to-string - Server-side rendering
Project Structure
frontend/
├── src/
│ ├── components/ # Reusable components
│ │ ├── Counter/
│ │ │ └── Counter.tsx
│ │ └── Form/
│ │ └── Form.tsx
│ ├── layouts/ # Layout components (Header, Footer)
│ │ ├── Header/
│ │ └── Footer/
│ ├── pages/ # Page components
│ │ └── Index/
│ │ ├── Index.m.tsx # Server-side module
│ │ └── Index.ts # Client-side entry
│ ├── utils/ # Utility functions
│ └── assets/ # Images, fonts, etc.
├── build/ # Build configuration
│ ├── build.js # Production build
│ ├── dev.js # Development build
│ ├── tpl.js # Template processing
│ └── ctrl.js # Control file generation
├── package.json
└── tsconfig.json
Preact Integration
Preact is integrated with full TypeScript support and SSR capabilities.
Installation
cd frontend
pnpm install
Available Dependencies
preact- Core librarypreact-render-to-string- SSR support- All hooks from
preact/hooks
Note: Preact 10+ includes built-in TypeScript definitions. No need for
@types/preact.
Server-Side Rendering (SSR)
Weber uses a hybrid approach combining Go templates with Preact SSR.
SSR Component Example (*.m.tsx)
// frontend/src/pages/Index/Index.m.tsx
import { Header, Footer } from '@/layouts';
export function Index() {
return (
<>
<Header />
<main>
<h1>Welcome to Weber</h1>
<div id="app-container"></div>
</main>
<Footer />
</>
);
}
How SSR Works
- Preact components are rendered to HTML strings during build
- HTML is saved to
webroot/directory - Go server serves the static HTML files
- Client-side JavaScript hydrates interactive components
Creating Components
Basic Component
// src/components/Button/Button.tsx
import { h } from 'preact';
interface ButtonProps {
label: string;
onClick: () => void;
}
export function Button({ label, onClick }: ButtonProps) {
return (
<button onClick={onClick}>
{label}
</button>
);
}
Component with State
// src/components/Counter/Counter.tsx
import { useState } from 'preact/hooks';
export function Counter() {
const [count, setCount] = useState(0);
return (
<div className="counter">
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
<button onClick={() => setCount(count - 1)}>
Decrement
</button>
</div>
);
}
Using Hooks
Available Hooks
useState- State managementuseEffect- Side effectsuseRef- DOM referencesuseContext- Context APIuseReducer- Complex state logicuseMemo- Memoized valuesuseCallback- Memoized callbacks
useState Example
import { useState } from 'preact/hooks';
function TodoList() {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState('');
const addTodo = () => {
setTodos([...todos, input]);
setInput('');
};
return (
<div>
<input
value={input}
onInput={e => setInput(e.target.value)}
/>
<button onClick={addTodo}>Add</button>
<ul>
{todos.map((todo, i) => <li key={i}>{todo}</li>)}
</ul>
</div>
);
}
useEffect Example
import { useState, useEffect } from 'preact/hooks';
function DataFetcher() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(setData);
// Cleanup function
return () => {
// Cancel requests, clear timers, etc.
};
}, []); // Empty deps = run once
return <div>{JSON.stringify(data)}</div>;
}
Client-Side Hydration
After SSR, interactive components need to be hydrated on the client side.
Client Entry Point (*.ts)
// frontend/src/pages/Index/Index.ts
import { render, h } from 'preact';
import { Counter } from '@/components/Counter/Counter';
(() => {
const container = document.getElementById('app-container');
if (container) {
render(h(Counter, null), container);
}
})();
Important: Use
h function in .ts files, not JSX syntax. JSX only works in .tsx files.
Multiple Components
import { render, h } from 'preact';
import { Counter } from '@/components/Counter/Counter';
import { Form } from '@/components/Form/Form';
(() => {
const counterEl = document.getElementById('counter');
const formEl = document.getElementById('form');
if (counterEl) render(h(Counter, null), counterEl);
if (formEl) render(h(Form, null), formEl);
})();
Build System
Development Mode
cd frontend
pnpm dev
Features:
- Hot reload on file changes
- Fast incremental builds
- Source maps for debugging
Production Build
cd frontend
pnpm build
Optimizations:
- Code minification
- Tree shaking
- Asset optimization
- Hash-based filenames for caching
Build Commands
| Command | Description |
|---|---|
pnpm dev |
Development build with hot reload |
pnpm build |
Production build |
pnpm clean |
Clean build artifacts |
pnpm redev |
Clean and rebuild |
pnpm ctrl |
Build control files |
pnpm tpl |
Process templates |
Best Practices
1. Component Organization
- One component per file
- Use folder structure for complex components
- Export components with named exports
2. TypeScript Usage
// Define prop types
interface Props {
name: string;
age?: number;
}
// Use type annotations
export function User({ name, age = 0 }: Props) {
return <div>{name} - {age}</div>;
}
3. Performance Optimization
- Use
useMemofor expensive calculations - Use
useCallbackfor event handlers - Avoid inline function definitions in render
- Use key props for lists
4. State Management
- Keep state as local as possible
- Lift state up only when necessary
- Use Context API for global state
- Consider external state libraries for complex apps
5. File Naming Conventions
*.tsx- Components with JSX*.ts- Client-side logic without JSX*.m.tsx- Server-side render modules- PascalCase for component files
6. Event Handling
// Good: Type-safe event handling
function handleInput(e: Event) {
const value = (e.target as HTMLInputElement).value;
console.log(value);
}
// Avoid: Inline arrow functions
// <button onClick={() => handleClick()}>
// Prefer: Direct reference
// <button onClick={handleClick}>