Chuyển đến nội dung chính

Lesson 4: Event Handling, Forms & Conditional Rendering

Event handling, synthetic events. Controlled/uncontrolled components, form libraries (React Hook Form, Zod validation). Lists, keys, conditional rendering patterns.

💻 Programming — Lesson 4 Lesson 4: Event Handling, Forms & Conditional Rendering

React & Next.js: From Basics to Advanced

Part 1: React Fundamentals

xdev.asia

1. Event Handling

function EventDemo() {
  // Inline handler
  return <button onClick={() => console.log('clicked')}>Click</button>;

  // Named handler (khuyến khích)
  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    e.preventDefault();
    console.log('clicked', e.currentTarget);
  };

  // Handler with parameters
  const handleDelete = (id: number) => () => {
    console.log('delete', id);
  };

  return (
    <div>
      <button onClick={handleClick}>Click me</button>
      <button onClick={handleDelete(1)}>Delete #1</button>
    </div>
  );
}

2. Controlled Forms

function LoginForm() {
  const [form, setForm] = useState({ email: '', password: '' });
  const [errors, setErrors] = useState<Record<string, string>>({});

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setForm(prev => ({ ...prev, [name]: value }));
  };

  const validate = () => {
    const newErrors: Record<string, string> = {};
    if (!form.email) newErrors.email = 'Email is required';
    if (!form.password) newErrors.password = 'Password is required';
    if (form.password.length < 8) newErrors.password = 'Min 8 characters';
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (validate()) {
      console.log('Submit:', form);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input name="email" value={form.email} onChange={handleChange} />
        {errors.email && <span className="error">{errors.email}</span>}
      </div>
      <div>
        <input name="password" type="password" value={form.password} onChange={handleChange} />
        {errors.password && <span className="error">{errors.password}</span>}
      </div>
      <button type="submit">Login</button>
    </form>
  );
}

3. React Hook Form + Zod

npm install react-hook-form @hookform/resolvers zod
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

// Schema definition
const registerSchema = z.object({
  name: z.string().min(2, 'Tên ít nhất 2 ký tự').max(50),
  email: z.string().email('Email không hợp lệ'),
  password: z.string().min(8, 'Mật khẩu ít nhất 8 ký tự'),
  confirmPassword: z.string(),
  role: z.enum(['user', 'admin']),
  agreeTerms: z.literal(true, {
    errorMap: () => ({ message: 'Bạn phải đồng ý điều khoản' }),
  }),
}).refine(data => data.password === data.confirmPassword, {
  message: 'Mật khẩu không khớp',
  path: ['confirmPassword'],
});

type RegisterForm = z.infer<typeof registerSchema>;

function RegisterPage() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
    reset,
  } = useForm<RegisterForm>({
    resolver: zodResolver(registerSchema),
    defaultValues: {
      role: 'user',
    },
  });

  const onSubmit = async (data: RegisterForm) => {
    const res = await fetch('/api/register', {
      method: 'POST',
      body: JSON.stringify(data),
    });
    if (res.ok) reset();
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label>Name</label>
        <input {...register('name')} />
        {errors.name && <p className="error">{errors.name.message}</p>}
      </div>

      <div>
        <label>Email</label>
        <input {...register('email')} type="email" />
        {errors.email && <p className="error">{errors.email.message}</p>}
      </div>

      <div>
        <label>Password</label>
        <input {...register('password')} type="password" />
        {errors.password && <p className="error">{errors.password.message}</p>}
      </div>

      <div>
        <label>Confirm Password</label>
        <input {...register('confirmPassword')} type="password" />
        {errors.confirmPassword && <p className="error">{errors.confirmPassword.message}</p>}
      </div>

      <div>
        <select {...register('role')}>
          <option value="user">User</option>
          <option value="admin">Admin</option>
        </select>
      </div>

      <div>
        <label>
          <input {...register('agreeTerms')} type="checkbox" />
          Đồng ý điều khoản sử dụng
        </label>
        {errors.agreeTerms && <p className="error">{errors.agreeTerms.message}</p>}
      </div>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Đang xử lý...' : 'Đăng ký'}
      </button>
    </form>
  );
}

4. Conditional Rendering Patterns

function Dashboard({ user, notifications, isLoading, error }: Props) {
  // Pattern 1: Early return
  if (isLoading) return <Skeleton />;
  if (error) return <ErrorMessage error={error} />;
  if (!user) return <LoginPrompt />;

  // Pattern 2: Logical AND (&&)
  // ⚠️ Cẩn thận với falsy values (0, '')
  return (
    <div>
      {notifications.length > 0 && (
        <Badge count={notifications.length} />
      )}

      {/* Pattern 3: Ternary */}
      {user.role === 'admin' ? <AdminPanel /> : <UserPanel />}

      {/* Pattern 4: Object mapping */}
      {{
        active: <ActiveStatus />,
        pending: <PendingStatus />,
        inactive: <InactiveStatus />,
      }[user.status]}
    </div>
  );
}

5. Lists & Keys

interface Todo {
  id: string;
  title: string;
  completed: boolean;
}

function TodoList({ todos }: { todos: Todo[] }) {
  return (
    <ul>
      {todos.map(todo => (
        // ✅ Key phải unique, stable, từ data
        <li key={todo.id}>
          <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
            {todo.title}
          </span>
        </li>
      ))}
    </ul>
  );
}

// ❌ KHÔNG dùng index làm key khi list thay đổi (add/remove/reorder)
// ❌ <li key={index}> → lỗi khi sort/filter
// ✅ <li key={item.id}> → unique identifier từ data

Next article: Context API & State Management — Zustand, TanStack Query, and state management strategies.