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 library
  • preact-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

  1. Preact components are rendered to HTML strings during build
  2. HTML is saved to webroot/ directory
  3. Go server serves the static HTML files
  4. 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 management
  • useEffect - Side effects
  • useRef - DOM references
  • useContext - Context API
  • useReducer - Complex state logic
  • useMemo - Memoized values
  • useCallback - 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 useMemo for expensive calculations
  • Use useCallback for 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}>

Next Steps