Preact Complete Guide | Fast 3KB React Alternative

Preact Complete Guide | Fast 3KB React Alternative

이 글의 핵심

Preact is a fast 3KB alternative to React with the same modern API. It's perfect for performance-critical applications and works with most React libraries.

Introduction

Preact is a fast 3KB alternative to React with the same modern API. It’s not a reimplementation of React, but a fresh take on the Virtual DOM with a focus on performance and size.

React vs Preact

React (40KB):

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

Preact (3KB):

import { h } from 'preact';
import { useState } from 'preact/hooks';

function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

Almost identical!

1. Installation

New Project with Vite

npm create vite@latest my-app -- --template preact-ts
cd my-app
npm install
npm run dev

Add to Existing Project

npm install preact

2. Core Concepts

JSX and h()

import { h } from 'preact';

// JSX (recommended)
const element = <div>Hello</div>;

// h() function (JSX compiles to this)
const element = h('div', null, 'Hello');

Components

import { h } from 'preact';

// Function component
function Greeting({ name }) {
  return <h1>Hello, {name}!</h1>;
}

// With TypeScript
interface GreetingProps {
  name: string;
}

function Greeting({ name }: GreetingProps) {
  return <h1>Hello, {name}!</h1>;
}

Props and Children

function Card({ title, children }) {
  return (
    <div class="card">
      <h2>{title}</h2>
      <div>{children}</div>
    </div>
  );
}

// Usage
<Card title="My Card">
  <p>Card content</p>
</Card>

3. Hooks

useState

import { useState } from 'preact/hooks';

function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(c => c + 1)}>Increment (updater)</button>
    </div>
  );
}

useEffect

import { useEffect } from 'preact/hooks';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(setUser);
  }, [userId]); // Re-run when userId changes
  
  return <div>{user?.name}</div>;
}

useMemo

import { useMemo } from 'preact/hooks';

function ExpensiveList({ items, filter }) {
  const filtered = useMemo(() => {
    console.log('Filtering...');
    return items.filter(item => item.includes(filter));
  }, [items, filter]);
  
  return (
    <ul>
      {filtered.map(item => <li key={item}>{item}</li>)}
    </ul>
  );
}

useCallback

import { useCallback } from 'preact/hooks';

function TodoList({ todos }) {
  const [filter, setFilter] = useState('');
  
  const handleFilter = useCallback((e) => {
    setFilter(e.target.value);
  }, []);
  
  return (
    <div>
      <input onInput={handleFilter} />
      {/* TodoItem won't re-render if handleFilter doesn't change */}
    </div>
  );
}

useRef

import { useRef } from 'preact/hooks';

function TextInput() {
  const inputRef = useRef(null);
  
  const focusInput = () => {
    inputRef.current?.focus();
  };
  
  return (
    <div>
      <input ref={inputRef} />
      <button onClick={focusInput}>Focus</button>
    </div>
  );
}

4. React Compatibility

Using preact/compat

npm install preact

vite.config.ts:

import { defineConfig } from 'vite';
import preact from '@preact/preset-vite';

export default defineConfig({
  plugins: [preact()],
  resolve: {
    alias: {
      react: 'preact/compat',
      'react-dom': 'preact/compat',
    },
  },
});

Now you can use React libraries!

// Works with Preact via compat
import { useState } from 'react';
import { createPortal } from 'react-dom';

5. Signals (Preact’s Secret Weapon)

npm install @preact/signals
import { signal, computed } from '@preact/signals';

const count = signal(0);
const doubled = computed(() => count.value * 2);

function Counter() {
  // No useState needed! Signals auto-update
  return (
    <div>
      <p>Count: {count}</p>
      <p>Doubled: {doubled}</p>
      <button onClick={() => count.value++}>Increment</button>
    </div>
  );
}

Why Signals?

  • No re-renders needed
  • Automatically optimized
  • Global state without context

6. Routing

npm install preact-router
import { Router, Route } from 'preact-router';

function App() {
  return (
    <Router>
      <Home path="/" />
      <Profile path="/profile/:user" />
      <NotFound default />
    </Router>
  );
}

function Profile({ user }) {
  return <h1>Profile: {user}</h1>;
}

7. Server-Side Rendering

import { render } from 'preact-render-to-string';

const html = render(<App />);
console.log(html); // <div>...</div>

8. Performance Tips

1. Use Signals for Global State

// Global signal (no context needed)
import { signal } from '@preact/signals';

export const user = signal(null);

// Use anywhere
function Header() {
  return <div>Welcome, {user.value?.name}</div>;
}

2. Memoize Components

import { memo } from 'preact/compat';

const ExpensiveComponent = memo(({ data }) => {
  // Only re-renders when data changes
  return <div>{data}</div>;
});

3. Lazy Load Components

import { lazy, Suspense } from 'preact/compat';

const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <HeavyComponent />
    </Suspense>
  );
}

9. Differences from React

class vs className

// Preact: use "class" (HTML standard)
<div class="container">Hello</div>

// React: use "className"
<div className="container">Hello</div>

// Both work in Preact!

Event Naming

// Preact: lowercase events work
<input onchange={handleChange} />

// React: camelCase only
<input onChange={handleChange} />

// Both work in Preact!

No Synthetic Events

Preact uses native browser events (faster!):

function handleClick(e) {
  // e is a native Event, not SyntheticEvent
  console.log(e.target);
}

10. Real-World Example

Todo App with Signals

import { signal, computed } from '@preact/signals';

const todos = signal([
  { id: 1, text: 'Learn Preact', done: false },
]);
const filter = signal('all');

const filteredTodos = computed(() => {
  if (filter.value === 'all') return todos.value;
  return todos.value.filter(t => 
    filter.value === 'done' ? t.done : !t.done
  );
});

function TodoApp() {
  const addTodo = (text) => {
    todos.value = [...todos.value, {
      id: Date.now(),
      text,
      done: false,
    }];
  };
  
  const toggleTodo = (id) => {
    todos.value = todos.value.map(t =>
      t.id === id ? { ...t, done: !t.done } : t
    );
  };
  
  return (
    <div>
      <input onKeyUp={(e) => {
        if (e.key === 'Enter') {
          addTodo(e.target.value);
          e.target.value = '';
        }
      }} />
      
      <div>
        <button onClick={() => filter.value = 'all'}>All</button>
        <button onClick={() => filter.value = 'active'}>Active</button>
        <button onClick={() => filter.value = 'done'}>Done</button>
      </div>
      
      <ul>
        {filteredTodos.value.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.done}
              onChange={() => toggleTodo(todo.id)}
            />
            {todo.text}
          </li>
        ))}
      </ul>
    </div>
  );
}

11. Bundle Size Comparison

LibrarySize (gzipped)Load Time (3G)
Preact3KB60ms
Vue34KB680ms
React40KB800ms

Summary

Preact is the perfect React alternative for performance:

  • 3KB vs React’s 40KB (13x smaller)
  • Same API - React knowledge transfers
  • Signals for ultra-fast state
  • Compatible with most React libraries
  • Faster rendering and smaller bundles

Key Takeaways:

  1. Almost identical API to React
  2. Use preact/compat for React libraries
  3. Signals eliminate re-renders
  4. Use class instead of className
  5. Perfect for performance-critical apps

Next Steps:

Resources: