Receber por email
← Voltar para o catálogo

frontend-component

Next.js 16+ usa o App Router com Server Components por padrão. Client Components são usados apenas quando a interatividade é necessária (hooks, manipuladores de eventos, APIs do navegador).

367estrelas
Atualizado há 4 meses

Ver no GitHub ↗

Como adicionar

/plugin marketplace add aiskillstore/marketplace

O comando exato pode variar conforme o repositório. Confira o README no GitHub.

Para o autor da skill

Cole no README do seu repo

Mostra que sua skill está catalogada na Skillteca, gera backlink e tráfego rastreável.

[![Skillteca](https://www.skillteca.com.br/api/badge/frontend-component/svg)](https://www.skillteca.com.br/skills/frontend-component)

Alerta por categoria

Receba novas skills de Design e Frontend toda segunda

1 email curto, só com as skills novas de Design e Frontend. 4 minutos de leitura, sem spam, cancela com 1 clique.

Você confirma o email no primeiro envio. Sem spam. Cancela com 1 clique.

Ler descrição completa

Frontend Component Skill

Purpose: Guidance for creating Next.js components following server/client patterns and existing component structures.

Overview

Next.js 16+ uses App Router with Server Components by default. Client Components are only used when interactivity is needed (hooks, event handlers, browser APIs).

Server vs Client Components

Server Components (Default)

When to Use:

  • Pages and layouts
  • Static content
  • Data fetching from API (when possible)
  • SEO-optimized content

Pattern:

// No "use client" directive
import { Metadata } from "next";

export const metadata: Metadata = {
  title: "Page Title",
};

export default function PageComponent() {
  return <div>Static content</div>;
}

Example: frontend/app/layout.tsx, frontend/app/page.tsx

Client Components (When Needed)

When to Use:

  • Interactive elements (buttons, forms, inputs)
  • Event handlers (onClick, onChange, etc.)
  • React hooks (useState, useEffect, useRouter, etc.)
  • Browser APIs (localStorage, window, document, etc.)
  • Real-time updates
  • Drag and drop functionality

Pattern:

"use client"; // MUST be first line

import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";

interface ComponentProps {
  prop1: string;
  prop2?: number;
}

export default function ComponentName({ prop1, prop2 }: ComponentProps) {
  const router = useRouter();
  const [state, setState] = useState("");

  useEffect(() => {
    // Side effects
  }, []);

  return <div>{/* Component JSX */}</div>;
}

Example: frontend/components/ProtectedRoute.tsx, frontend/app/signup/page.tsx

Component Structure Template

"use client"; // Only if client component

/**
 * Component Name
 *
 * Brief description of what this component does
 */

import { useState, useEffect } from "react";
import { ComponentType } from "@/types";
import { cn } from "@/lib/utils";

interface ComponentProps {
  prop1: string;
  prop2?: number;
  className?: string;
}

export default function ComponentName({ prop1, prop2, className }: ComponentProps) {
  // State
  const [state, setState] = useState("");

  // Effects
  useEffect(() => {
    // Side effects
  }, []);

  // Handlers
  const handleClick = () => {
    // Handler logic
  };

  // Render
  return (
    <div className={cn("base-classes", className)}>
      {/* Component content */}
    </div>
  );
}

Specific Component Patterns

1. ProtectedRoute Pattern

From: frontend/components/ProtectedRoute.tsx

"use client";

import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { isAuthenticated } from "@/lib/auth";
import LoadingSpinner from "./LoadingSpinner";

interface ProtectedRouteProps {
  children: React.ReactNode;
}

export default function ProtectedRoute({ children }: ProtectedRouteProps) {
  const router = useRouter();
  const [isAuthorized, setIsAuthorized] = useState(false);
  const [isChecking, setIsChecking] = useState(true);

  useEffect(() => {
    async function checkAuth() {
      try {
        const authenticated = await isAuthenticated();

        if (!authenticated) {
          const currentPath = window.location.pathname;
          if (currentPath !== "/signin") {
            sessionStorage.setItem("redirectAfterLogin", currentPath);
          }
          router.push("/signin");
        } else {
          setIsAuthorized(true);
        }
      } catch (error) {
        console.error("Auth check failed:", error);
        router.push("/signin");
      } finally {
        setIsChecking(false);
      }
    }

    checkAuth();
  }, [router]);

  if (isChecking) {
    return (
      <div className="min-h-screen flex items-center justify-center">
        <LoadingSpinner size="large" />
      </div>
    );
  }

  if (!isAuthorized) {
    return null;
  }

  return <>{children}</>;
}

Pattern:

  • Check authentication on mount
  • Show loading spinner during check
  • Store intended destination in sessionStorage
  • Redirect to /signin if not authenticated
  • Only render children if authorized

2. LoadingSpinner Pattern

From: frontend/components/LoadingSpinner.tsx

interface LoadingSpinnerProps {
  size?: "small" | "medium" | "large";
  color?: string;
  label?: string;
}

export default function LoadingSpinner({
  size = "medium",
  color = "blue",
  label = "Loading...",
}: LoadingSpinnerProps) {
  const sizeClasses = {
    small: "w-4 h-4 border-2",
    medium: "w-8 h-8 border-3",
    large: "w-12 h-12 border-4",
  };

  const colorClasses = {
    blue: "border-blue-600 border-t-transparent",
    gray: "border-gray-600 border-t-transparent",
    white: "border-white border-t-transparent",
  };

  const spinnerClass = `${sizeClasses[size]} ${
    colorClasses[color as keyof typeof colorClasses] || colorClasses.blue
  } rounded-full animate-spin`;

  return (
    <div
      className="flex items-center justify-center"
      role="status"
      aria-label={label}
      aria-live="polite"
    >
      <div className={spinnerClass}></div>
      <span className="sr-only">{label}</span>
    </div>
  );
}

Pattern:

  • Multiple sizes (small, medium, large)
  • Multiple colors (blue, gray, white)
  • Accessibility labels (role="status", aria-label, aria-live)
  • Screen reader text with sr-only class

3. Form Handling Pattern

From: frontend/app/signup/page.tsx

"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";
import { api } from "@/lib/api";
import { isValidEmail, getPasswordStrength } from "@/lib/utils";

export default function SignupPage() {
  const router = useRouter();
  const [formData, setFormData] = useState({
    name: "",
    email: "",
    password: "",
  });
  const [errors, setErrors] = useState<Record<string, string>>({});
  const [isLoading, setIsLoading] = useState(false);
  const [apiError, setApiError] = useState("");

  const validateForm = (): boolean => {
    const newErrors: Record<string, string> = {};

    if (!formData.name.trim()) {
      newErrors.name = "Name is required";
    }

    if (!formData.email.trim()) {
      newErrors.email = "Email is required";
    } else if (!isValidEmail(formData.email)) {
      newErrors.email = "Please enter a valid email address";
    }

    // More validation...

    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setApiError("");

    if (!validateForm()) {
      return;
    }

    setIsLoading(true);

    try {
      const response = await api.signup(formData);
      if (response.success) {
        router.push("/dashboard");
      } else {
        setApiError(response.message || "Signup failed");
      }
    } catch (error: any) {
      setApiError(error.message || "An error occurred");
    } finally {
      setIsLoading(false);
    }
  };

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setFormData((prev) => ({ ...prev, [name]: value }));
    // Clear error for this field when user starts typing
    if (errors[name]) {
      setErrors((prev) => ({ ...prev, [name]: "" }));
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* Form fields */}
    </form>
  );
}

Pattern:

  • Separate state for form data, errors, loading, API errors
  • Validation function that returns boolean
  • Clear errors on input change
  • Loading state during submission
  • Try-catch for error handling
  • Redirect on success

4. ToastNotification Pattern

From: frontend/components/ToastNotification.tsx

"use client";

import { useEffect, useState } from "react";
import { ToastMessage, ToastType } from "@/types";

export function useToast() {
  const [toasts, setToasts] = useState<ToastMessage[]>([]);

  const showToast = (type:
Compartilhar X LinkedIn