Introduzione ai Framework II: React

Niccolò Maltoni

Motivazioni per React

Perché React?

React è una libreria JavaScript per costruire interfacce utente dichiarative e reattive.

  • Dichiarativo: descrivi cosa vuoi renderizzare, React gestisce il come.
  • Component-based: suddividi l’UI in componenti riutilizzabili e indipendenti.
  • Reattivo: l’UI si aggiorna automaticamente quando lo stato cambia.
  • Virtual DOM: ottimizza gli aggiornamenti del DOM reale per migliori performance.
  • Ecosystem ricco: router, state management, testing tools integrati.

React vs Vue vs Vanilla JS

Aspetto Vanilla JS Vue React
Approccio Imperativo Dichiarativo (progressivo) Dichiarativo
Curva apprendimento Media Bassa Media-Alta
Componenti Manual nesting SFC (.vue) JSX
State management Manual Reactive data useState hook
Performance Dipende da dev Buona Ottima (Virtual DOM)
Community Grande Crescente Molto grande

Setup React: Vite (consigliato)

Creare un progetto React

Il metodo moderno per creare app React è utilizzare Vite.js:

# 1. Crea progetto React con Vite
npm create vite@latest my-react-app -- --template react

# 2. Entra nella directory
cd my-react-app

# 3. Installa dipendenze
npm install

# 4. Avvia dev server
npm run dev

# 5. Accedi a http://localhost:5173

Vantaggi di Vite:

  • HMR (Hot Module Replacement) ultra-veloce
  • Setup minimale (zero config per React)
  • Build ottimizzato per produzione
  • ES modules nativi in sviluppo

Struttura progetto Vite + React

my-react-app/
├── src/
│   ├── App.jsx              # Componente root
│   ├── main.jsx             # Entry point
│   ├── components/          # Componenti riutilizzabili
│   ├── assets/              # Immagini, font
│   └── styles/              # CSS globale
├── public/                  # Asset statici
├── index.html               # Template HTML
├── package.json             # Dipendenze
├── vite.config.js           # Config Vite
└── dist/                    # Output build (generato)

ReactDOM e createRoot

Entry point: main.jsx

Per avviare un’applicazione React, devi montare il componente radice nel DOM:

// main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'

// Crea una root React nel div#root
ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)

createRoot (React 18+):

  • Moderno: sostituisce il vecchio ReactDOM.render()
  • Concurrent rendering: abilita features avanzate di React 18+
  • Migliore performance: priorità intelligente degli aggiornamenti

HTML template: index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>React App</title>
  </head>
  <body>
    <div id="root"></div>  <!-- React monta qui -->
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

Componenti funzionali e JSX

Cos’è un componente React?

Un componente React è una funzione JavaScript che ritorna un elemento JSX.

// Componente semplice
function Welcome() {
  return <h1>Ciao React!</h1>;
}

// Componente con parametri (props)
function Greeting(props) {
  return <p>Benvenuto, {props.name}!</p>;
}

// Arrow function (moderna)
const Card = () => {
  return (
    <div className="card">
      <h2>Card Title</h2>
      <p>Card content</p>
    </div>
  );
};

// Esporta il componente
export default Card;

Regole dei componenti:

  • Deve ritornare un singolo elemento JSX (o Fragment)
  • Nome deve iniziare con maiuscola (es. Card, non card)
  • Puoi importare e usare componenti dentro altri componenti
  • Ciascun componente ha una responsabilità singola

JSX syntax

Cos’è JSX?

JSX è una sintassi che permette di scrivere HTML-like code dentro JavaScript.

// JSX: sembra HTML ma è JavaScript
const element = <h1>Hello World</h1>;

// Si traduce in:
const element = React.createElement('h1', null, 'Hello World');

Interpolazione e espressioni

function App() {
  const name = 'Alice';
  const count = 42;
  const isActive = true;

  return (
    <div>
      {/* Variabili */}
      <p>Ciao, {name}!</p>

      {/* Espressioni */}
      <p>Risultato: {count * 2}</p>
      <p>Status: {isActive ? 'Attivo' : 'Inattivo'}</p>

      {/* Funzioni */}
      <p>Array: {[1, 2, 3].join(', ')}</p>

      {/* Commenti */}
      {/* Questo è un commento in JSX */}
    </div>
  );
}

export default App;

Attributi HTML in JSX

function ImageComponent() {
  const src = 'https://example.com/img.png';
  const altText = 'Profile Picture';

  return (
    <div>
      {/* Attributi con camelCase */}
      <img src={src} alt={altText} className="profile-pic" />
      <button onClick={() => console.log('Clicked!')}>Click me</button>
      <input type="text" maxLength={10} disabled={false} />

      {/* Attributi booleani */}
      <input type="checkbox" checked={true} />
    </div>
  );
}

Nota: class diventa className, for diventa htmlFor in JSX.

Props: passare dati ai componenti

Props base

Props sono dati passati da un componente genitore a uno figlio:

// Componente figlio - riceve props
function TodoItem(props) {
  return (
    <div>
      <h3>{props.title}</h3>
      <p>{props.description}</p>
      <p>Completato: {props.completed ? '✓' : '○'}</p>
    </div>
  );
}

// Componente genitore - passa props
function App() {
  return (
    <div>
      <TodoItem 
        title="Imparare React"
        description="Comprendere componenti e state"
        completed={true}
      />
      <TodoItem 
        title="Fare esercizi"
        description="Pratica con componenti"
        completed={false}
      />
    </div>
  );
}

export default App;

Destructuring props

Per renderere il codice più leggibile:

// Destructuring nei parametri
function UserCard({ name, age, city }) {
  return (
    <div className="card">
      <h2>{name}</h2>
      <p>Età: {age}</p>
      <p>Città: {city}</p>
    </div>
  );
}

// Uso
<UserCard name="Alice" age={30} city="Rome" />

Default props

function Greeting({ greeting = 'Ciao', name = 'Ospite' }) {
  return <h1>{greeting}, {name}!</h1>;
}

// Se non passi props, usa i default
<Greeting />  // Output: "Ciao, Ospite!"
<Greeting name="Bob" />  // Output: "Ciao, Bob!"

State con useState

Hook useState

Lo state è dati che cambiano nel tempo e controllano il componente.

import { useState } from 'react';

function Counter() {
  // Dichiara una variabile di stato
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Contatore: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => setCount(count - 1)}>-1</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}

export default Counter;

Sintassi useState:

const [state, setState] = useState(initialValue);
  • state: valore attuale dello stato
  • setState: funzione per aggiornare lo stato
  • initialValue: valore iniziale (numero, stringa, array, oggetto, ecc.)

Stato complesso (oggetti e array)

function UserForm() {
  const [user, setUser] = useState({
    name: '',
    email: '',
    age: 0
  });

  const handleChange = (e) => {
    const { name, value } = e.target;
    setUser(prevUser => ({
      ...prevUser,
      [name]: value
    }));
  };

  return (
    <form>
      <input
        name="name"
        value={user.name}
        onChange={handleChange}
        placeholder="Nome"
      />
      <input
        name="email"
        value={user.email}
        onChange={handleChange}
        placeholder="Email"
        type="email"
      />
      <p>Dati: {JSON.stringify(user)}</p>
    </form>
  );
}

Event handling

Ascoltare eventi

function EventDemo() {
  const handleClick = () => {
    console.log('Button clicked!');
  };

  const handleSubmit = (e) => {
    e.preventDefault();  // Previene submit default
    console.log('Form submitted!');
  };

  const handleInputChange = (e) => {
    console.log('Input value:', e.target.value);
  };

  return (
    <div>
      <button onClick={handleClick}>Click me</button>
      
      <form onSubmit={handleSubmit}>
        <input onChange={handleInputChange} placeholder="Type..."/>
        <button type="submit">Submit</button>
      </form>
    </div>
  );
}

Event handler inline con arrow function

function Button() {
  return (
    <div>
      {/* Inline arrow function */}
      <button onClick={() => console.log('Clicked!')}>
        Inline handler
      </button>

      {/* Con parametri */}
      <button onClick={(e) => {
        console.log('Clicked with event:', e);
      }}>
        With event
      </button>
    </div>
  );
}

Form handling completo

function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('Login:', { email, password });
    // Invia dati a API
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
      />
      <button type="submit">Login</button>
    </form>
  );
}

Conditional rendering

if/else classico

function LoginStatus({ isLoggedIn }) {
  if (isLoggedIn) {
    return <p>Welcome back!</p>;
  } else {
    return <p>Please log in.</p>;
  }
}

Operatore ternario

function UserGreeting({ isLoggedIn, username }) {
  return (
    <div>
      {isLoggedIn ? (
        <p>Benvenuto, {username}!</p>
      ) : (
        <p>Per favore, effettua il login.</p>
      )}
    </div>
  );
}

Operatore &&

function NotificationBadge({ unreadCount }) {
  return (
    <div>
      Messages
      {unreadCount > 0 && <span className="badge">{unreadCount}</span>}
    </div>
  );
}

Switch statement

function StatusMessage({ status }) {
  switch (status) {
    case 'loading':
      return <p>Caricamento...</p>;
    case 'success':
      return <p>Dati caricati!</p>;
    case 'error':
      return <p>Errore nel caricamento.</p>;
    default:
      return <p>Status sconosciuto.</p>;
  }
}

List rendering con map()

Iterare su array

function TodoList() {
  const todos = [
    { id: 1, title: 'Imparare React', done: false },
    { id: 2, title: 'Fare esercizi', done: true },
    { id: 3, title: 'Progetto finale', done: false }
  ];

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          <input type="checkbox" checked={todo.done} />
          <span>{todo.title}</span>
        </li>
      ))}
    </ul>
  );
}

export default TodoList;

⚠️ Importante: sempre includi una prop key unica!

// ✅ CORRETTO
{items.map(item => <Item key={item.id} {...item} />)}

// ❌ SBAGLIATO (non usare index come key)
{items.map((item, index) => <Item key={index} {...item} />)}

Filtrare e trasformare array

function ProductList() {
  const products = [
    { id: 1, name: 'Laptop', price: 1000 },
    { id: 2, name: 'Mouse', price: 25 },
    { id: 3, name: 'Keyboard', price: 80 }
  ];

  return (
    <div>
      {/* Filtrare */}
      <h2>Prodotti > 50€</h2>
      <ul>
        {products
          .filter(p => p.price > 50)
          .map(p => <li key={p.id}>{p.name}: {p.price}</li>)
        }
      </ul>

      {/* Trasformare */}
      <h2>Nomi prodotti</h2>
      <ul>
        {products.map(p => <li key={p.id}>{p.name.toUpperCase()}</li>)}
      </ul>
    </div>
  );
}

Side effects con useEffect

Hook useEffect

useEffect esegue effetti collaterali (fetch, subscribe, timer) nei componenti:

import { useState, useEffect } from 'react';

function DataFetcher() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  // Esegui effetto al mount
  useEffect(() => {
    fetch('https://api.example.com/data')
      .then(res => res.json())
      .then(json => {
        setData(json);
        setLoading(false);
      })
      .catch(err => {
        setError(err.message);
        setLoading(false);
      });
  }, []);  // Dipendenza vuota = solo al mount

  if (loading) return <p>Caricamento...</p>;
  if (error) return <p>Errore: {error}</p>;
  return <p>Dati: {JSON.stringify(data)}</p>;
}

Dependency array

function Counter() {
  const [count, setCount] = useState(0);
  const [message, setMessage] = useState('');

  // Esegui quando count cambia
  useEffect(() => {
    console.log('Count aggiornato:', count);
  }, [count]);  // Dipendenza: [count]

  // Esegui una sola volta al mount
  useEffect(() => {
    console.log('Componente montato');
  }, []);  // Dipendenza vuota

  // Esegui ad ogni render (evita se possibile)
  useEffect(() => {
    console.log('Componente renderizzato');
  });  // Nessuna dipendenza

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  );
}

Cleanup (unmount)

function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    // Setup: crea un timer
    const interval = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);

    // Cleanup: pulisci il timer quando unmount
    return () => clearInterval(interval);
  }, []);

  return <p>Secondi: {seconds}</p>;
}

Composizione di componenti

Pattern: parent → child → grandchild

// Grandchild
function TodoItem({ title, onDelete }) {
  return (
    <li>
      {title}
      <button onClick={onDelete}>Elimina</button>
    </li>
  );
}

// Child
function TodoList({ todos, onDeleteTodo }) {
  return (
    <ul>
      {todos.map(todo => (
        <TodoItem
          key={todo.id}
          title={todo.title}
          onDelete={() => onDeleteTodo(todo.id)}
        />
      ))}
    </ul>
  );
}

// Parent
function App() {
  const [todos, setTodos] = useState([
    { id: 1, title: 'React' },
    { id: 2, title: 'Esercizi' }
  ]);

  const handleDelete = (id) => {
    setTodos(todos.filter(t => t.id !== id));
  };

  return <TodoList todos={todos} onDeleteTodo={handleDelete} />;
}

Esercizio

TODO

  • Creare componente funzionale semplice e renderizzarlo
  • Passare props tra componenti parent-child
  • Usare useState per gestire uno stato semplice
  • Implementare button click e form submission
  • Usare map() per renderizzare liste dinamiche
  • Applicare conditional rendering (ternario, &&)
  • Usare useEffect per fetch dati da API

Esercizio: Counter App

TODO

  • Creare progetto con npm create vite@latest my-counter-app -- --template react
  • Implementare componente Counter.jsx:
    • Stato iniziale: count = 0
    • 3 pulsanti: “+1”, “-1”, “Reset”
    • Display del valore corrente
  • Implementare componente Stats.jsx:
    • Riceve count come prop
    • Mostra: “Pari/Dispari”, “Positivo/Negativo/Zero”
  • Importare entrambi in App.jsx
  • Styling base con CSS (centered, buttons spaced)
  • Acceptance Criteria:
    • ✅ App carica senza errori
    • ✅ Pulsanti incrementano/decrementano/resettano correttamente
    • ✅ Stats si aggiorna in real-time con il valore di count
    • ✅ Layout leggibile e responsive
    • ✅ Console pulita (zero warning)
    • Bonus: Aggiungere input number per impostare count direttamente