Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Projekt: Zaawansowana aplikacja React 18+

W tym projekcie stworzysz nowoczesną aplikację React wykorzystującą wszystkie poznane funkcjonalności React 18+. Będziesz budować Advanced Task Management System - zaawansowany system zarządzania zadaniami z wykorzystaniem Concurrent Features, Server Components i nowoczesnych hooków.

Specyfikacja projektu

Funkcjonalności główne

  1. Dashboard z real-time updates

    • Wykorzystanie useTransition i useDeferredValue
    • Optymistyczne aktualizacje z useOptimistic
    • Streaming zawartości z Suspense
  2. Zaawansowane formularze

    • useFormStatus i useActionState
    • Server Actions
    • Walidacja w czasie rzeczywistym
  3. System notyfikacji

    • useId dla accessibility
    • Concurrent rendering
    • Progressive enhancement
  4. Wydajność i optymalizacja

    • React Compiler (opcjonalnie)
    • Automatic Batching
    • Selective Hydration

Struktura aplikacji

1src/
2├── app/
3│   ├── layout.tsx          # Root layout (Server Component)
4│   ├── page.tsx            # Home page
5│   ├── dashboard/
6│   │   ├── page.tsx        # Dashboard (Server Component)
7│   │   └── loading.tsx     # Loading UI
8│   ├── tasks/
9│   │   ├── page.tsx        # Tasks list
10│   │   ├── [id]/page.tsx   # Task details
11│   │   └── new/page.tsx    # Create task
12│   └── api/
13│       ├── tasks/route.ts  # Tasks API
14│       └── notifications/route.ts
15├── components/
16│   ├── ui/                 # Basic UI components
17│   ├── forms/              # Form components
18│   ├── dashboard/          # Dashboard components
19│   └── tasks/              # Task components
20├── hooks/                  # Custom hooks
21├── lib/                    # Utilities
22└── types/                  # TypeScript types

Implementacja krok po kroku

Krok 1: Podstawowa struktura i typy

1// types/index.ts
2export interface Task {
3  id: string;
4  title: string;
5  description: string;
6  status: 'pending' | 'in_progress' | 'completed';
7  priority: 'low' | 'medium' | 'high';
8  assignee?: string;
9  dueDate?: Date;
10  createdAt: Date;
11  updatedAt: Date;
12}
13
14export interface User {
15  id: string;
16  name: string;
17  email: string;
18  avatar?: string;
19}
20
21export interface Notification {
22  id: string;
23  type: 'task_created' | 'task_updated' | 'task_completed';
24  message: string;
25  timestamp: Date;
26  read: boolean;
27}

Krok 2: Root Layout z Server Components

1// app/layout.tsx (Server Component)
2import { Suspense } from 'react';
3import { Navigation } from '@/components/Navigation';
4import { NotificationProvider } from '@/components/NotificationProvider';
5
6async function RootLayout({ children }: { children: React.ReactNode }) {
7  // Pobierz dane użytkownika na serwerze
8  const user = await getCurrentUser();
9  const notifications = await getUnreadNotifications(user.id);
10  
11  return (
12    <html lang="pl">
13      <head>
14        <title>Advanced Task Manager</title>
15        <meta name="description" content="Zaawansowany system zarządzania zadaniami" />
16      </head>
17      <body>
18        <NotificationProvider initialNotifications={notifications}>
19          <div className="app-layout">
20            <Suspense fallback={<NavigationSkeleton />}>
21              <Navigation user={user} />
22            </Suspense>
23            
24            <main className="main-content">
25              <Suspense fallback={<PageSkeleton />}>
26                {children}
27              </Suspense>
28            </main>
29          </div>
30        </NotificationProvider>
31      </body>
32    </html>
33  );
34}
35
36export default RootLayout;

Krok 3: Dashboard z Concurrent Features

1// app/dashboard/page.tsx (Server Component)
2import { Suspense } from 'react';
3import { TasksOverview } from '@/components/dashboard/TasksOverview';
4import { RecentActivity } from '@/components/dashboard/RecentActivity';
5import { PerformanceMetrics } from '@/components/dashboard/PerformanceMetrics';
6
7async function DashboardPage() {
8  // Równoległe pobieranie danych
9  const [tasksStats, recentTasks] = await Promise.all([
10    getTasksStatistics(),
11    getRecentTasks(10)
12  ]);
13  
14  return (
15    <div className="dashboard">
16      <h1>Dashboard</h1>
17      
18      {/* Szybko ładująca się sekcja */}
19      <TasksOverview stats={tasksStats} />
20      
21      <div className="dashboard-grid">
22        {/* Streams niezależnie */}
23        <Suspense fallback={<RecentActivitySkeleton />}>
24          <RecentActivity initialTasks={recentTasks} />
25        </Suspense>
26        
27        {/* Może ładować się później */}
28        <Suspense fallback={<MetricsSkeleton />}>
29          <PerformanceMetrics />
30        </Suspense>
31      </div>
32    </div>
33  );
34}
35
36export default DashboardPage;

Krok 4: Interactive Components z nowymi hookami

1// components/dashboard/RecentActivity.tsx (Client Component)
2'use client';
3
4import { useState, useTransition, useDeferredValue } from 'react';
5import { Task } from '@/types';
6
7interface RecentActivityProps {
8  initialTasks: Task[];
9}
10
11export function RecentActivity({ initialTasks }: RecentActivityProps) {
12  const [tasks, setTasks] = useState(initialTasks);
13  const [searchQuery, setSearchQuery] = useState('');
14  const [isPending, startTransition] = useTransition();
15  
16  // Opóźniona wartość dla wyszukiwania
17  const deferredSearchQuery = useDeferredValue(searchQuery);
18  
19  // Filtrowanie zadań z wykorzystaniem deferredValue
20  const filteredTasks = tasks.filter(task =>
21    task.title.toLowerCase().includes(deferredSearchQuery.toLowerCase()) ||
22    task.description.toLowerCase().includes(deferredSearchQuery.toLowerCase())
23  );
24  
25  const handleSearch = (query: string) => {
26    setSearchQuery(query);
27    
28    // Użyj transition dla mniej priorytetowych aktualizacji
29    startTransition(() => {
30      // Dodatkowe operacje które mogą być przerwane
31      updateSearchAnalytics(query);
32    });
33  };
34  
35  const refreshTasks = async () => {
36    startTransition(async () => {
37      const newTasks = await fetchRecentTasks();
38      setTasks(newTasks);
39    });
40  };
41  
42  return (
43    <div className="recent-activity">
44      <div className="activity-header">
45        <h2>Ostatnia aktywność</h2>
46        <button 
47          onClick={refreshTasks}
48          disabled={isPending}
49          className={isPending ? 'loading' : ''}
50        >
51          {isPending ? 'Odświeżanie...' : 'Odśwież'}
52        </button>
53      </div>
54      
55      <SearchInput
56        value={searchQuery}
57        onChange={handleSearch}
58        isPending={isPending}
59      />
60      
61      <div className="tasks-list">
62        {filteredTasks.map(task => (
63          <TaskCard 
64            key={task.id} 
65            task={task}
66            onUpdate={setTasks}
67          />
68        ))}
69        
70        {deferredSearchQuery && filteredTasks.length === 0 && (
71          <div className="no-results">
72            Brak wyników dla "{deferredSearchQuery}"
73          </div>
74        )}
75      </div>
76    </div>
77  );
78}

Krok 5: Zaawansowane formularze z Server Actions

1// components/forms/TaskForm.tsx (Client Component)
2'use client';
3
4import { useActionState, useFormStatus } from 'react';
5import { useId, useOptimistic } from 'react';
6import { createTask, updateTask } from '@/lib/actions';
7
8interface TaskFormProps {
9  task?: Task;
10  onSuccess?: () => void;
11}
12
13export function TaskForm({ task, onSuccess }: TaskFormProps) {
14  const formId = useId();
15  const titleId = useId();
16  const descriptionId = useId();
17  
18  // useActionState dla zarządzania stanem akcji
19  const [state, formAction] = useActionState(
20    task ? updateTask.bind(null, task.id) : createTask,
21    {
22      success: false,
23      errors: {},
24      data: task || null
25    }
26  );
27  
28  // useOptimistic dla natychmiastowej informacji zwrotnej
29  const [optimisticState, addOptimistic] = useOptimistic(
30    state,
31    (currentState, optimisticUpdate) => ({
32      ...currentState,
33      ...optimisticUpdate,
34      optimistic: true
35    })
36  );
37  
38  const handleSubmit = async (formData: FormData) => {
39    // Optymistyczna aktualizacja
40    addOptimistic({
41      success: true,
42      data: {
43        id: task?.id || 'temp-' + Date.now(),
44        title: formData.get('title') as string,
45        description: formData.get('description') as string,
46        status: formData.get('status') as Task['status'],
47        priority: formData.get('priority') as Task['priority'],
48        createdAt: new Date(),
49        updatedAt: new Date()
50      }
51    });
52    
53    // Wykonaj rzeczywistą akcję
54    await formAction(formData);
55    
56    if (state.success) {
57      onSuccess?.();
58    }
59  };
60  
61  return (
62    <form 
63      id={formId}
64      action={handleSubmit}
65      className="task-form"
66    >
67      <div className="form-group">
68        <label htmlFor={titleId}>Tytuł zadania</label>
69        <input
70          id={titleId}
71          name="title"
72          type="text"
73          required
74          defaultValue={optimisticState.data?.title || ''}
75          className={optimisticState.errors?.title ? 'error' : ''}
76        />
77        {optimisticState.errors?.title && (
78          <span className="error-message" role="alert">
79            {optimisticState.errors.title}
80          </span>
81        )}
82      </div>
83      
84      <div className="form-group">
85        <label htmlFor={descriptionId}>Opis</label>
86        <textarea
87          id={descriptionId}
88          name="description"
89          rows={4}
90          defaultValue={optimisticState.data?.description || ''}
91        />
92      </div>
93      
94      <div className="form-group">
95        <label>Status</label>
96        <select name="status" defaultValue={optimisticState.data?.status || 'pending'}>
97          <option value="pending">Oczekujące</option>
98          <option value="in_progress">W trakcie</option>
99          <option value="completed">Zakończone</option>
100        </select>
101      </div>
102      
103      <div className="form-group">
104        <label>Priorytet</label>
105        <select name="priority" defaultValue={optimisticState.data?.priority || 'medium'}>
106          <option value="low">Niski</option>
107          <option value="medium">Średni</option>
108          <option value="high">Wysoki</option>
109        </select>
110      </div>
111      
112      <FormSubmitButton />
113      
114      {optimisticState.optimistic && (
115        <div className="optimistic-indicator">
116          Zapisywanie...
117        </div>
118      )}
119      
120      {optimisticState.success && !optimisticState.optimistic && (
121        <div className="success-message">
122          Zadanie zostało {task ? 'zaktualizowane' : 'utworzone'}!
123        </div>
124      )}
125    </form>
126  );
127}
128
129// Komponent wykorzystujący useFormStatus
130function FormSubmitButton() {
131  const { pending } = useFormStatus();
132  
133  return (
134    <button 
135      type="submit" 
136      disabled={pending}
137      className={pending ? 'loading' : ''}
138    >
139      {pending ? 'Zapisywanie...' : 'Zapisz zadanie'}
140    </button>
141  );
142}

Krok 6: Server Actions

1// lib/actions.ts
2'use server';
3
4import { revalidatePath } from 'next/cache';
5import { redirect } from 'next/navigation';
6import { z } from 'zod';
7
8const TaskSchema = z.object({
9  title: z.string().min(1, 'Tytuł jest wymagany').max(100, 'Tytuł jest za długi'),
10  description: z.string().optional(),
11  status: z.enum(['pending', 'in_progress', 'completed']),
12  priority: z.enum(['low', 'medium', 'high']),
13  dueDate: z.string().optional().transform(str => str ? new Date(str) : undefined)
14});
15
16export async function createTask(prevState: any, formData: FormData) {
17  try {
18    const validatedFields = TaskSchema.safeParse({
19      title: formData.get('title'),
20      description: formData.get('description'),
21      status: formData.get('status'),
22      priority: formData.get('priority'),
23      dueDate: formData.get('dueDate')
24    });
25    
26    if (!validatedFields.success) {
27      return {
28        success: false,
29        errors: validatedFields.error.flatten().fieldErrors,
30        data: null
31      };
32    }
33    
34    const task = await db.tasks.create({
35      data: {
36        ...validatedFields.data,
37        id: generateId(),
38        createdAt: new Date(),
39        updatedAt: new Date()
40      }
41    });
42    
43    // Revalidate cache
44    revalidatePath('/dashboard');
45    revalidatePath('/tasks');
46    
47    return {
48      success: true,
49      errors: {},
50      data: task
51    };
52    
53  } catch (error) {
54    return {
55      success: false,
56      errors: { _form: ['Wystąpił błąd podczas tworzenia zadania'] },
57      data: null
58    };
59  }
60}
61
62export async function updateTask(taskId: string, prevState: any, formData: FormData) {
63  try {
64    const validatedFields = TaskSchema.safeParse({
65      title: formData.get('title'),
66      description: formData.get('description'),
67      status: formData.get('status'),
68      priority: formData.get('priority'),
69      dueDate: formData.get('dueDate')
70    });
71    
72    if (!validatedFields.success) {
73      return {
74        success: false,
75        errors: validatedFields.error.flatten().fieldErrors,
76        data: null
77      };
78    }
79    
80    const task = await db.tasks.update({
81      where: { id: taskId },
82      data: {
83        ...validatedFields.data,
84        updatedAt: new Date()
85      }
86    });
87    
88    revalidatePath(`/tasks/${taskId}`);
89    revalidatePath('/dashboard');
90    
91    return {
92      success: true,
93      errors: {},
94      data: task
95    };
96    
97  } catch (error) {
98    return {
99      success: false,
100      errors: { _form: ['Wystąpił błąd podczas aktualizacji zadania'] },
101      data: null
102    };
103  }
104}

Krok 7: Zaawansowane wzorce Suspense

1// components/tasks/TasksList.tsx (Server Component)
2import { Suspense } from 'react';
3import { TaskCard } from './TaskCard';
4import { TasksFilter } from './TasksFilter';
5
6async function TasksList({ searchParams }: { searchParams: any }) {
7  return (
8    <div className="tasks-page">
9      <div className="tasks-header">
10        <h1>Zadania</h1>
11        
12        {/* Filter nie wymaga Suspense - szybko się ładuje */}
13        <TasksFilter />
14      </div>
15      
16      <div className="tasks-content">
17        {/* Główna lista zadań */}
18        <Suspense fallback={<TasksListSkeleton />}>
19          <TasksGrid searchParams={searchParams} />
20        </Suspense>
21        
22        {/* Sidebar z dodatkowymi informacjami */}
23        <aside className="tasks-sidebar">
24          <Suspense fallback={<StatsSkeleton />}>
25            <TasksStatistics />
26          </Suspense>
27          
28          <Suspense fallback={<ActivitySkeleton />}>
29            <RecentActivity />
30          </Suspense>
31        </aside>
32      </div>
33    </div>
34  );
35}
36
37// Komponent z długim czasem ładowania
38async function TasksGrid({ searchParams }: { searchParams: any }) {
39  // Symuluj długie zapytanie
40  const tasks = await getFilteredTasks(searchParams);
41  
42  return (
43    <div className="tasks-grid">
44      {tasks.map(task => (
45        <Suspense 
46          key={task.id} 
47          fallback={<TaskCardSkeleton />}
48        >
49          <TaskCard task={task} />
50        </Suspense>
51      ))}
52    </div>
53  );
54}

Krok 8: Accessibility z useId

1// components/ui/FormField.tsx
2'use client';
3
4import { useId, forwardRef } from 'react';
5
6interface FormFieldProps {
7  label: string;
8  error?: string;
9  help?: string;
10  required?: boolean;
11  children: React.ReactNode;
12}
13
14export const FormField = forwardRef<HTMLDivElement, FormFieldProps>(
15  ({ label, error, help, required, children }, ref) => {
16    const fieldId = useId();
17    const errorId = useId();
18    const helpId = useId();
19    
20    return (
21      <div ref={ref} className="form-field">
22        <label 
23          htmlFor={fieldId}
24          className={required ? 'required' : ''}
25        >
26          {label}
27          {required && <span aria-hidden="true">*</span>}
28        </label>
29        
30        <div className="field-input">
31          {React.cloneElement(children as React.ReactElement, {
32            id: fieldId,
33            'aria-describedby': [
34              help ? helpId : null,
35              error ? errorId : null
36            ].filter(Boolean).join(' ') || undefined,
37            'aria-invalid': error ? 'true' : undefined,
38            required
39          })}
40        </div>
41        
42        {help && (
43          <div id={helpId} className="field-help">
44            {help}
45          </div>
46        )}
47        
48        {error && (
49          <div 
50            id={errorId} 
51            className="field-error" 
52            role="alert"
53            aria-live="polite"
54          >
55            {error}
56          </div>
57        )}
58      </div>
59    );
60  }
61);
62
63FormField.displayName = 'FormField';

Krok 9: Performance Monitoring

1// components/providers/PerformanceProvider.tsx
2'use client';
3
4import { createContext, useContext, useEffect, useState } from 'react';
5
6interface PerformanceMetrics {
7  renderTime: number;
8  interactionTime: number;
9  suspenseTime: number;
10}
11
12const PerformanceContext = createContext<{
13  metrics: PerformanceMetrics;
14  recordMetric: (type: keyof PerformanceMetrics, time: number) => void;
15}>({
16  metrics: { renderTime: 0, interactionTime: 0, suspenseTime: 0 },
17  recordMetric: () => {}
18});
19
20export function PerformanceProvider({ children }: { children: React.ReactNode }) {
21  const [metrics, setMetrics] = useState<PerformanceMetrics>({
22    renderTime: 0,
23    interactionTime: 0,
24    suspenseTime: 0
25  });
26  
27  const recordMetric = (type: keyof PerformanceMetrics, time: number) => {
28    setMetrics(prev => ({
29      ...prev,
30      [type]: time
31    }));
32    
33    // Wyślij metryki do systemu analitycznego
34    if (time > getThreshold(type)) {
35      analytics.track('performance_issue', {
36        type,
37        time,
38        url: window.location.pathname
39      });
40    }
41  };
42  
43  useEffect(() => {
44    // Obserwuj Web Vitals
45    const observer = new PerformanceObserver((list) => {
46      for (const entry of list.getEntries()) {
47        if (entry.entryType === 'measure') {
48          recordMetric('renderTime', entry.duration);
49        }
50      }
51    });
52    
53    observer.observe({ entryTypes: ['measure'] });
54    
55    return () => observer.disconnect();
56  }, []);
57  
58  return (
59    <PerformanceContext.Provider value={{ metrics, recordMetric }}>
60      {children}
61    </PerformanceContext.Provider>
62  );
63}
64
65export const usePerformance = () => useContext(PerformanceContext);
66
67function getThreshold(type: keyof PerformanceMetrics): number {
68  const thresholds = {
69    renderTime: 100,      // 100ms
70    interactionTime: 50,  // 50ms
71    suspenseTime: 1000    // 1s
72  };
73  return thresholds[type];
74}

Podsumowanie projektu

Ten projekt demonstruje:

  1. Concurrent Features: useTransition, useDeferredValue dla responsywnego UI
  2. Form Handling: useActionState, useFormStatus z Server Actions
  3. Accessibility: useId dla unikatowych identyfikatorów
  4. Optimistic Updates: useOptimistic dla natychmiastowej informacji zwrotnej
  5. Server Components: Renderowanie na serwerze z równoległym pobieraniem danych
  6. Suspense Patterns: Hierarchiczne loading z streaming
  7. Performance: Monitoring i optymalizacja wydajności
  8. Error Handling: Granularne Error Boundaries
  9. Type Safety: Pełne wykorzystanie TypeScript

Następne kroki do rozszerzenia:

  1. Dodaj real-time synchronizację z WebSockets
  2. Implementuj offline support z Service Workers
  3. Dodaj zaawansowane filtry i sortowanie
  4. Stwórz system uprawnień użytkowników
  5. Zintegruj z systemem powiadomień push
  6. Dodaj eksport danych do różnych formatów
  7. Implementuj dark mode z CSS-in-JS
  8. Stwórz mobilną aplikację z React Native

Ten projekt to solidna podstawa do budowania nowoczesnych aplikacji React wykorzystujących najnowsze funkcjonalności frameworka.

Przejdź do CodeWorlds